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:
Rijk van Zanten
2020-04-20 18:15:55 -04:00
committed by GitHub
parent 008ff4bc76
commit 8bf25d3e3c
12 changed files with 340 additions and 17 deletions

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,4 @@
import AddFolder from './add-folder.vue';
export { AddFolder };
export default AddFolder;

View File

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

View File

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

View 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);
}

View File

@@ -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() {

View File

@@ -96,7 +96,7 @@ export default defineComponent({
}
@include breakpoint(medium) {
.action-buttons {
.action-buttons ::v-deep {
> * {
display: inherit !important;
}