Re-style file detail preview + replace interaction (#12689)

* Return BigIntegers as Strings in GraphQL

Fixes #12051

* Redesign fallback image style

* Re-add replace button in new position
This commit is contained in:
Rijk van Zanten
2022-04-11 18:24:12 -04:00
committed by GitHub
parent f92fb0762f
commit f89aa95140
6 changed files with 322 additions and 330 deletions

View File

@@ -16,36 +16,29 @@
</v-dialog>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false,
},
file: {
type: Object,
default: () => ({}),
},
preset: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue', 'replaced'],
setup(_props, { emit }) {
const { t } = useI18n();
interface Props {
modelValue?: boolean;
file?: Record<string, any>;
preset?: Record<string, any>;
}
return { t, uploaded };
function uploaded() {
emit('update:modelValue', false);
emit('replaced');
}
},
withDefaults(defineProps<Props>(), {
modelValue: false,
file: () => ({}),
preset: () => ({}),
});
const emit = defineEmits(['update:modelValue', 'replaced']);
const { t } = useI18n();
function uploaded() {
emit('update:modelValue', false);
emit('replaced');
}
</script>
<style lang="scss" scoped>

View File

@@ -108,15 +108,20 @@
</template>
<div class="file-item">
<file-preview
v-if="isBatch === false && item"
:src="fileSrc"
:mime="item.type"
:width="item.width"
:height="item.height"
:title="item.title"
@click="replaceFileDialogActive = true"
/>
<div class="preview">
<file-preview
v-if="isBatch === false && item"
:src="fileSrc"
:mime="item.type"
:width="item.width"
:height="item.height"
:title="item.title"
/>
<button v-if="isBatch === false && item" class="replace-toggle" @click="replaceFileDialogActive = true">
{{ t('replace_file') }}
</button>
</div>
<image-editor
v-if="item && item.type.startsWith('image')"
@@ -155,7 +160,7 @@
<file-info-sidebar-detail :file="item" />
<revisions-drawer-detail
v-if="isBatch === false && isNew === false && revisionsAllowed"
ref="revisionsDrawerDetail"
ref="revisionsDrawerDetailRef"
collection="directus_files"
:primary-key="primaryKey"
/>
@@ -170,292 +175,229 @@
</private-view>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed, toRefs, ref, watch, ComponentPublicInstance } from 'vue';
import FilesNavigation from '../components/navigation.vue';
import { useRouter } from 'vue-router';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import CommentsSidebarDetail from '@/views/private/components/comments-sidebar-detail';
import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
import FilePreview from '@/views/private/components/file-preview';
import ImageEditor from '@/views/private/components/image-editor';
import { Field } from '@directus/shared/types';
import FileInfoSidebarDetail from '../components/file-info-sidebar-detail.vue';
import FolderPicker from '@/views/private/components/folder-picker/folder-picker.vue';
<script lang="ts" setup>
import api, { addTokenToURL } from '@/api';
import { getRootPath } from '@/utils/get-root-path';
import FilesNotFound from './not-found.vue';
import useShortcut from '@/composables/use-shortcut';
import ReplaceFile from '../components/replace-file.vue';
import useEditsGuard from '@/composables/use-edits-guard';
import useItem from '@/composables/use-item';
import { usePermissions } from '@/composables/use-permissions';
import useShortcut from '@/composables/use-shortcut';
import { getRootPath } from '@/utils/get-root-path';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
import useEditsGuard from '@/composables/use-edits-guard';
import CommentsSidebarDetail from '@/views/private/components/comments-sidebar-detail';
import FilePreview from '@/views/private/components/file-preview';
import FolderPicker from '@/views/private/components/folder-picker/folder-picker.vue';
import ImageEditor from '@/views/private/components/image-editor';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import SaveOptions from '@/views/private/components/save-options';
import { Field } from '@directus/shared/types';
import { ComponentPublicInstance, computed, ref, toRefs, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FileInfoSidebarDetail from '../components/file-info-sidebar-detail.vue';
import FilesNavigation from '../components/navigation.vue';
import ReplaceFile from '../components/replace-file.vue';
import FilesNotFound from './not-found.vue';
export default defineComponent({
name: 'FilesItem',
components: {
FilesNavigation,
RevisionsDrawerDetail,
CommentsSidebarDetail,
FilePreview,
ImageEditor,
FileInfoSidebarDetail,
FolderPicker,
FilesNotFound,
ReplaceFile,
SaveOptions,
},
props: {
primaryKey: {
type: String,
required: true,
},
},
setup(props) {
const { t } = useI18n();
interface Props {
primaryKey: string;
}
const router = useRouter();
const props = defineProps<Props>();
const form = ref<HTMLElement>();
const { primaryKey } = toRefs(props);
const { breadcrumb } = useBreadcrumb();
const replaceFileDialogActive = ref(false);
const { t } = useI18n();
const revisionsDrawerDetail = ref<ComponentPublicInstance | null>(null);
const router = useRouter();
const {
isNew,
edits,
hasEdits,
item,
saving,
loading,
error,
save,
remove,
deleting,
saveAsCopy,
isBatch,
refresh,
validationErrors,
} = useItem(ref('directus_files'), primaryKey);
const form = ref<HTMLElement>();
const { primaryKey } = toRefs(props);
const { breadcrumb } = useBreadcrumb();
const replaceFileDialogActive = ref(false);
const isSavable = computed(() => saveAllowed.value && hasEdits.value);
const revisionsDrawerDetailRef = ref<ComponentPublicInstance | null>(null);
const { confirmLeave, leaveTo } = useEditsGuard(hasEdits);
const {
isNew,
edits,
hasEdits,
item,
saving,
loading,
save,
remove,
deleting,
saveAsCopy,
isBatch,
refresh,
validationErrors,
} = useItem(ref('directus_files'), primaryKey);
const confirmDelete = ref(false);
const editActive = ref(false);
const fileSrc = computed(() => {
if (item.value && item.value.modified_on) {
return addTokenToURL(
getRootPath() + `assets/${props.primaryKey}?cache-buster=${item.value.modified_on}&key=system-large-contain`
);
}
const isSavable = computed(() => saveAllowed.value && hasEdits.value);
return addTokenToURL(getRootPath() + `assets/${props.primaryKey}?key=system-large-contain`);
});
const { confirmLeave, leaveTo } = useEditsGuard(hasEdits);
// These are the fields that will be prevented from showing up in the form because they'll be shown in the sidebar
const fieldsDenyList: string[] = [
'type',
'width',
'height',
'filesize',
'checksum',
'uploaded_by',
'uploaded_on',
'modified_by',
'modified_on',
'duration',
'folder',
'charset',
'embed',
];
const to = computed(() => {
if (item.value && item.value?.folder) return `/files/folders/${item.value.folder}`;
else return '/files';
});
const { moveToDialogActive, moveToFolder, moving, selectedFolder } = useMovetoFolder();
useShortcut('meta+s', saveAndStay, form);
const { createAllowed, deleteAllowed, saveAllowed, updateAllowed, fields, revisionsAllowed } = usePermissions(
ref('directus_files'),
item,
isNew
const confirmDelete = ref(false);
const editActive = ref(false);
const fileSrc = computed(() => {
if (item.value && item.value.modified_on) {
return addTokenToURL(
getRootPath() + `assets/${props.primaryKey}?cache-buster=${item.value.modified_on}&key=system-large-contain`
);
}
const fieldsFiltered = computed(() => {
return fields.value.filter((field: Field) => fieldsDenyList.includes(field.field) === false);
});
return {
t,
router,
item,
loading,
error,
isNew,
breadcrumb,
edits,
hasEdits,
saving,
saveAndQuit,
deleteAndQuit,
confirmDelete,
deleting,
saveAndStay,
saveAsCopyAndNavigate,
discardAndStay,
isBatch,
editActive,
revisionsDrawerDetail,
confirmLeave,
leaveTo,
discardAndLeave,
downloadFile,
moveToDialogActive,
moveToFolder,
moving,
selectedFolder,
fileSrc,
form,
to,
replaceFileDialogActive,
refresh,
createAllowed,
deleteAllowed,
saveAllowed,
updateAllowed,
fields,
fieldsFiltered,
revisionsAllowed,
validationErrors,
isSavable,
};
function useBreadcrumb() {
const breadcrumb = computed(() => {
if (!item?.value?.folder) {
return [
{
name: t('file_library'),
to: '/files',
},
];
}
return [
{
name: t('file_library'),
to: { path: `/files/folders/${item.value.folder}` },
},
];
});
return { breadcrumb };
}
async function saveAndQuit() {
try {
await save();
router.push(to.value);
} catch {
// `save` will show unexpected error dialog
}
}
async function saveAndStay() {
try {
await save();
revisionsDrawerDetail.value?.refresh?.();
} catch {
// `save` will show unexpected error dialog
}
}
async function saveAsCopyAndNavigate() {
const newPrimaryKey = await saveAsCopy();
if (newPrimaryKey) router.push(`/files/${newPrimaryKey}`);
}
async function deleteAndQuit() {
try {
await remove();
router.replace(to.value);
} catch {
// `remove` will show the unexpected error dialog
confirmDelete.value = false;
}
}
function discardAndLeave() {
if (!leaveTo.value) return;
edits.value = {};
confirmLeave.value = false;
router.push(leaveTo.value);
}
function discardAndStay() {
edits.value = {};
confirmLeave.value = false;
}
function downloadFile() {
const filePath = addTokenToURL(getRootPath() + `assets/${props.primaryKey}?download`);
window.open(filePath, '_blank');
}
function useMovetoFolder() {
const moveToDialogActive = ref(false);
const moving = ref(false);
const selectedFolder = ref<number | null>();
watch(item, () => {
selectedFolder.value = item.value?.folder || null;
});
return { moveToDialogActive, moving, moveToFolder, selectedFolder };
async function moveToFolder() {
moving.value = true;
try {
const response = await api.patch(
`/files/${props.primaryKey}`,
{
folder: selectedFolder.value,
},
{
params: {
fields: 'folder.name',
},
}
);
await refresh();
const folder = response.data.data.folder?.name || t('file_library');
notify({
title: t('file_moved', { folder }),
icon: 'folder_move',
});
} catch (err: any) {
unexpectedError(err);
} finally {
moveToDialogActive.value = false;
moving.value = false;
}
}
}
},
return addTokenToURL(getRootPath() + `assets/${props.primaryKey}?key=system-large-contain`);
});
// These are the fields that will be prevented from showing up in the form because they'll be shown in the sidebar
const fieldsDenyList: string[] = [
'type',
'width',
'height',
'filesize',
'checksum',
'uploaded_by',
'uploaded_on',
'modified_by',
'modified_on',
'duration',
'folder',
'charset',
'embed',
];
const to = computed(() => {
if (item.value && item.value?.folder) return `/files/folders/${item.value.folder}`;
else return '/files';
});
const { moveToDialogActive, moveToFolder, moving, selectedFolder } = useMovetoFolder();
useShortcut('meta+s', saveAndStay, form);
const { createAllowed, deleteAllowed, saveAllowed, updateAllowed, fields, revisionsAllowed } = usePermissions(
ref('directus_files'),
item,
isNew
);
const fieldsFiltered = computed(() => {
return fields.value.filter((field: Field) => fieldsDenyList.includes(field.field) === false);
});
function useBreadcrumb() {
const breadcrumb = computed(() => {
if (!item?.value?.folder) {
return [
{
name: t('file_library'),
to: '/files',
},
];
}
return [
{
name: t('file_library'),
to: { path: `/files/folders/${item.value.folder}` },
},
];
});
return { breadcrumb };
}
async function saveAndQuit() {
try {
await save();
router.push(to.value);
} catch {
// `save` will show unexpected error dialog
}
}
async function saveAndStay() {
try {
await save();
revisionsDrawerDetailRef.value?.refresh?.();
} catch {
// `save` will show unexpected error dialog
}
}
async function saveAsCopyAndNavigate() {
const newPrimaryKey = await saveAsCopy();
if (newPrimaryKey) router.push(`/files/${newPrimaryKey}`);
}
async function deleteAndQuit() {
try {
await remove();
router.replace(to.value);
} catch {
// `remove` will show the unexpected error dialog
confirmDelete.value = false;
}
}
function discardAndLeave() {
if (!leaveTo.value) return;
edits.value = {};
confirmLeave.value = false;
router.push(leaveTo.value);
}
function discardAndStay() {
edits.value = {};
confirmLeave.value = false;
}
function downloadFile() {
const filePath = addTokenToURL(getRootPath() + `assets/${props.primaryKey}?download`);
window.open(filePath, '_blank');
}
function useMovetoFolder() {
const moveToDialogActive = ref(false);
const moving = ref(false);
const selectedFolder = ref<number | null>();
watch(item, () => {
selectedFolder.value = item.value?.folder || null;
});
return { moveToDialogActive, moving, moveToFolder, selectedFolder };
async function moveToFolder() {
moving.value = true;
try {
const response = await api.patch(
`/files/${props.primaryKey}`,
{
folder: selectedFolder.value,
},
{
params: {
fields: 'folder.name',
},
}
);
await refresh();
const folder = response.data.data.folder?.name || t('file_library');
notify({
title: t('file_moved', { folder }),
icon: 'folder_move',
});
} catch (err: any) {
unexpectedError(err);
} finally {
moveToDialogActive.value = false;
moving.value = false;
}
}
}
</script>
<style lang="scss" scoped>
@@ -472,4 +414,15 @@ export default defineComponent({
padding: var(--content-padding);
padding-bottom: var(--content-padding-bottom);
}
.preview {
margin-bottom: var(--form-vertical-gap);
}
.replace-toggle {
color: var(--primary);
cursor: pointer;
font-weight: 600;
margin-top: 12px;
}
</style>