From b6524b20ebf9c13328ba11133b7497e2f9e6762c Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 20 Jan 2021 21:26:46 -0500 Subject: [PATCH 1/7] Tweak perf values --- api/src/services/server.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/services/server.ts b/api/src/services/server.ts index 20f998f703..17d19c9b0a 100644 --- a/api/src/services/server.ts +++ b/api/src/services/server.ts @@ -151,7 +151,7 @@ export class ServerService { checks[`${client}:responseTime`][0].observedValue = +(endTime - startTime).toFixed(3); if ( - checks[`${client}:responseTime`][0].observedValue! > 50 && + checks[`${client}:responseTime`][0].observedValue! > 150 && checks[`${client}:responseTime`][0].status !== 'error' ) { checks[`${client}:responseTime`][0].status = 'warn'; @@ -205,7 +205,7 @@ export class ServerService { const endTime = performance.now(); checks['cache:responseTime'][0].observedValue = +(endTime - startTime).toFixed(3); - if (checks['cache:responseTime'][0].observedValue > 50 && checks['cache:responseTime'][0].status !== 'error') { + if (checks['cache:responseTime'][0].observedValue > 150 && checks['cache:responseTime'][0].status !== 'error') { checks['cache:responseTime'][0].status = 'warn'; } } @@ -242,7 +242,7 @@ export class ServerService { checks['rateLimiter:responseTime'][0].observedValue = +(endTime - startTime).toFixed(3); if ( - checks['rateLimiter:responseTime'][0].observedValue > 50 && + checks['rateLimiter:responseTime'][0].observedValue > 150 && checks['rateLimiter:responseTime'][0].status !== 'error' ) { checks['rateLimiter:responseTime'][0].status = 'warn'; @@ -281,7 +281,7 @@ export class ServerService { checks[`storage:${location}:responseTime`][0].observedValue = +(endTime - startTime).toFixed(3); if ( - checks[`storage:${location}:responseTime`][0].observedValue! > 500 && + checks[`storage:${location}:responseTime`][0].observedValue! > 750 && checks[`storage:${location}:responseTime`][0].status !== 'error' ) { checks[`storage:${location}:responseTime`][0].status = 'warn'; From d2705713e1b949bd6e4e5c87fff21727b4cdd53d Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 20 Jan 2021 21:37:46 -0500 Subject: [PATCH 2/7] Use body for moving files to folder Fixes #19 --- api/src/controllers/files.ts | 56 ++++++++++++++++++++- app/src/modules/files/routes/collection.vue | 12 +++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/api/src/controllers/files.ts b/api/src/controllers/files.ts index b942f9bbe5..dc0d5f040b 100644 --- a/api/src/controllers/files.ts +++ b/api/src/controllers/files.ts @@ -7,7 +7,7 @@ import formatTitle from '@directus/format-title'; import env from '../env'; import axios from 'axios'; import Joi from 'joi'; -import { InvalidPayloadException, ForbiddenException } from '../exceptions'; +import { InvalidPayloadException, ForbiddenException, FailedValidationException } from '../exceptions'; import url from 'url'; import path from 'path'; import useCollection from '../middleware/use-collection'; @@ -218,6 +218,60 @@ router.get( respond ); +router.patch( + '/', + asyncHandler(async (req, res, next) => { + const service = new FilesService({ + accountability: req.accountability, + schema: req.schema, + }); + + if (Array.isArray(req.body)) { + const primaryKeys = await service.update(req.body); + + try { + const result = await service.readByKey(primaryKeys, req.sanitizedQuery); + res.locals.payload = { data: result || null }; + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + } + + const updateSchema = Joi.object({ + keys: Joi.array().items(Joi.alternatives(Joi.string(), Joi.number())).required(), + data: Joi.object().required().unknown(), + }); + + const { error } = updateSchema.validate(req.body); + + if (error) { + throw new FailedValidationException(error.details[0]); + } + + const primaryKeys = await service.update(req.body.data, req.body.keys); + + try { + const result = await service.readByKey(primaryKeys, req.sanitizedQuery); + res.locals.payload = { data: result || null }; + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + }), + respond +); + router.patch( '/:pk', multipartHandler, diff --git a/app/src/modules/files/routes/collection.vue b/app/src/modules/files/routes/collection.vue index 74b8cc28ff..e759d0cb26 100644 --- a/app/src/modules/files/routes/collection.vue +++ b/app/src/modules/files/routes/collection.vue @@ -221,9 +221,7 @@ export default defineComponent({ const userStore = useUserStore(); - const { layout, layoutOptions, layoutQuery, filters, searchQuery, resetPreset } = usePreset( - ref('directus_files') - ); + const { layout, layoutOptions, layoutQuery, filters, searchQuery, resetPreset } = usePreset(ref('directus_files')); const { confirmDelete, deleting, batchDelete, error: deleteError, batchEditActive } = useBatch(); @@ -412,9 +410,13 @@ export default defineComponent({ async function moveToFolder() { moving.value = true; + try { - await api.patch(`/files/${selection.value}`, { - folder: selectedFolder.value, + await api.patch(`/files`, { + keys: selection.value, + data: { + folder: selectedFolder.value, + }, }); selection.value = []; From 7bbce08b981c696889e037b406b5d7a9252c1df2 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 21 Jan 2021 11:49:35 -0500 Subject: [PATCH 3/7] Add edit drawers to file/image interfaces Closes #3752 --- app/src/interfaces/file/file.vue | 78 +++++++++++++++++-- app/src/interfaces/image/image.vue | 76 +++++++++++++----- .../components/drawer-item/drawer-item.vue | 9 +-- 3 files changed, 131 insertions(+), 32 deletions(-) diff --git a/app/src/interfaces/file/file.vue b/app/src/interfaces/file/file.vue index f939be9221..796f7601d0 100644 --- a/app/src/interfaces/file/file.vue +++ b/app/src/interfaces/file/file.vue @@ -28,7 +28,10 @@ @@ -67,6 +70,15 @@ + + {{ $t('upload_from_device') }} @@ -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)?.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)?.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) { + if (!file.value) return; + + emit('input', { + id: file.value.id, + ...newEdits, + }); + } + } }, }); @@ -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); + } +} diff --git a/app/src/interfaces/image/image.vue b/app/src/interfaces/image/image.vue index 5eeea27beb..8941cc1a58 100644 --- a/app/src/interfaces/image/image.vue +++ b/app/src/interfaces/image/image.vue @@ -18,8 +18,8 @@ - - + + @@ -31,12 +31,15 @@
{{ meta }}
- + @@ -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(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)?.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) { + if (!image.value) return; + + emit('input', { + id: image.value.id, + ...newEdits, + }); + } } }, }); diff --git a/app/src/views/private/components/drawer-item/drawer-item.vue b/app/src/views/private/components/drawer-item/drawer-item.vue index 56a5639073..77298938b0 100644 --- a/app/src/views/private/components/drawer-item/drawer-item.vue +++ b/app/src/views/private/components/drawer-item/drawer-item.vue @@ -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; From 9e2826057e95650d20161275c30dd7a1804df935 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 21 Jan 2021 14:38:37 -0500 Subject: [PATCH 4/7] Optimize search performance --- app/src/composables/use-items/use-items.ts | 22 +++++++++++++++++-- app/src/composables/use-preset/use-preset.ts | 2 -- .../components/search-input/search-input.vue | 3 +-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/composables/use-items/use-items.ts b/app/src/composables/use-items/use-items.ts index c121e8f532..c36549f8c7 100644 --- a/app/src/composables/use-items/use-items.ts +++ b/app/src/composables/use-items/use-items.ts @@ -5,7 +5,7 @@ import Vue from 'vue'; import { isEqual } from 'lodash'; import { Filter } from '@/types/'; import filtersToQuery from '@/utils/filters-to-query'; -import { orderBy } from 'lodash'; +import { orderBy, throttle } from 'lodash'; import moveInArray from '@/utils/move-in-array'; type Query = { @@ -91,7 +91,7 @@ export function useItems(collection: Ref, query: Query) { } }); - watch([filters, limit, searchQuery], async (after, before) => { + watch([filters, limit], async (after, before) => { if (!before || isEqual(after, before)) { return; } @@ -102,6 +102,24 @@ export function useItems(collection: Ref, query: Query) { } }); + watch( + searchQuery, + throttle( + async (after, before) => { + if (!before || isEqual(after, before)) { + return; + } + page.value = 1; + await Vue.nextTick(); + if (loading.value === false) { + getItems(); + } + }, + 500, + { trailing: true } + ) + ); + return { itemCount, totalCount, items, totalPages, loading, error, changeManualSort, getItems }; async function getItems() { diff --git a/app/src/composables/use-preset/use-preset.ts b/app/src/composables/use-preset/use-preset.ts index 743c01312f..ad11bbc6af 100644 --- a/app/src/composables/use-preset/use-preset.ts +++ b/app/src/composables/use-preset/use-preset.ts @@ -27,7 +27,6 @@ export function usePreset(collection: Ref, bookmark: Ref const savePreset = async (preset?: Partial) => { busy.value = true; const updatedValues = await presetsStore.savePreset(preset ? preset : localPreset.value); - initLocalPreset(); localPreset.value.id = updatedValues.id; busy.value = false; return updatedValues; @@ -35,7 +34,6 @@ export function usePreset(collection: Ref, bookmark: Ref const saveLocal = () => { presetsStore.saveLocal(localPreset.value); - initLocalPreset(); }; const clearLocalSave = async () => { diff --git a/app/src/views/private/components/search-input/search-input.vue b/app/src/views/private/components/search-input/search-input.vue index d8bbae2b6b..2a114e3387 100644 --- a/app/src/views/private/components/search-input/search-input.vue +++ b/app/src/views/private/components/search-input/search-input.vue @@ -14,7 +14,6 @@