mirror of
https://github.com/directus/directus.git
synced 2026-01-29 22:57:55 -05:00
Add new export experience (#12201)
* Use script setup * Start on export dialog * Use new system field interface, replace limit with numeric input * Set placeholder * Add sort config * Use folder picker, correct layoutQuery use * Add local download button * Allow writing exports to file * Add notification after export * Fix sort config, use new export endpoint * Setup notification hints * Add information notice * Fix local limit, cancel button * Add (basic) docs for export functionality * Fix json export file format * Implement xml batch stitching * Resolve review points
This commit is contained in:
@@ -248,6 +248,7 @@
|
||||
:collection="collection"
|
||||
:filter="mergeFilters(filter, archiveFilter)"
|
||||
:search="search"
|
||||
:layout-query="layoutQuery"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div class="folder-picker-list-item">
|
||||
<v-list-item
|
||||
v-if="folder.children.length === 0"
|
||||
clickable
|
||||
:active="currentFolder === folder.id"
|
||||
:disabled="disabled"
|
||||
@click="clickHandler(folder.id)"
|
||||
>
|
||||
<v-list-item-icon><v-icon :name="currentFolder === folder.id ? 'folder_open' : 'folder'" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ folder.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group
|
||||
v-else
|
||||
clickable
|
||||
:active="currentFolder === folder.id"
|
||||
:disabled="disabled"
|
||||
@click="clickHandler(folder.id)"
|
||||
>
|
||||
<template #activator>
|
||||
<v-list-item-icon>
|
||||
<v-icon :name="currentFolder === folder.id ? 'folder_open' : 'folder'" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ folder.name }}</v-list-item-content>
|
||||
</template>
|
||||
<folder-picker-list-item
|
||||
v-for="childFolder in folder.children"
|
||||
:key="childFolder.id"
|
||||
:folder="childFolder"
|
||||
:current-folder="currentFolder"
|
||||
:click-handler="clickHandler"
|
||||
:disabled="disabledFolders.includes(childFolder.id)"
|
||||
:disabled-folders="disabledFolders"
|
||||
/>
|
||||
</v-list-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
type Folder = {
|
||||
id: string;
|
||||
name: string;
|
||||
children: Folder[];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FolderPickerListItem',
|
||||
props: {
|
||||
folder: {
|
||||
type: Object as PropType<Folder>,
|
||||
required: true,
|
||||
},
|
||||
currentFolder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
clickHandler: {
|
||||
type: Function,
|
||||
default: () => undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabledFolders: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<v-skeleton-loader v-if="loading" />
|
||||
<div v-else class="folder-picker">
|
||||
<v-list>
|
||||
<v-item-group v-model="openFolders" scope="folder-picker" multiple>
|
||||
<v-list-group
|
||||
disable-groupable-parent
|
||||
clickable
|
||||
:active="modelValue === null"
|
||||
scope="folder-picker"
|
||||
value="root"
|
||||
@click="$emit('update:modelValue', null)"
|
||||
>
|
||||
<template #activator>
|
||||
<v-list-item-icon>
|
||||
<v-icon name="folder_special" outline />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ t('file_library') }}</v-list-item-content>
|
||||
</template>
|
||||
|
||||
<folder-picker-list-item
|
||||
v-for="folder in tree"
|
||||
:key="folder.id"
|
||||
:folder="folder"
|
||||
:current-folder="modelValue"
|
||||
:click-handler="(id) => $emit('update:modelValue', id)"
|
||||
:disabled="disabledFolders.includes(folder.id)"
|
||||
:disabled-folders="disabledFolders"
|
||||
/>
|
||||
</v-list-group>
|
||||
</v-item-group>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, computed, PropType } from 'vue';
|
||||
import api from '@/api';
|
||||
import FolderPickerListItem from './folder-picker-list-item.vue';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
|
||||
type FolderRaw = {
|
||||
id: string;
|
||||
name: string;
|
||||
parent: null | string;
|
||||
};
|
||||
|
||||
type Folder = {
|
||||
id: string;
|
||||
name: string;
|
||||
children: Folder[];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { FolderPickerListItem },
|
||||
props: {
|
||||
disabledFolders: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
const folders = ref<FolderRaw[]>([]);
|
||||
const tree = computed<Folder[]>(() => {
|
||||
return folders.value
|
||||
.filter((folder) => folder.parent === null)
|
||||
.map((folder) => {
|
||||
return {
|
||||
...folder,
|
||||
children: getChildFolders(folder),
|
||||
};
|
||||
});
|
||||
|
||||
function getChildFolders(folder: FolderRaw): Folder[] {
|
||||
return folders.value
|
||||
.filter((childFolder) => {
|
||||
return childFolder.parent === folder.id;
|
||||
})
|
||||
.map((childFolder) => {
|
||||
return {
|
||||
...childFolder,
|
||||
children: getChildFolders(childFolder),
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const shouldBeOpen: string[] = [];
|
||||
const folder = folders.value.find((folder) => folder.id === props.modelValue);
|
||||
|
||||
if (folder && folder.parent) parseFolder(folder.parent);
|
||||
|
||||
const startOpenFolders = ['root'];
|
||||
|
||||
for (const folderID of shouldBeOpen) {
|
||||
if (startOpenFolders.includes(folderID) === false) {
|
||||
startOpenFolders.push(folderID);
|
||||
}
|
||||
}
|
||||
const selectedFolder = computed(() => {
|
||||
return folders.value.find((folder) => folder.id === props.modelValue) || {};
|
||||
});
|
||||
|
||||
const openFolders = ref(startOpenFolders);
|
||||
|
||||
fetchFolders();
|
||||
|
||||
return { t, loading, folders, tree, selectedFolder, openFolders };
|
||||
|
||||
async function fetchFolders() {
|
||||
if (folders.value.length > 0) return;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/folders`, {
|
||||
params: {
|
||||
limit: -1,
|
||||
sort: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
folders.value = response.data.data;
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFolder(id: string) {
|
||||
if (!folders.value) return;
|
||||
shouldBeOpen.push(id);
|
||||
|
||||
const folder = folders.value.find((folder) => folder.id === id);
|
||||
|
||||
if (folder && folder.parent) {
|
||||
parseFolder(folder.parent);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folder-picker {
|
||||
--v-list-item-background-color-hover: var(--background-normal-alt);
|
||||
--v-list-item-background-color-active: var(--background-normal-alt);
|
||||
|
||||
padding: 12px;
|
||||
background-color: var(--background-normal);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
@@ -118,7 +118,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, PropType, ref } from 'vue';
|
||||
import useFolders, { Folder } from '@/composables/use-folders';
|
||||
import api from '@/api';
|
||||
import FolderPicker from './folder-picker.vue';
|
||||
import FolderPicker from '@/views/private/components/folder-picker/folder-picker.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
<component :is="`layout-sidebar-${layout}`" v-bind="layoutState" />
|
||||
<export-sidebar-detail
|
||||
collection="directus_files"
|
||||
:layout-query="layoutQuery"
|
||||
:filter="mergeFilters(filter, folderTypeFilter)"
|
||||
:search="search"
|
||||
/>
|
||||
@@ -191,7 +192,7 @@ import usePreset from '@/composables/use-preset';
|
||||
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
|
||||
import AddFolder from '../components/add-folder.vue';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
import FolderPicker from '../components/folder-picker.vue';
|
||||
import FolderPicker from '@/views/private/components/folder-picker/folder-picker.vue';
|
||||
import emitter, { Events } from '@/events';
|
||||
import { useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
|
||||
import { useNotificationsStore, useUserStore, usePermissionsStore } from '@/stores';
|
||||
|
||||
@@ -183,7 +183,7 @@ 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 '../components/folder-picker.vue';
|
||||
import FolderPicker from '@/views/private/components/folder-picker/folder-picker.vue';
|
||||
import api, { addTokenToURL } from '@/api';
|
||||
import { getRootPath } from '@/utils/get-root-path';
|
||||
import FilesNotFound from './not-found.vue';
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
<component :is="`layout-sidebar-${layout}`" v-bind="layoutState" />
|
||||
<export-sidebar-detail
|
||||
collection="directus_users"
|
||||
:layout-query="layoutQuery"
|
||||
:filter="mergeFilters(filter, roleFilter)"
|
||||
:search="search"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user