mirror of
https://github.com/directus/directus.git
synced 2026-01-28 17:28:06 -05:00
Files nav (#441)
* Add support for item-group top level links * Add active prop support to v-list-group * Add all-files string * Add files nav * Fix nav order, render divider only when items exist * Add add-new-folder flow * Add add-folder button to header * Dont unmount nav on refresh of folders * Fix codesmell
This commit is contained in:
@@ -139,8 +139,9 @@ export default defineComponent({
|
||||
.v-button {
|
||||
--v-button-width: auto;
|
||||
--v-button-height: 44px;
|
||||
--v-button-color: var(--white);
|
||||
--v-button-color-activated: var(--white);
|
||||
--v-button-color: var(--foreground-inverted);
|
||||
--v-button-color-hover: var(--foreground-inverted);
|
||||
--v-button-color-activated: var(--foreground-inverted);
|
||||
--v-button-color-disabled: var(--foreground-subdued);
|
||||
--v-button-background-color: var(--primary);
|
||||
--v-button-background-color-hover: var(--primary-125);
|
||||
@@ -189,6 +190,7 @@ export default defineComponent({
|
||||
transition-property: background-color border;
|
||||
|
||||
&:hover {
|
||||
color: var(--v-button-color-hover);
|
||||
background-color: var(--v-button-background-color-hover);
|
||||
border-color: var(--v-button-background-color-hover);
|
||||
}
|
||||
|
||||
@@ -271,9 +271,11 @@ Provides the ability to make a collapsable (sub)group of list items, within a li
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Description | Default |
|
||||
| ---------- | ---------------------------------------------------- | ------- |
|
||||
| `multiple` | Allow multiple subgroups to be open at the same time | `true` |
|
||||
| Prop | Description | Default |
|
||||
|------------|---------------------------------------------------------------------------------|---------|
|
||||
| `multiple` | Allow multiple subgroups to be open at the same time | `true` |
|
||||
| `to` | Where to link to. This will only make the chevron toggle the group active state | |
|
||||
| `active` | Render the activitor item in the active state | `false` |
|
||||
|
||||
## Events
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="v-list-group">
|
||||
<v-list-item class="activator" @click="toggle">
|
||||
<v-list-item :active="active" class="activator" :to="to" @click="onClick">
|
||||
<slot name="activator" />
|
||||
|
||||
<v-list-item-icon class="activator-icon" :class="{ active }">
|
||||
<v-icon name="chevron_left" />
|
||||
<v-list-item-icon class="activator-icon" :class="{ active: groupActive }">
|
||||
<v-icon name="chevron_left" @click.stop.prevent="toggle" />
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
|
||||
<transition-expand>
|
||||
<div class="items" v-show="active">
|
||||
<div class="items" v-show="groupActive">
|
||||
<slot />
|
||||
</div>
|
||||
</transition-expand>
|
||||
@@ -26,16 +26,33 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { active, toggle } = useGroupable();
|
||||
setup(props, { listeners, emit }) {
|
||||
const { active: groupActive, toggle } = useGroupable();
|
||||
|
||||
useGroupableParent(
|
||||
{},
|
||||
{
|
||||
multiple: toRefs(props).multiple,
|
||||
}
|
||||
);
|
||||
return { active, toggle };
|
||||
|
||||
return { groupActive, toggle, onClick };
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
if (props.to) return null;
|
||||
if (listeners.click) return emit('click', event);
|
||||
|
||||
toggle();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
'two-line': lines === 2,
|
||||
'one-line': lines === 1,
|
||||
disabled,
|
||||
dashed,
|
||||
}"
|
||||
v-on="disabled === false && $listeners"
|
||||
>
|
||||
@@ -45,6 +46,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dashed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { listeners }) {
|
||||
const component = computed<string>(() => (props.to ? 'router-link' : 'li'));
|
||||
@@ -102,6 +107,20 @@ export default defineComponent({
|
||||
text-decoration: none;
|
||||
border-radius: var(--v-list-item-border-radius);
|
||||
|
||||
&.dashed {
|
||||
&::after {
|
||||
// Borders normally render outside the element, this is a way of showing it as inner
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
border: 2px dashed var(--border-normal);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.link {
|
||||
cursor: pointer;
|
||||
transition: var(--fast) var(--transition);
|
||||
|
||||
@@ -247,6 +247,9 @@
|
||||
|
||||
"n_items_selected": "No Items Selected | 1 Item Selected | {n} Items Selected",
|
||||
"per_page": "Per Page",
|
||||
"all_files": "All Files",
|
||||
"add_new_folder": "Add New Folder",
|
||||
"folder_name": "Folder Name...",
|
||||
|
||||
"about_directus": "About Directus",
|
||||
"activity_log": "Activity Log",
|
||||
|
||||
84
src/modules/files/components/add-folder/add-folder.vue
Normal file
84
src/modules/files/components/add-folder/add-folder.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialogActive">
|
||||
<template #activator="{ on }">
|
||||
<v-button rounded icon class="add-new" @click="on">
|
||||
<v-icon name="create_new_folder" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('add_new_folder') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-input :placeholder="$t('folder_name')" v-model="newFolderName" full-width />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="dialogActive = false">{{ $t('cancel') }}</v-button>
|
||||
<v-button
|
||||
:disabled="!newFolderName || newFolderName.length === 0"
|
||||
@click="addFolder"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import useFolders from '../../compositions/use-folders';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
parent: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const projectsStore = useProjectsStore();
|
||||
const dialogActive = ref(false);
|
||||
const saving = ref(false);
|
||||
const newFolderName = ref(null);
|
||||
const savingError = ref(null);
|
||||
|
||||
const { fetchFolders } = useFolders();
|
||||
|
||||
return { addFolder, dialogActive, newFolderName, saving, savingError };
|
||||
|
||||
async function addFolder() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await api.post(`/${currentProjectKey}/folders`, {
|
||||
name: newFolderName.value,
|
||||
parent_folder: props.parent,
|
||||
});
|
||||
|
||||
await fetchFolders();
|
||||
|
||||
dialogActive.value = false;
|
||||
newFolderName.value = null;
|
||||
} catch (err) {
|
||||
savingError.value = err;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-new {
|
||||
--v-button-background-color: var(--primary-alt);
|
||||
--v-button-color: var(--primary);
|
||||
--v-button-background-color-hover: var(--primary);
|
||||
--v-button-color-hover: var(--foreground-inverted);
|
||||
}
|
||||
</style>
|
||||
4
src/modules/files/components/add-folder/index.ts
Normal file
4
src/modules/files/components/add-folder/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import AddFolder from './add-folder.vue';
|
||||
|
||||
export { AddFolder };
|
||||
export default AddFolder;
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="folder.children === undefined"
|
||||
@click="$emit('click', folder.id)"
|
||||
:active="currentFolder === folder.id"
|
||||
>
|
||||
<v-list-item-icon><v-icon name="folder" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ folder.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group v-else @click="$emit('click', folder.id)" :active="currentFolder === folder.id">
|
||||
<template #activator="{ active }">
|
||||
<v-list-item-icon>
|
||||
<v-icon :name="active ? 'folder_open' : 'folder'" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ folder.name }}</v-list-item-content>
|
||||
</template>
|
||||
<navigation-folder
|
||||
v-for="childFolder in folder.children"
|
||||
:key="childFolder.id"
|
||||
:folder="childFolder"
|
||||
:current-folder="currentFolder"
|
||||
@click="$emit('click', childFolder.id)"
|
||||
/>
|
||||
</v-list-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { Folder } from '../../compositions/use-folders';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'navigation-folder',
|
||||
props: {
|
||||
folder: {
|
||||
type: Object as PropType<Folder>,
|
||||
required: true,
|
||||
},
|
||||
currentFolder: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,3 +1,55 @@
|
||||
<template>
|
||||
<div>files nav</div>
|
||||
<v-list nav>
|
||||
<v-list-item @click="$emit('filter', null)" :active="currentFolder === null">
|
||||
<v-list-item-icon><v-icon name="folder_special" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('all_files') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="loading || folders.length > 0" />
|
||||
|
||||
<template v-if="loading && (folders === null || folders.length === 0)">
|
||||
<v-list-item v-for="n in 4" :key="n">
|
||||
<v-skeleton-loader type="list-item-icon" />
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<navigation-folder
|
||||
@click="$emit('filter', $event)"
|
||||
v-for="folder in folders"
|
||||
:key="folder.id"
|
||||
:folder="folder"
|
||||
:current-folder="currentFolder"
|
||||
/>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import useFolders from '../../compositions/use-folders';
|
||||
import NavigationFolder from './navigation-folder.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { NavigationFolder },
|
||||
model: {
|
||||
prop: 'currentFolder',
|
||||
event: 'filter',
|
||||
},
|
||||
props: {
|
||||
currentFolder: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { folders, error, loading } = useFolders();
|
||||
|
||||
return { folders, error, loading };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .v-skeleton-loader {
|
||||
--v-skeleton-loader-background-color: var(--background-normal-alt);
|
||||
}
|
||||
</style>
|
||||
|
||||
75
src/modules/files/compositions/use-folders.ts
Normal file
75
src/modules/files/compositions/use-folders.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import api from '@/api';
|
||||
import { ref, Ref } from '@vue/composition-api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
|
||||
type FolderRaw = {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_folder: number;
|
||||
};
|
||||
|
||||
export type Folder = {
|
||||
id: number;
|
||||
name: string;
|
||||
children?: Folder[];
|
||||
};
|
||||
|
||||
let loading: Ref<boolean> | null = null;
|
||||
let folders: Ref<Folder[] | null> | null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let error: Ref<any> | null = null;
|
||||
|
||||
export default function useFolders() {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
if (loading === null) loading = ref(false);
|
||||
if (folders === null) folders = ref<Folder[]>(null);
|
||||
if (error === null) error = ref(null);
|
||||
|
||||
if (folders.value === null && loading.value === false) {
|
||||
fetchFolders();
|
||||
}
|
||||
|
||||
return { loading, folders, error, fetchFolders };
|
||||
|
||||
async function fetchFolders() {
|
||||
if (loading === null) return;
|
||||
if (folders === null) return;
|
||||
if (error === null) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/${projectsStore.state.currentProjectKey}/folders`, {
|
||||
params: {
|
||||
limit: -1,
|
||||
sort: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
folders.value = nestFolders(response.data.data);
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function nestFolders(rawFolders: FolderRaw[]) {
|
||||
return rawFolders
|
||||
.map((folderRaw) => {
|
||||
const folder: FolderRaw & Folder = { ...folderRaw };
|
||||
|
||||
const children = rawFolders.filter(
|
||||
(childFolder) => childFolder.parent_folder === folderRaw.id
|
||||
);
|
||||
|
||||
if (children.length > 0) {
|
||||
folder.children = children;
|
||||
}
|
||||
|
||||
return folder;
|
||||
})
|
||||
.filter((folder) => folder.parent_folder === null);
|
||||
}
|
||||
@@ -44,13 +44,15 @@
|
||||
<v-icon name="edit" />
|
||||
</v-button>
|
||||
|
||||
<add-folder :parent="currentFolder" />
|
||||
|
||||
<v-button rounded icon :to="addNewLink">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<template #navigation>
|
||||
<files-navigation />
|
||||
<files-navigation v-model="currentFolder" />
|
||||
</template>
|
||||
|
||||
<component
|
||||
@@ -61,7 +63,8 @@
|
||||
:selection.sync="selection"
|
||||
:view-options.sync="viewOptions"
|
||||
:view-query.sync="viewQuery"
|
||||
:filters.sync="filters"
|
||||
:filters="filtersWithFolder"
|
||||
@update:filters="filters = $event"
|
||||
:detail-route="'/{{project}}/files/{{primaryKey}}'"
|
||||
/>
|
||||
</private-view>
|
||||
@@ -77,6 +80,7 @@ import { LayoutComponent } from '@/layouts/types';
|
||||
import useCollectionPreset from '@/compositions/use-collection-preset';
|
||||
import FilterDrawerDetail from '@/views/private/components/filter-drawer-detail';
|
||||
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
|
||||
import AddFolder from '../../components/add-folder';
|
||||
|
||||
type Item = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -85,7 +89,7 @@ type Item = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'files-browse',
|
||||
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail },
|
||||
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail, AddFolder },
|
||||
props: {},
|
||||
setup() {
|
||||
const layout = ref<LayoutComponent>(null);
|
||||
@@ -100,6 +104,21 @@ export default defineComponent({
|
||||
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
const currentFolder = ref(null);
|
||||
|
||||
const filtersWithFolder = computed(() => {
|
||||
if (currentFolder.value !== null) {
|
||||
return [
|
||||
...filters.value,
|
||||
{
|
||||
field: 'folder',
|
||||
operator: 'eq',
|
||||
value: currentFolder.value,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
addNewLink,
|
||||
batchDelete,
|
||||
@@ -113,6 +132,8 @@ export default defineComponent({
|
||||
viewOptions,
|
||||
viewQuery,
|
||||
viewType,
|
||||
currentFolder,
|
||||
filtersWithFolder,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
|
||||
@@ -96,7 +96,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
@include breakpoint(medium) {
|
||||
.action-buttons {
|
||||
.action-buttons ::v-deep {
|
||||
> * {
|
||||
display: inherit !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user