Move to folder (#700)

* Add move to folder on file detail

* Add save logic for move to folder on detail

* Change style to be inline

* Only stop propagation on toggle

* Add move to folder on browse
This commit is contained in:
Rijk van Zanten
2020-06-10 14:33:06 -04:00
committed by GitHub
parent aa23da6806
commit 51b939be4a
8 changed files with 330 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="v-list-group">
<v-list-item :active="active" class="activator" :to="to" @click.stop="onClick">
<v-list-item :active="active" class="activator" :to="to" @click="onClick">
<slot name="activator" :active="groupActive" />
<v-list-item-icon class="activator-icon" :class="{ active: groupActive }">
@@ -51,6 +51,7 @@ export default defineComponent({
if (props.to) return null;
if (listeners.click) return emit('click', event);
event.stopPropagation();
toggle();
}
},

View File

@@ -121,6 +121,10 @@
"submit": "Submit",
"move_to_folder": "Move to Folder",
"select_folder": "Select Folder",
"move": "Move",
"system": "System",
"include_system_collections": "Include System Collections",

View File

@@ -0,0 +1,53 @@
<template>
<v-list-item
v-if="folder.children.length === 0"
@click="clickHandler(folder.id)"
:active="currentFolder === 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 @click="clickHandler(folder.id)" :active="currentFolder === 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"
/>
</v-list-group>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
type Folder = {
id: number;
name: string;
children: Folder[];
};
export default defineComponent({
name: 'folder-picker-list-item',
props: {
folder: {
type: Object as PropType<Folder>,
required: true,
},
currentFolder: {
type: Number,
default: null,
},
clickHandler: {
type: Function,
default: () => undefined,
},
},
});
</script>

View File

@@ -0,0 +1,119 @@
<template>
<v-skeleton-loader v-if="loading" />
<div class="folder-picker" v-else>
<v-list>
<v-list-group @click="$emit('input', null)" :active="value === null">
<template #activator>
<v-list-item-icon>
<v-icon name="folder_special" />
</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="value"
:click-handler="(id) => $emit('input', id)"
/>
</v-list-group>
</v-list>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import useProjectsStore from '@/stores/projects';
import api from '@/api';
import FolderPickerListItem from './folder-picker-list-item.vue';
type FolderRaw = {
id: number;
name: string;
parent_folder: null | number;
};
type Folder = {
id: number;
name: string;
children: Folder[];
};
export default defineComponent({
components: { FolderPickerListItem },
props: {
value: {
type: Number,
default: null,
},
},
setup(props) {
const projectsStore = useProjectsStore();
const loading = ref(false);
const folders = ref<FolderRaw[]>([]);
const error = ref<any>(null);
const tree = computed<Folder[]>(() => {
return folders.value
.filter((folder) => folder.parent_folder === null)
.map((folder) => {
return {
...folder,
children: getChildFolders(folder),
};
});
function getChildFolders(folder: FolderRaw): Folder[] {
return folders.value
.filter((childFolder) => {
return childFolder.parent_folder === folder.id;
})
.map((childFolder) => {
return {
...childFolder,
children: getChildFolders(childFolder),
};
});
}
});
const selectedFolder = computed(() => {
return folders.value.find((folder) => folder.id === props.value) || {};
});
fetchFolders();
return { loading, folders, tree, selectedFolder };
async function fetchFolders() {
if (folders.value.length > 0) return;
const { currentProjectKey } = projectsStore.state;
loading.value = true;
try {
const response = await api.get(`/${currentProjectKey}/folders`, {
params: {
limit: -1,
},
});
folders.value = response.data.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
},
});
</script>
<style lang="scss" scoped>
.folder-picker {
padding: 12px;
background-color: var(--background-normal);
border-radius: var(--border-radius);
}
</style>

View File

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

View File

@@ -13,6 +13,33 @@
<template #actions>
<search-input v-model="searchQuery" />
<add-folder :parent="currentFolder" />
<v-dialog v-model="moveToDialogActive" v-if="selection.length > 0">
<template #activator="{ on }">
<v-button rounded icon @click="on" class="folder">
<v-icon name="folder" />
</v-button>
</template>
<v-card>
<v-card-title>{{ $t('move_to_folder') }}</v-card-title>
<v-card-text>
<folder-picker v-model="selectedFolder" />
</v-card-text>
<v-card-actions>
<v-button @click="moveToDialogActive = false" secondary>
{{ $t('cancel') }}
</v-button>
<v-button @click="moveToFolder" :loading="moving">
{{ $t('move') }}
</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="confirmDelete" v-if="selection.length > 0">
<template #activator="{ on }">
<v-button rounded icon class="action-delete" @click="on">
@@ -38,8 +65,6 @@
<v-icon name="edit" />
</v-button>
<add-folder :parent="currentFolder" />
<v-button rounded icon :to="addNewLink">
<v-icon name="add" />
</v-button>
@@ -89,6 +114,7 @@ import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail'
import AddFolder from '../../components/add-folder';
import SearchInput from '@/views/private/components/search-input';
import marked from 'marked';
import FolderPicker from '../../components/folder-picker';
type Item = {
[field: string]: any;
@@ -96,7 +122,7 @@ type Item = {
export default defineComponent({
name: 'files-browse',
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail, AddFolder, SearchInput },
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail, AddFolder, SearchInput, FolderPicker },
props: {},
setup() {
const layout = ref<LayoutComponent | null>(null);
@@ -153,6 +179,8 @@ export default defineComponent({
};
}
const { moveToDialogActive, moveToFolder, moving, selectedFolder } = useMovetoFolder();
return {
addNewLink,
batchDelete,
@@ -170,6 +198,10 @@ export default defineComponent({
filtersWithFolderAndType,
searchQuery,
marked,
moveToDialogActive,
moveToFolder,
moving,
selectedFolder,
};
function useBatchDelete() {
@@ -226,6 +258,32 @@ export default defineComponent({
return { breadcrumb };
}
function useMovetoFolder() {
const moveToDialogActive = ref(false);
const moving = ref(false);
const selectedFolder = ref<number | null>();
return { moveToDialogActive, moving, moveToFolder, selectedFolder };
async function moveToFolder() {
moving.value = true;
const { currentProjectKey } = projectsStore.state;
try {
await api.patch(`/${currentProjectKey}/files/${selection.value}`, {
folder: selectedFolder.value,
});
await layout.value?.refresh();
} catch (err) {
console.error(err);
} finally {
moveToDialogActive.value = false;
moving.value = false;
}
}
}
},
});
</script>
@@ -245,6 +303,13 @@ export default defineComponent({
--v-button-color-hover: var(--warning);
}
.folder {
--v-button-background-color: var(--primary-25);
--v-button-color: var(--primary);
--v-button-background-color-hover: var(--primary-50);
--v-button-color-hover: var(--primary);
}
.header-icon {
--v-button-color-disabled: var(--foreground-normal);
}

View File

@@ -90,6 +90,8 @@ export default defineComponent({
watch(
() => props.file,
async () => {
if (!props.file) return null;
creationDate.value = await localizedFormat(
new Date(props.file.uploaded_on),
String(i18n.t('date-fns_date_short'))
@@ -116,6 +118,8 @@ export default defineComponent({
return { user };
async function fetchUser() {
if (!props.file) return null;
loading.value = true;
const { currentProjectKey } = projectsStore.state;
@@ -153,6 +157,7 @@ export default defineComponent({
return { folder };
async function fetchFolder() {
if (!props.file) return null;
if (!props.file.folder) return;
loading.value = true;
const { currentProjectKey } = projectsStore.state;

View File

@@ -32,6 +32,31 @@
</v-card>
</v-dialog>
<v-dialog v-model="moveToDialogActive" v-if="isNew === false">
<template #activator="{ on }">
<v-button rounded icon :disabled="item === null" @click="on" class="folder">
<v-icon name="folder" />
</v-button>
</template>
<v-card>
<v-card-title>{{ $t('move_to_folder') }}</v-card-title>
<v-card-text>
<folder-picker v-model="selectedFolder" />
</v-card-text>
<v-card-actions>
<v-button @click="moveToDialogActive = false" secondary>
{{ $t('cancel') }}
</v-button>
<v-button @click="moveToFolder" :loading="moving">
{{ $t('move') }}
</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<v-button v-if="item && item.type.includes('image')" rounded icon @click="editActive = true" class="edit">
<v-icon name="tune" />
</v-button>
@@ -136,6 +161,8 @@ import { Field } from '@/stores/fields/types';
import FileInfoDrawerDetail from './components/file-info-drawer-detail.vue';
import marked from 'marked';
import useFormFields from '@/composables/use-form-fields';
import FolderPicker from '../../components/folder-picker';
import api from '@/api';
type Values = {
[field: string]: any;
@@ -164,6 +191,7 @@ export default defineComponent({
ImageEditor,
FileLightbox,
FileInfoDrawerDetail,
FolderPicker,
},
props: {
primaryKey: {
@@ -180,10 +208,20 @@ export default defineComponent({
const revisionsDrawerDetail = ref<Vue | null>(null);
const { isNew, edits, item, saving, loading, error, save, remove, deleting, saveAsCopy, isBatch } = useItem(
ref('directus_files'),
primaryKey
);
const {
isNew,
edits,
item,
saving,
loading,
error,
save,
remove,
deleting,
saveAsCopy,
isBatch,
refresh,
} = useItem(ref('directus_files'), primaryKey);
const hasEdits = computed<boolean>(() => Object.keys(edits.value).length > 0);
const confirmDelete = ref(false);
@@ -214,6 +252,8 @@ export default defineComponent({
const confirmLeave = ref(false);
const leaveTo = ref<string | null>(null);
const { moveToDialogActive, moveToFolder, moving, selectedFolder } = useMovetoFolder();
return {
item,
loading,
@@ -241,6 +281,10 @@ export default defineComponent({
confirmLeave,
leaveTo,
discardAndLeave,
moveToDialogActive,
moveToFolder,
moving,
selectedFolder,
};
function changeCacheBuster() {
@@ -295,6 +339,31 @@ export default defineComponent({
edits.value = {};
router.push(leaveTo.value);
}
function useMovetoFolder() {
const moveToDialogActive = ref(false);
const moving = ref(false);
const selectedFolder = ref<number | null>();
return { moveToDialogActive, moving, moveToFolder, selectedFolder };
async function moveToFolder() {
moving.value = true;
const { currentProjectKey } = projectsStore.state;
try {
await api.patch(`/${currentProjectKey}/files/${props.primaryKey}`, {
folder: selectedFolder.value,
});
await refresh();
} catch (err) {
console.error(err);
} finally {
moveToDialogActive.value = false;
moving.value = false;
}
}
}
},
});
</script>
@@ -311,7 +380,8 @@ export default defineComponent({
--v-button-background-color: var(--background-normal);
}
.edit {
.edit,
.folder {
--v-button-background-color: var(--primary-25);
--v-button-color: var(--primary);
--v-button-background-color-hover: var(--primary-50);