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 @@