mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user