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:
Rijk van Zanten
2022-03-17 15:43:45 -04:00
committed by GitHub
parent aca9ff9709
commit 1c3e94d830
25 changed files with 1095 additions and 416 deletions

View File

@@ -248,6 +248,7 @@
:collection="collection"
:filter="mergeFilters(filter, archiveFilter)"
:search="search"
:layout-query="layoutQuery"
@refresh="refresh"
/>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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