From efe7b076a33b3df8eef3aef2b85af9834a201e25 Mon Sep 17 00:00:00 2001 From: Adrian Dimitrov Date: Thu, 22 Jul 2021 00:29:21 +0300 Subject: [PATCH] Add default-folder option (#3209) * Allow set folder for imported files * Allow passing folder in file/files component; Allow pick folder for file/files/image interfaces. * Added folder system component for picking folders; Move folder picker the field from data to interface (file, files, image). * Add custom folder interface; use props for interfaces file/files/image in upload component * Allow set folder for imported files * Allow passing folder in file/files component; Allow pick folder for file/files/image interfaces. * Added folder system component for picking folders; Move folder picker the field from data to interface (file, files, image). * Add custom folder interface; use props for interfaces file/files/image in upload component * Update after rebase * Add storage_default_folder setting, use folder when deploy file * Fix files options; Add default label option for folder interface. * Fix set folder for file * UX * Add migration for column, undo seed change * Update nomanclature * Make sure file library always submits folder, cleanup setting retrieval * Use indexName on down migrate * Fix import default folder, rename customPresets->folderPreset * Rename interface import * Use undefined as default folder * Remove deprecated lang file * Fix display of folder interface, treat null as value * Move shared composable * Remove unused ref Co-authored-by: rijkvanzanten --- .../20210721A-add-default-folder.ts | 22 +++ .../database/system-data/fields/settings.yaml | 5 + api/src/services/files.ts | 8 + .../v-form/form-field-interface.vue | 4 +- app/src/components/v-form/form-field.vue | 4 +- app/src/components/v-upload/v-upload.vue | 23 ++- .../files => }/composables/use-folders.ts | 0 .../system-folder/folder-list-item.vue | 70 +++++++++ .../_system/system-folder/folder.vue | 140 ++++++++++++++++++ .../interfaces/_system/system-folder/index.ts | 14 ++ app/src/interfaces/file-image/file-image.vue | 6 +- app/src/interfaces/file-image/index.ts | 16 +- app/src/interfaces/file/file.vue | 9 +- app/src/interfaces/file/index.ts | 16 +- app/src/lang/translations/en-US.yaml | 6 + .../modules/files/components/add-folder.vue | 2 +- .../files/components/navigation-folder.vue | 2 +- .../modules/files/components/navigation.vue | 2 +- app/src/modules/files/routes/collection.vue | 10 +- app/src/utils/upload-files/upload-files.ts | 1 + .../theme/global-components/CodeBlock.vue | 2 + .../theme/global-components/CodeGroup.vue | 4 + docs/guides/installation/docker.md | 1 - docs/reference/environment-variables.md | 28 ++-- packages/specs/src/components/setting.yaml | 4 + 25 files changed, 365 insertions(+), 34 deletions(-) create mode 100644 api/src/database/migrations/20210721A-add-default-folder.ts rename app/src/{modules/files => }/composables/use-folders.ts (100%) create mode 100644 app/src/interfaces/_system/system-folder/folder-list-item.vue create mode 100644 app/src/interfaces/_system/system-folder/folder.vue create mode 100644 app/src/interfaces/_system/system-folder/index.ts diff --git a/api/src/database/migrations/20210721A-add-default-folder.ts b/api/src/database/migrations/20210721A-add-default-folder.ts new file mode 100644 index 0000000000..5bf17ef17a --- /dev/null +++ b/api/src/database/migrations/20210721A-add-default-folder.ts @@ -0,0 +1,22 @@ +import { Knex } from 'knex'; +import { getDefaultIndexName } from '../../utils/get-default-index-name'; + +const indexName = getDefaultIndexName('foreign', 'directus_settings', 'storage_default_folder'); + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('directus_settings', (table) => { + table + .uuid('storage_default_folder') + .references('id') + .inTable('directus_folders') + .withKeyName(indexName) + .onDelete('SET NULL'); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('directus_files', (table) => { + table.dropForeign(['storage_default_folder'], indexName); + table.dropColumn('storage_default_folder'); + }); +} diff --git a/api/src/database/system-data/fields/settings.yaml b/api/src/database/system-data/fields/settings.yaml index 6a1965dc23..aba76bc0ae 100644 --- a/api/src/database/system-data/fields/settings.yaml +++ b/api/src/database/system-data/fields/settings.yaml @@ -243,6 +243,11 @@ fields: text: Presets Only width: half + - field: storage_default_folder + interface: system-folder + width: half + note: Default folder where new files are uploaded + - field: overrides_divider interface: presentation-divider options: diff --git a/api/src/services/files.ts b/api/src/services/files.ts index b1c7e89f67..a9a8d8bcf1 100644 --- a/api/src/services/files.ts +++ b/api/src/services/files.ts @@ -32,6 +32,14 @@ export class FilesService extends ItemsService { ): Promise { const payload = clone(data); + if ('folder' in payload === false) { + const settings = await this.knex.select('storage_default_folder').from('directus_settings').first(); + + if (settings?.storage_default_folder) { + payload.folder = settings.storage_default_folder; + } + } + if (primaryKey !== undefined) { await this.updateOne(primaryKey, payload, { emitEvents: false }); diff --git a/app/src/components/v-form/form-field-interface.vue b/app/src/components/v-form/form-field-interface.vue index f19180c562..49de3d32bc 100644 --- a/app/src/components/v-form/form-field-interface.vue +++ b/app/src/components/v-form/form-field-interface.vue @@ -18,7 +18,7 @@ :autofocus="disabled !== true && autofocus" :disabled="disabled" :loading="loading" - :value="modelValue === undefined ? field.schema.default_value : modelValue" + :value="modelValue === undefined ? field.schema?.default_value : modelValue" :width="(field.meta && field.meta.width) || 'full'" :type="field.type" :collection="field.collection" @@ -62,7 +62,7 @@ export default defineComponent({ }, modelValue: { type: [String, Number, Object, Array, Boolean], - default: null, + default: undefined, }, loading: { type: Boolean, diff --git a/app/src/components/v-form/form-field.vue b/app/src/components/v-form/form-field.vue index e028607e83..521a357c35 100644 --- a/app/src/components/v-form/form-field.vue +++ b/app/src/components/v-form/form-field.vue @@ -129,10 +129,10 @@ export default defineComponent({ }); const defaultValue = computed(() => { - const value = props.field.schema?.default_value; + const value = props.field?.schema?.default_value; if (value !== undefined) return value; - return null; + return undefined; }); const internalValue = computed(() => { diff --git a/app/src/components/v-upload/v-upload.vue b/app/src/components/v-upload/v-upload.vue index 3c47087775..29e9476146 100644 --- a/app/src/components/v-upload/v-upload.vue +++ b/app/src/components/v-upload/v-upload.vue @@ -117,6 +117,10 @@ export default defineComponent({ type: Boolean, default: false, }, + folder: { + type: String, + default: undefined, + }, }, emits: ['input'], setup(props, { emit }) { @@ -159,6 +163,12 @@ export default defineComponent({ uploading.value = true; progress.value = 0; + const folderPreset: { folder?: string } = {}; + + if (props.folder) { + folderPreset.folder = props.folder; + } + try { numberOfFiles.value = files.length; @@ -168,7 +178,10 @@ export default defineComponent({ progress.value = Math.round(percentage.reduce((acc, cur) => (acc += cur)) / files.length); done.value = percentage.filter((p) => p === 100).length; }, - preset: props.preset, + preset: { + ...props.preset, + ...folderPreset, + }, }); uploadedFiles && emit('input', uploadedFiles); @@ -179,7 +192,10 @@ export default defineComponent({ done.value = percentage === 100 ? 1 : 0; }, fileId: props.fileId, - preset: props.preset, + preset: { + ...props.preset, + ...folderPreset, + }, }); uploadedFile && emit('input', uploadedFile); @@ -272,6 +288,9 @@ export default defineComponent({ try { const response = await api.post(`/files/import`, { url: url.value, + data: { + folder: props.folder, + }, }); if (props.multiple) { diff --git a/app/src/modules/files/composables/use-folders.ts b/app/src/composables/use-folders.ts similarity index 100% rename from app/src/modules/files/composables/use-folders.ts rename to app/src/composables/use-folders.ts diff --git a/app/src/interfaces/_system/system-folder/folder-list-item.vue b/app/src/interfaces/_system/system-folder/folder-list-item.vue new file mode 100644 index 0000000000..7f77e93fc9 --- /dev/null +++ b/app/src/interfaces/_system/system-folder/folder-list-item.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/src/interfaces/_system/system-folder/folder.vue b/app/src/interfaces/_system/system-folder/folder.vue new file mode 100644 index 0000000000..59d9d16088 --- /dev/null +++ b/app/src/interfaces/_system/system-folder/folder.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/app/src/interfaces/_system/system-folder/index.ts b/app/src/interfaces/_system/system-folder/index.ts new file mode 100644 index 0000000000..7e8ccc4193 --- /dev/null +++ b/app/src/interfaces/_system/system-folder/index.ts @@ -0,0 +1,14 @@ +import { defineInterface } from '@/interfaces/define'; +import InterfaceSystemFolder from './folder.vue'; + +export default defineInterface({ + id: 'system-folder', + name: '$t:interfaces.system-folder.folder', + description: '$t:interfaces.system-folder.description', + icon: 'folder', + component: InterfaceSystemFolder, + types: ['uuid'], + options: [], + system: true, + recommendedDisplays: ['raw'], +}); diff --git a/app/src/interfaces/file-image/file-image.vue b/app/src/interfaces/file-image/file-image.vue index fa66862bd1..9d83455438 100644 --- a/app/src/interfaces/file-image/file-image.vue +++ b/app/src/interfaces/file-image/file-image.vue @@ -42,7 +42,7 @@ - + @@ -78,6 +78,10 @@ export default defineComponent({ type: Boolean, default: false, }, + folder: { + type: String, + default: undefined, + }, }, emits: ['input'], setup(props, { emit }) { diff --git a/app/src/interfaces/file-image/index.ts b/app/src/interfaces/file-image/index.ts index 68ddb1ea7e..944e2245e0 100644 --- a/app/src/interfaces/file-image/index.ts +++ b/app/src/interfaces/file-image/index.ts @@ -10,6 +10,20 @@ export default defineInterface({ types: ['uuid'], groups: ['file'], relational: true, - options: [], + options: [ + { + field: 'folder', + name: '$t:interfaces.system-folder.folder', + type: 'uuid', + meta: { + width: 'full', + interface: 'system-folder', + note: '$t:interfaces.system-folder.field_hint', + }, + schema: { + default_value: undefined, + }, + }, + ], recommendedDisplays: ['image'], }); diff --git a/app/src/interfaces/file/file.vue b/app/src/interfaces/file/file.vue index e4e4e4548a..e09e77819f 100644 --- a/app/src/interfaces/file/file.vue +++ b/app/src/interfaces/file/file.vue @@ -95,7 +95,7 @@ {{ t('upload_from_device') }} - + {{ t('cancel') }} @@ -163,6 +163,10 @@ export default defineComponent({ type: Boolean, default: false, }, + folder: { + type: String, + default: undefined, + }, }, emits: ['input'], setup(props, { emit }) { @@ -289,6 +293,9 @@ export default defineComponent({ try { const response = await api.post(`/files/import`, { url: url.value, + data: { + folder: props.folder, + }, }); file.value = response.data.data; diff --git a/app/src/interfaces/file/index.ts b/app/src/interfaces/file/index.ts index 6d25c0793f..b77a47114b 100644 --- a/app/src/interfaces/file/index.ts +++ b/app/src/interfaces/file/index.ts @@ -10,6 +10,20 @@ export default defineInterface({ types: ['uuid'], groups: ['file'], relational: true, - options: [], + options: [ + { + field: 'folder', + name: '$t:interfaces.system-folder.folder', + type: 'uuid', + meta: { + width: 'full', + interface: 'system-folder', + note: '$t:interfaces.system-folder.field_hint', + }, + schema: { + default_value: undefined, + }, + }, + ], recommendedDisplays: ['file'], }); diff --git a/app/src/lang/translations/en-US.yaml b/app/src/lang/translations/en-US.yaml index aa61494001..020eb9b7f9 100644 --- a/app/src/lang/translations/en-US.yaml +++ b/app/src/lang/translations/en-US.yaml @@ -1002,6 +1002,12 @@ interfaces: one-to-many: One to Many description: Select multiple related items no_collection: The collection could not be found + system-folder: + folder: Folder + description: Select a folder + field_hint: Puts newly uploaded files in selected folder. Does not affect existing files that are selected. + root_name: File Library Root + system_default: System Defaults select-radio: radio-buttons: Radio Buttons description: Select one of multiple choices diff --git a/app/src/modules/files/components/add-folder.vue b/app/src/modules/files/components/add-folder.vue index 64f7230dbe..1bef74a02a 100644 --- a/app/src/modules/files/components/add-folder.vue +++ b/app/src/modules/files/components/add-folder.vue @@ -31,7 +31,7 @@