Merge pull request #106 from directus/folders-ctx

Folders ctx
This commit is contained in:
Rijk van Zanten
2020-08-11 14:27:42 -04:00
committed by GitHub
27 changed files with 343 additions and 83 deletions

View File

@@ -1,9 +1,9 @@
<template>
<div class="v-dialog">
<slot name="activator" v-bind="{ on: () => $emit('toggle', true) }" />
<slot name="activator" v-bind="{ on: () => (_active = true) }" />
<portal to="dialog-outlet">
<div v-if="active" class="container" :class="[className]" :key="id">
<div v-if="_active" class="container" :class="[className]" :key="id">
<v-overlay active absolute @click="emitToggle" />
<slot />
</div>
@@ -23,7 +23,7 @@ export default defineComponent({
props: {
active: {
type: Boolean,
default: false,
default: undefined,
},
persistent: {
type: Boolean,
@@ -31,10 +31,22 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const localActive = ref(false);
const className = ref<string | null>(null);
const id = computed(() => nanoid());
return { emitToggle, className, nudge, id };
const _active = computed({
get() {
return props.active !== undefined ? props.active : localActive.value;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('toggle', newActive);
},
});
return { emitToggle, className, nudge, id, _active };
function emitToggle() {
if (props.persistent === false) {

View File

@@ -1,5 +1,5 @@
<template>
<v-menu attached close-on-content-click v-model="menuActive">
<v-menu attached v-model="menuActive">
<template #activator="{ toggle }">
<v-input>
<template #input>

View File

@@ -1,12 +1,6 @@
<template>
<div class="field" :key="field.field" :class="field.meta.width">
<v-menu
v-if="field.hideLabel !== true"
placement="bottom-start"
show-arrow
close-on-content-click
:disabled="isDisabled"
>
<v-menu v-if="field.hideLabel !== true" placement="bottom-start" show-arrow :disabled="isDisabled">
<template #activator="{ toggle, active }">
<form-field-label
:field="field"

View File

@@ -123,7 +123,7 @@ export const withMenu = () =>
components: { VMenu },
template: `
<div>
<v-menu placement="bottom-start" close-on-content-click attached>
<v-menu placement="bottom-start" attached>
<template #activator="{ toggle, active }">
<v-input placeholder="Enter value...">
<template #append><v-icon @click="toggle" name="public" :style="{

View File

@@ -1,10 +1,10 @@
<template>
<div class="v-list-group">
<v-list-item :active="active" class="activator" :to="to" @click="onClick">
<v-list-item :active="active" class="activator" :to="to" :exact="exact" @click="onClick" :disabled="disabled">
<slot name="activator" :active="groupActive" />
<v-list-item-icon class="activator-icon" :class="{ active: groupActive }">
<v-icon name="chevron_right" @click.stop.prevent="toggle" />
<v-icon name="chevron_right" @click.stop.prevent="toggle" :disabled="disabled" />
</v-list-item-icon>
</v-list-item>
@@ -34,6 +34,14 @@ export default defineComponent({
type: Boolean,
default: false,
},
exact: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props, { listeners, emit }) {
const { active: groupActive, toggle } = useGroupable();

View File

@@ -55,7 +55,7 @@ body {
--v-list-padding: 4px 0;
--v-list-max-height: none;
--v-list-max-width: none;
--v-list-min-width: none;
--v-list-min-width: 220px;
--v-list-min-height: none;
--v-list-color: var(--foreground-normal);
--v-list-color-hover: var(--foreground-normal);

View File

@@ -14,7 +14,7 @@ within a menu. If you ever find yourself doing this:
| `placement` | Where to position the popper. | `bottom` |
| `value` | Value to control menu active state | `undefined` |
| `close-on-click` | Close the menu when clicking outside of the menu | `true` |
| `close-on-content-click` | Close the menu when clicking the content of the menu | `false` |
| `close-on-content-click` | Close the menu when clicking the content of the menu | `true` |
| `attached` | Attach the menu to an input | `false` |
| `show-arrow` | Show an arrow pointer | `false` |
| `disabled` | Menu does not appear | `false` |

View File

@@ -82,6 +82,12 @@ export function usePopper(
padding: 8,
},
},
{
name: 'arrow',
options: {
padding: 6,
},
},
computeStyles,
flip,
eventListeners,

View File

@@ -34,12 +34,7 @@
events: ['click'],
}"
>
<div
class="arrow"
:class="{ active: showArrow && isActive }"
:style="arrowStyles"
data-popper-arrow
/>
<div class="arrow" :class="{ active: showArrow && isActive }" :style="arrowStyles" data-popper-arrow />
<div class="v-menu-content" @click.stop="onContentClick">
<slot :active="isActive" />
</div>
@@ -55,18 +50,6 @@ import { Placement } from '@popperjs/core';
import { nanoid } from 'nanoid';
import Vue from 'vue';
/**
* @NOTE
*
* The framerate takes a little hit when opening menus, as we're rendering the content of it while
* we're opening it. We could potentially optimize this by rendering the menu content ahead of time,
* so all the processing power is freed up for the popper calculations and transition logic.
*
* However, we _can not_ render all menu content at all times, as that greatly decreases perf in the
* app. I'm thinking we might be able to pre-render the menu content on hover of the activator, so
* the actual act of opening the menu is quicker.
*/
export default defineComponent({
props: {
placement: {
@@ -83,7 +66,7 @@ export default defineComponent({
},
closeOnContentClick: {
type: Boolean,
default: false,
default: true,
},
attached: {
type: Boolean,
@@ -109,9 +92,19 @@ export default defineComponent({
},
setup(props, { emit }) {
const activator = ref<HTMLElement | null>(null);
const reference = ref<HTMLElement | null>(null);
const reference = computed<HTMLElement | null>(() => {
return (activator.value as HTMLElement)?.childNodes[0] as HTMLElement;
const virtualReference = ref({
getBoundingClientRect() {
return {
top: 0,
left: 0,
bottom: 0,
right: 0,
width: 0,
height: 0,
};
},
});
const id = computed(() => nanoid());
@@ -175,6 +168,8 @@ export default defineComponent({
return localIsActive.value;
},
async set(newActive) {
reference.value =
((activator.value as HTMLElement)?.childNodes[0] as HTMLElement) || virtualReference.value;
localIsActive.value = newActive;
emit('input', newActive);
},
@@ -194,7 +189,21 @@ export default defineComponent({
return { isActive, activate, deactivate, toggle };
function activate() {
function activate(event?: MouseEvent) {
if (event) {
virtualReference.value = {
getBoundingClientRect() {
return {
top: event.clientY,
left: event.clientX,
bottom: 0,
right: 0,
width: 0,
height: 0,
};
},
};
}
isActive.value = true;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="file">
<v-menu attached close-on-content-click :disabled="disabled || loading">
<v-menu attached :disabled="disabled || loading">
<template #activator="{ toggle }">
<div>
<v-skeleton-loader type="input" v-if="loading" />

View File

@@ -1,6 +1,6 @@
<template>
<v-menu attached :disabled="disabled" close-on-content-click>
<template #activator="{ toggle, active, activate }">
<v-menu attached :disabled="disabled">
<template #activator="{ active, activate }">
<v-input
:disabled="disabled"
:placeholder="value ? formatTitle(value) : $t('search_for_icon')"

View File

@@ -6,7 +6,7 @@
{{ $t('display_template_not_setup') }}
</v-notice>
<div class="many-to-one" v-else>
<v-menu v-model="menuActive" attached close-on-content-click :disabled="disabled">
<v-menu v-model="menuActive" attached :disabled="disabled">
<template #activator="{ active }">
<v-skeleton-loader type="input" v-if="loadingCurrent" />
<v-input

View File

@@ -2,7 +2,7 @@
<v-notice v-if="!statuses">
{{ $t('statuses_not_configured') }}
</v-notice>
<v-menu v-else attached :disabled="disabled" close-on-content-click>
<v-menu v-else attached :disabled="disabled">
<template #activator="{ toggle, active }">
<v-input
readonly

View File

@@ -1,6 +1,6 @@
<template>
<div class="user">
<v-menu v-model="menuActive" attached close-on-content-click :disabled="disabled">
<v-menu v-model="menuActive" attached :disabled="disabled">
<template #activator="{ active }">
<v-skeleton-loader type="input" v-if="loadingCurrent" />
<v-input

View File

@@ -14,6 +14,10 @@
"add_role": "Add Role",
"add_user": "Add User",
"rename_folder": "Rename Folder",
"nested_files_folders_will_be_moved": "Nested files and folders will be moved one level up.",
"uploaded_by": "Uploaded By",
"hide_field_on_detail": "Hide Field on Detail",
"show_field_on_detail": "Show Field on Detail",
@@ -206,6 +210,7 @@
"submit": "Submit",
"move_to_folder": "Move to Folder",
"delete_folder": "Delete Folder",
"select_folder": "Select Folder",
"move": "Move",

View File

@@ -3,11 +3,12 @@
v-if="folder.children.length === 0"
@click="clickHandler(folder.id)"
:active="currentFolder === folder.id"
:disabled="disabled"
>
<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 @click="clickHandler(folder.id)" :active="currentFolder === folder.id">
<v-list-group v-else @click="clickHandler(folder.id)" :active="currentFolder === folder.id" :disabled="disabled">
<template #activator>
<v-list-item-icon>
<v-icon :name="currentFolder === folder.id ? 'folder_open' : 'folder'" />
@@ -20,6 +21,8 @@
:folder="childFolder"
:current-folder="currentFolder"
:click-handler="clickHandler"
:disabled="disabledFolders.includes(childFolder.id)"
:disabled-folders="disabledFolders"
/>
</v-list-group>
</template>
@@ -48,6 +51,14 @@ export default defineComponent({
type: Function,
default: () => undefined,
},
disabled: {
type: Boolean,
default: false,
},
disabledFolders: {
type: Array as PropType<string[]>,
default: () => [],
},
},
});
</script>

View File

@@ -16,6 +16,8 @@
:folder="folder"
:current-folder="value"
:click-handler="(id) => $emit('input', id)"
:disabled="disabledFolders.includes(folder.id)"
:disabled-folders="disabledFolders"
/>
</v-list-group>
</v-list>
@@ -23,7 +25,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { defineComponent, ref, computed, PropType } from '@vue/composition-api';
import api from '@/api';
import FolderPickerListItem from './folder-picker-list-item.vue';
@@ -42,6 +44,10 @@ type Folder = {
export default defineComponent({
components: { FolderPickerListItem },
props: {
disabledFolders: {
type: Array as PropType<string[]>,
default: () => [],
},
value: {
type: String,
default: null,
@@ -91,6 +97,7 @@ export default defineComponent({
const response = await api.get(`/folders`, {
params: {
limit: -1,
sort: 'name',
},
});

View File

@@ -1,35 +1,118 @@
<template>
<v-list-item
v-if="folder.children === undefined"
:to="`/files?folder=${folder.id}`"
exact
>
<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="clickHandler(folder.id)" :active="currentFolder === folder.id">
<template #activator="{ active }">
<v-list-item-icon>
<v-icon :name="active ? 'folder_open' : 'folder'" />
</v-list-item-icon>
<div>
<v-list-item
v-if="folder.children === undefined"
:to="`/files?folder=${folder.id}`"
exact
@contextmenu.native.prevent.stop="$refs.contextMenu.activate"
>
<v-list-item-icon><v-icon name="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-handler="clickHandler"
/>
</v-list-group>
</v-list-item>
<v-list-group
v-else
:to="`/files?folder=${folder.id}`"
exact
@contextmenu.native.prevent="$refs.contextMenu.activate"
>
<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-handler="clickHandler"
/>
</v-list-group>
<v-menu ref="contextMenu" show-arrow placement="bottom-start">
<v-list dense>
<v-list-item @click="renameActive = true">
<v-list-item-icon>
<v-icon name="edit" />
</v-list-item-icon>
<v-list-item-content>
{{ $t('rename_folder') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="moveActive = true">
<v-list-item-icon>
<v-icon name="folder_move" />
</v-list-item-icon>
<v-list-item-content>
{{ $t('move_to_folder') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="deleteActive = true">
<v-list-item-icon>
<v-icon name="delete" />
</v-list-item-icon>
<v-list-item-content>
{{ $t('delete_folder') }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="renameActive" persistent>
<v-card>
<v-card-title>{{ $t('rename_folder') }}</v-card-title>
<v-card-text>
<v-input v-model="renameValue" />
</v-card-text>
<v-card-actions>
<v-button secondary @click="renameActive = false">{{ $t('cancel') }}</v-button>
<v-button @click="renameSave" :loading="renameSaving">{{ $t('save') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="moveActive" persistent>
<v-card>
<v-card-title>{{ $t('move_to_folder') }}</v-card-title>
<v-card-text>
<folder-picker v-model="moveValue" :disabled-folders="[folder.id]" />
</v-card-text>
<v-card-actions>
<v-button secondary @click="moveActive = false">{{ $t('cancel') }}</v-button>
<v-button @click="moveSave" :loading="moveSaving">{{ $t('save') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="deleteActive" persistent>
<v-card>
<v-card-title>{{ $t('delete_folder') }}</v-card-title>
<v-card-text>
<v-notice>
{{ $t('nested_files_folders_will_be_moved') }}
</v-notice>
</v-card-text>
<v-card-actions>
<v-button secondary @click="deleteActive = false">{{ $t('cancel') }}</v-button>
<v-button @click="deleteSave" :loading="deleteSaving">{{ $t('delete') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { Folder } from '../../composables/use-folders';
import { defineComponent, PropType, ref } from '@vue/composition-api';
import useFolders, { Folder } from '../../composables/use-folders';
import notify from '@/utils/notify';
import api from '@/api';
import FolderPicker from '../folder-picker';
export default defineComponent({
name: 'navigation-folder',
components: { FolderPicker },
props: {
folder: {
type: Object as PropType<Folder>,
@@ -44,5 +127,129 @@ export default defineComponent({
default: () => undefined,
},
},
setup(props) {
const { renameActive, renameValue, renameSave, renameSaving } = useRenameFolder();
const { moveActive, moveValue, moveSave, moveSaving } = useMoveFolder();
const { deleteActive, deleteSave, deleteSaving } = useDeleteFolder();
const { fetchFolders } = useFolders();
return {
renameActive,
renameValue,
renameSave,
renameSaving,
moveActive,
moveValue,
moveSave,
moveSaving,
deleteActive,
deleteSave,
deleteSaving,
};
function useRenameFolder() {
const renameActive = ref(false);
const renameValue = ref(props.folder.name);
const renameSaving = ref(false);
return { renameActive, renameValue, renameSave, renameSaving };
async function renameSave() {
renameSaving.value = true;
try {
await api.patch(`/folders/${props.folder.id}`, {
name: renameValue.value,
});
} catch (error) {
console.error(error);
} finally {
renameSaving.value = false;
await fetchFolders();
renameActive.value = false;
}
}
}
function useMoveFolder() {
const moveActive = ref(false);
const moveValue = ref(props.folder.parent_folder);
const moveSaving = ref(false);
return { moveActive, moveValue, moveSave, moveSaving };
async function moveSave() {
moveSaving.value = true;
try {
await api.patch(`/folders/${props.folder.id}`, {
parent_folder: moveValue.value,
});
} catch (error) {
console.error(error);
} finally {
moveSaving.value = false;
await fetchFolders();
moveActive.value = false;
}
}
}
function useDeleteFolder() {
const deleteActive = ref(false);
const deleteSaving = ref(false);
return { deleteActive, deleteSave, deleteSaving };
async function deleteSave() {
deleteSaving.value = true;
try {
const foldersToUpdate = await api.get('/folders', {
params: {
filter: {
parent_folder: {
_eq: props.folder.id,
},
},
},
});
const filesToUpdate = await api.get('/files', {
params: {
filter: {
folder: {
_eq: props.folder.id,
},
},
},
});
const newParent = props.folder.parent_folder || null;
const folderKeys = foldersToUpdate.data.data.map((folder: { id: string }) => folder.id);
const fileKeys = filesToUpdate.data.data.map((file: { id: string }) => file.id);
if (folderKeys.length > 0) {
await api.patch(`/folders/${folderKeys.join(',')}`, { parent_folder: newParent });
}
if (fileKeys.length > 0) {
await api.patch(`/files/${fileKeys.join(',')}`, { folder: newParent });
}
await api.delete(`/folders/${props.folder.id}`);
deleteActive.value = false;
} catch (error) {
console.error(error);
} finally {
await fetchFolders();
deleteSaving.value = false;
}
}
}
},
});
</script>

View File

@@ -2,14 +2,15 @@ import api from '@/api';
import { ref, Ref } from '@vue/composition-api';
type FolderRaw = {
id: number;
id: string;
name: string;
parent_folder: number;
parent_folder: string;
};
export type Folder = {
id: number;
id: string;
name: string;
parent_folder: string;
children?: Folder[];
};
@@ -63,7 +64,7 @@ export function nestChildren(rawFolder: FolderRaw, rawFolders: FolderRaw[]) {
const folder: FolderRaw & Folder = { ...rawFolder };
const children = rawFolders
.filter((childFolder) => childFolder.parent_folder === rawFolder.id)
.filter((childFolder) => childFolder.parent_folder === rawFolder.id && childFolder.id !== rawFolder.id)
.map((childRawFolder) => nestChildren(childRawFolder, rawFolders));
if (children.length > 0) {

View File

@@ -11,7 +11,7 @@
</v-button>
<div v-else-if="collection.collection.startsWith('directus_') === false">
<v-menu placement="left-start" show-arrow close-on-content-click :disabled="savingManaged">
<v-menu placement="left-start" show-arrow :disabled="savingManaged">
<template #activator="{ toggle }">
<v-progress-circular small v-if="savingManaged" indeterminate />
<v-icon v-else name="more_vert" @click="toggle" class="ctx-toggle" />

View File

@@ -1,6 +1,6 @@
<template>
<div :class="field.meta.width || 'full'">
<v-menu attached close-on-content-click>
<v-menu attached>
<template #activator="{ toggle, active }">
<v-input class="field" :class="{ hidden, active }" readonly @click="toggle">
<template #prepend>

View File

@@ -42,7 +42,7 @@
/>
</draggable>
<v-menu attached close-on-content-click>
<v-menu attached>
<template #activator="{ toggle, active }">
<v-button
@click="toggle"

View File

@@ -24,7 +24,7 @@
</div>
<div class="header-right">
<v-menu show-arrow placement="bottom-end" close-on-content-click>
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle, active }">
<v-icon class="more" :class="{ active }" name="more_horiz" @click="toggle" />
<div class="time">

View File

@@ -5,7 +5,7 @@
<span v-if="filter.field.includes('.')" class="relational-indicator"></span>
{{ name }}
</div>
<v-menu show-arrow :disabled="disabled" close-on-content-click>
<v-menu show-arrow :disabled="disabled">
<template #activator="{ toggle }">
<div class="operator" @click="toggle">
<span>{{ $t(`operators.${activeOperator}`) }}</span>

View File

@@ -16,7 +16,7 @@
<v-divider v-if="filters.length" />
<v-menu attached close-on-content-click :disabled="loading">
<v-menu attached :disabled="loading">
<template #activator="{ toggle, active }">
<v-input @click="toggle" :class="{ active }" readonly :value="$t('add_filter')" :disabled="loading">
<template #prepend><v-icon name="add" /></template>

View File

@@ -45,7 +45,7 @@
v-tooltip.bottom.inverted="$t('flip_vertical')"
/>
<v-menu placement="top" show-arrow close-on-content-click>
<v-menu placement="top" show-arrow>
<template #activator="{ toggle }">
<v-icon
:name="aspectRatioIcon"

View File

@@ -1,5 +1,5 @@
<template>
<v-menu close-on-content-click show-arrow>
<v-menu show-arrow>
<template #activator="{ toggle }">
<span @click="toggle" class="picker">
{{ selectedOption && selectedOption.text }}