Add edit drawers to file/image interfaces

Closes #3752
This commit is contained in:
rijkvanzanten
2021-01-21 11:49:35 -05:00
parent 833b35a6bc
commit 7bbce08b98
3 changed files with 131 additions and 32 deletions

View File

@@ -28,7 +28,10 @@
</div>
</template>
<template #append>
<v-icon class="deselect" name="close" v-if="file" @click.stop="$emit('input', null)" />
<template v-if="file">
<v-icon name="open_in_new" class="edit" v-tooltip="$t('edit')" @click.stop="editDrawerActive = true" />
<v-icon class="deselect" name="close" @click.stop="$emit('input', null)" v-tooltip="$t('deselect')" />
</template>
<v-icon v-else name="attach_file" />
</template>
</v-input>
@@ -67,6 +70,15 @@
</v-list>
</v-menu>
<drawer-item
v-if="!disabled && file"
:active.sync="editDrawerActive"
collection="directus_files"
:primary-key="file.id"
:edits="edits"
@input="stageEdits"
/>
<v-dialog :active="activeDialog === 'upload'" @esc="activeDialog = null" @toggle="activeDialog = null">
<v-card>
<v-card-title>{{ $t('upload_from_device') }}</v-card-title>
@@ -118,18 +130,19 @@ import readableMimeType from '@/utils/readable-mime-type';
import { getRootPath } from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
import { addTokenToURL } from '@/api';
import DrawerItem from '../../views/private/components/drawer-item';
type FileInfo = {
id: number;
id: string;
title: string;
type: string;
};
export default defineComponent({
components: { DrawerCollection },
components: { DrawerCollection, DrawerItem },
props: {
value: {
type: String,
type: [String, Object],
default: null,
},
disabled: {
@@ -148,7 +161,10 @@ export default defineComponent({
return readableMimeType(file.value.type, true);
});
const assetURL = computed(() => getRootPath() + `assets/${props.value}`);
const assetURL = computed(() => {
const id = typeof props.value === 'string' ? props.value : (props.value as Record<string, any>)?.id;
return getRootPath() + `assets/${id}`;
});
const imageThumbnail = computed(() => {
if (file.value === null || props.value === null) return null;
@@ -158,8 +174,11 @@ export default defineComponent({
return addTokenToURL(url);
});
const { edits, stageEdits } = useEdits();
const { url, isValidURL, loading: urlLoading, importFromURL } = useURLImport();
const editDrawerActive = ref(false);
return {
activeDialog,
setSelection,
@@ -173,6 +192,9 @@ export default defineComponent({
importFromURL,
isValidURL,
assetURL,
editDrawerActive,
edits,
stageEdits,
};
function useFile() {
@@ -191,13 +213,22 @@ export default defineComponent({
loading.value = true;
try {
const response = await api.get(`/files/${props.value}`, {
const id = typeof props.value === 'string' ? props.value : (props.value as Record<string, any>)?.id;
const response = await api.get(`/files/${id}`, {
params: {
fields: ['title', 'type', 'filename_download'],
fields: ['id', 'title', 'type', 'filename_download'],
},
});
file.value = response.data.data;
if (props.value !== null && typeof props.value === 'object') {
file.value = {
...response.data.data,
...props.value,
};
} else {
file.value = response.data.data;
}
} catch (err) {
unexpectedError(err);
} finally {
@@ -255,6 +286,29 @@ export default defineComponent({
}
}
}
function useEdits() {
const edits = computed(() => {
// If the current value isn't a primitive, it means we've already staged some changes
// This ensures we continue on those changes instead of starting over
if (props.value && typeof props.value === 'object') {
return props.value;
}
return {};
});
return { edits, stageEdits };
function stageEdits(newEdits: Record<string, any>) {
if (!file.value) return;
emit('input', {
id: file.value.id,
...newEdits,
});
}
}
},
});
</script>
@@ -304,4 +358,12 @@ export default defineComponent({
.deselect:hover {
--v-icon-color: var(--danger);
}
.edit {
margin-right: 4px;
&:hover {
--v-icon-color: var(--foreground-normal);
}
}
</style>

View File

@@ -18,8 +18,8 @@
<v-button icon rounded :href="downloadSrc" :download="image.filename_download" v-tooltip="$t('download')">
<v-icon name="get_app" />
</v-button>
<v-button icon rounded @click="editorActive = true" v-tooltip="$t('edit')">
<v-icon name="crop_rotate" />
<v-button icon rounded @click="editDrawerActive = true" v-tooltip="$t('edit')">
<v-icon name="open_in_new" />
</v-button>
<v-button icon rounded @click="deselect" v-tooltip="$t('deselect')">
<v-icon name="close" />
@@ -31,12 +31,15 @@
<div class="meta">{{ meta }}</div>
</div>
<image-editor
v-if="image && image.type.startsWith('image')"
:id="image.id"
@refresh="changeCacheBuster"
v-model="editorActive"
<drawer-item
v-if="!disabled && image"
:active.sync="editDrawerActive"
collection="directus_files"
:primary-key="image.id"
:edits="edits"
@input="stageEdits"
/>
<file-lightbox v-model="lightboxActive" :id="image.id" />
</div>
<v-upload v-else @input="setImage" from-library from-url />
@@ -54,6 +57,7 @@ import { nanoid } from 'nanoid';
import { getRootPath } from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
import { addTokenToURL } from '@/api';
import DrawerItem from '../../views/private/components/drawer-item';
type Image = {
id: string; // uuid
@@ -65,10 +69,10 @@ type Image = {
};
export default defineComponent({
components: { FileLightbox, ImageEditor },
components: { FileLightbox, ImageEditor, DrawerItem },
props: {
value: {
type: String,
type: [String, Object],
default: null,
},
disabled: {
@@ -80,7 +84,7 @@ export default defineComponent({
const loading = ref(false);
const image = ref<Image | null>(null);
const lightboxActive = ref(false);
const editorActive = ref(false);
const editDrawerActive = ref(false);
const cacheBuster = ref(nanoid());
@@ -114,43 +118,56 @@ export default defineComponent({
watch(
() => props.value,
(newID, oldID) => {
if (newID === oldID) return;
(newValue, oldValue) => {
if (newValue === oldValue) return;
if (newID) {
if (newValue) {
fetchImage();
}
if (oldID && newID === null) {
if (oldValue && newValue === null) {
deselect();
}
}
);
const { edits, stageEdits } = useEdits();
return {
loading,
image,
src,
meta,
lightboxActive,
editorActive,
editDrawerActive,
changeCacheBuster,
setImage,
deselect,
downloadSrc,
edits,
stageEdits,
};
async function fetchImage() {
loading.value = true;
try {
const response = await api.get(`/files/${props.value}`, {
const id = typeof props.value === 'string' ? props.value : (props.value as Record<string, any>)?.id;
const response = await api.get(`/files/${id}`, {
params: {
fields: ['id', 'title', 'width', 'height', 'filesize', 'type', 'filename_download'],
},
});
image.value = response.data.data;
if (props.value !== null && typeof props.value === 'object') {
image.value = {
...response.data.data,
...props.value,
};
} else {
image.value = response.data.data;
}
} catch (err) {
unexpectedError(err);
} finally {
@@ -173,7 +190,30 @@ export default defineComponent({
loading.value = false;
image.value = null;
lightboxActive.value = false;
editorActive.value = false;
editDrawerActive.value = false;
}
function useEdits() {
const edits = computed(() => {
// If the current value isn't a primitive, it means we've already staged some changes
// This ensures we continue on those changes instead of starting over
if (props.value && typeof props.value === 'object') {
return props.value;
}
return {};
});
return { edits, stageEdits };
function stageEdits(newEdits: Record<string, any>) {
if (!image.value) return;
emit('input', {
id: image.value.id,
...newEdits,
});
}
}
},
});

View File

@@ -106,9 +106,8 @@ export default defineComponent({
const showDivider = computed(() => {
return (
fieldsStore
.getFieldsForCollection(props.collection)
.filter((field: Field) => field.meta?.hidden !== true).length > 0
fieldsStore.getFieldsForCollection(props.collection).filter((field: Field) => field.meta?.hidden !== true)
.length > 0
);
});
@@ -243,9 +242,7 @@ export default defineComponent({
const relations = relationsStore.getRelationsForField(props.collection, props.junctionField);
const relationForField = relations.find((relation: Relation) => {
return (
relation.many_collection === props.collection && relation.many_field === props.junctionField
);
return relation.many_collection === props.collection && relation.many_field === props.junctionField;
});
if (relationForField.one_collection) return relationForField.one_collection;