mirror of
https://github.com/directus/directus.git
synced 2026-01-27 21:17:56 -05:00
Finish settings > datamodel > collections (#414)
* Add missing slot to readme * Show and order all collections * Render correct icon for each collection type * Add ctx menu on collections overview * Use subdued colors in icons by default * Add delete collection flow * Add delete flow * Move options to separate component * Add manage / unmanage toggles * Add collections filter * Make non-nav list item icons subdued * Tweak where loader shows up when managing * Pass item key to v-table
This commit is contained in:
@@ -46,6 +46,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dense {
|
||||
#{$this} {
|
||||
margin-top: 4px;
|
||||
@@ -60,6 +61,10 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dense:not(.nav) #{$this} {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,10 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&.small {
|
||||
--v-progress-circular-size: 16px;
|
||||
--v-progress-circular-size: 20px;
|
||||
--v-progress-circular-line-size: 3px;
|
||||
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
|
||||
@@ -137,10 +137,11 @@ export default defineComponent({
|
||||
| `select` | Emitted when selected items change | `any[]` |
|
||||
|
||||
## Slots
|
||||
| Slot | Description |
|
||||
|------------------|----------------------------------|
|
||||
| `header.[value]` | Override individual header cells |
|
||||
| `item.[value]` | Override individual row cells |
|
||||
| Slot | Description |
|
||||
|------------------|----------------------------------------------------|
|
||||
| `header.[value]` | Override individual header cells |
|
||||
| `item.[value]` | Override individual row cells |
|
||||
| `item-append` | Append content at the end of each row in the table |
|
||||
|
||||
## CSS Variables
|
||||
| Variable | Default |
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</td>
|
||||
|
||||
<td class="spacer cell" />
|
||||
<td v-if="$scopedSlots['item-append']" class="append cell">
|
||||
<td v-if="$scopedSlots['item-append']" class="append cell" @click.stop>
|
||||
<slot name="item-append" />
|
||||
</td>
|
||||
</tr>
|
||||
@@ -146,6 +146,7 @@ export default defineComponent({
|
||||
.append {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -179,6 +179,21 @@
|
||||
|
||||
"all_users": "All Users",
|
||||
|
||||
"delete_collection": "Delete Collection",
|
||||
"unmanage_collection": "Unmanage Collection",
|
||||
|
||||
"update_collection_success": "Updated Collection",
|
||||
"update_collection_failed": "Couldn't Update Collection",
|
||||
"delete_collection_success": "Deleted Collection",
|
||||
"delete_collection_failed": "Couldn't Delete Collection",
|
||||
|
||||
"delete_collection_are_you_sure": "Are you sure you want to delete this collection? This will delete the collection and all items in it. This action is permanent.",
|
||||
|
||||
"collections_shown": "Collections Shown",
|
||||
"visible_collections": "Visible Collections",
|
||||
"hidden_collections": "Hidden Collections",
|
||||
"unmanaged_collections": "Unmanaged Collections",
|
||||
"system_collections": "System Collections",
|
||||
|
||||
"about_directus": "About Directus",
|
||||
"activity_log": "Activity Log",
|
||||
@@ -326,7 +341,6 @@
|
||||
"delete_are_you_sure": "This action is permanent and can not be undone. Are you sure you would like to proceed?",
|
||||
"delete_bookmark": "Delete Bookmark",
|
||||
"delete_bookmark_body": "Are you sure you want to delete this bookmark? This action cannot be undone.",
|
||||
"delete_collection_are_you_sure": "Are you sure you want to delete this collection? This action can not be undone.",
|
||||
"delete_confirmation": "Delete Confirmation",
|
||||
"delete_field_are_you_sure": "Are you sure you want to delete the field \"{field}\"? This action can not be undone.",
|
||||
"delete_role_are_you_sure": "Are you sure to delete the role \"{name}\"? This action cannot be undone.",
|
||||
|
||||
@@ -16,28 +16,59 @@
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<v-table
|
||||
:headers.sync="tableHeaders"
|
||||
:items="items"
|
||||
@click:row="openCollection"
|
||||
show-resize
|
||||
>
|
||||
<template #item.icon="{ item }">
|
||||
<v-icon class="icon" :class="{ hidden: item.hidden }" :name="item.icon" />
|
||||
</template>
|
||||
<template #drawer>
|
||||
<collections-filter v-model="activeTypes" />
|
||||
</template>
|
||||
|
||||
<template #item.collection="{ item }">
|
||||
<span class="collection" :class="{ hidden: item.hidden }">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</template>
|
||||
<div class="padding-box">
|
||||
<v-table
|
||||
:headers.sync="tableHeaders"
|
||||
:items="items"
|
||||
@click:row="openCollection"
|
||||
show-resize
|
||||
fixed-header
|
||||
item-key="collection"
|
||||
>
|
||||
<template #item.icon="{ item }">
|
||||
<v-icon
|
||||
class="icon"
|
||||
:class="{
|
||||
hidden: item.hidden,
|
||||
system: item.collection.startsWith('directus_'),
|
||||
unmanaged:
|
||||
item.managed === false &&
|
||||
item.collection.startsWith('directus_') === false,
|
||||
}"
|
||||
:name="item.icon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item.note="{ item }">
|
||||
<span class="note" :class="{ hidden: item.hidden }">
|
||||
{{ item.note }}
|
||||
</span>
|
||||
</template>
|
||||
</v-table>
|
||||
<template #item.collection="{ item }">
|
||||
<span
|
||||
class="collection"
|
||||
:class="{
|
||||
hidden: item.hidden,
|
||||
system: item.collection.startsWith('directus_'),
|
||||
unmanaged:
|
||||
item.managed === false &&
|
||||
item.collection.startsWith('directus_') === false,
|
||||
}"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #item.note="{ item }">
|
||||
<span class="note" :class="{ hidden: item.hidden }">
|
||||
{{ item.note }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #item-append="{ item }">
|
||||
<collection-options :collection="item" />
|
||||
</template>
|
||||
</v-table>
|
||||
</div>
|
||||
|
||||
<new-collection v-model="addNewActive" />
|
||||
</private-view>
|
||||
@@ -53,11 +84,16 @@ import useCollectionsStore from '@/stores/collections';
|
||||
import { Collection } from '@/stores/collections/types';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import router from '@/router';
|
||||
import { sortBy } from 'lodash';
|
||||
import CollectionOptions from './components/collection-options';
|
||||
import CollectionsFilter from './components/collections-filter';
|
||||
|
||||
export default defineComponent({
|
||||
components: { SettingsNavigation, NewCollection },
|
||||
components: { SettingsNavigation, NewCollection, CollectionOptions, CollectionsFilter },
|
||||
setup() {
|
||||
const addNewActive = ref(false);
|
||||
const activeTypes = ref(['visible', 'hidden', 'unmanaged']);
|
||||
|
||||
const collectionsStore = useCollectionsStore();
|
||||
|
||||
const tableHeaders = ref<HeaderRaw[]>([
|
||||
@@ -67,27 +103,104 @@ export default defineComponent({
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: i18n.tc('collection', 0),
|
||||
text: i18n.t('name'),
|
||||
value: 'collection',
|
||||
},
|
||||
{
|
||||
text: i18n.t('note'),
|
||||
text: i18n.t('description'),
|
||||
value: 'note',
|
||||
},
|
||||
]);
|
||||
|
||||
const items = computed(() => {
|
||||
return collectionsStore.state.collections.filter(
|
||||
({ collection }) => collection.startsWith('directus_') === false
|
||||
);
|
||||
});
|
||||
|
||||
return { addNewActive, tableHeaders, items, openCollection };
|
||||
|
||||
function openCollection({ collection }: Collection) {
|
||||
const { currentProjectKey } = useProjectsStore().state;
|
||||
router.push(`/${currentProjectKey}/settings/data-model/${collection}`);
|
||||
}
|
||||
|
||||
const { items } = useItems();
|
||||
|
||||
return {
|
||||
addNewActive,
|
||||
tableHeaders,
|
||||
items,
|
||||
openCollection,
|
||||
activeTypes,
|
||||
};
|
||||
|
||||
function useItems() {
|
||||
const visible = computed(() => {
|
||||
return sortBy(
|
||||
collectionsStore.state.collections.filter(
|
||||
(collection) =>
|
||||
collection.collection.startsWith('directus_') === false &&
|
||||
collection.managed === true &&
|
||||
collection.hidden === false
|
||||
),
|
||||
'collection'
|
||||
);
|
||||
});
|
||||
|
||||
const hidden = computed(() => {
|
||||
return sortBy(
|
||||
collectionsStore.state.collections
|
||||
.filter(
|
||||
(collection) =>
|
||||
collection.collection.startsWith('directus_') === false &&
|
||||
collection.managed === true &&
|
||||
collection.hidden === true
|
||||
)
|
||||
.map((collection) => ({ ...collection, icon: 'visibility_off' })),
|
||||
'collection'
|
||||
);
|
||||
});
|
||||
|
||||
const system = computed(() => {
|
||||
return sortBy(
|
||||
collectionsStore.state.collections
|
||||
.filter(
|
||||
(collection) => collection.collection.startsWith('directus_') === true
|
||||
)
|
||||
.map((collection) => ({ ...collection, icon: 'settings' })),
|
||||
'collection'
|
||||
);
|
||||
});
|
||||
|
||||
const unmanaged = computed(() => {
|
||||
return sortBy(
|
||||
collectionsStore.state.collections
|
||||
.filter(
|
||||
(collection) => collection.collection.startsWith('directus_') === false
|
||||
)
|
||||
.filter((collection) => collection.managed === false)
|
||||
.map((collection) => ({ ...collection, icon: 'block' })),
|
||||
'collection'
|
||||
);
|
||||
});
|
||||
|
||||
const items = computed(() => {
|
||||
const items = [];
|
||||
|
||||
if (activeTypes.value.includes('visible')) {
|
||||
items.push(visible.value);
|
||||
}
|
||||
|
||||
if (activeTypes.value.includes('hidden')) {
|
||||
items.push(hidden.value);
|
||||
}
|
||||
|
||||
if (activeTypes.value.includes('unmanaged')) {
|
||||
items.push(unmanaged.value);
|
||||
}
|
||||
|
||||
if (activeTypes.value.includes('system')) {
|
||||
items.push(system.value);
|
||||
}
|
||||
|
||||
return items.flat();
|
||||
});
|
||||
|
||||
return { items };
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -101,10 +214,24 @@ export default defineComponent({
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.v-table {
|
||||
.system {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.unmanaged {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.padding-box {
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
.v-table {
|
||||
--v-table-sticky-offset-top: 64px;
|
||||
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
--v-button-color-disabled: var(--warning);
|
||||
--v-button-background-color-disabled: var(--warning-alt);
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<v-button
|
||||
v-if="
|
||||
collection.managed === false && collection.collection.startsWith('directus_') === false
|
||||
"
|
||||
x-small
|
||||
@click="toggleManaged(true)"
|
||||
:loading="savingManaged"
|
||||
>
|
||||
{{ $t('manage') }}
|
||||
</v-button>
|
||||
<v-menu
|
||||
v-else-if="collection.collection.startsWith('directus_') === false"
|
||||
placement="left-start"
|
||||
show-arrow
|
||||
close-on-content-click
|
||||
: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" />
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item @click="toggleManaged(false)">
|
||||
<v-list-item-icon>
|
||||
<v-icon name="block" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ $t('unmanage_collection') }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-dialog v-model="deleteActive">
|
||||
<template #activator="{ on }">
|
||||
<v-list-item @click="on">
|
||||
<v-list-item-icon>
|
||||
<v-icon name="delete" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ $t('delete_collection') }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('delete_collection_are_you_sure') }}</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-button :disabled="deleting" secondary @click="deleteActive = null">
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button :loading="deleting" class="delete" @click="deleteCollection">
|
||||
{{ $t('delete_collection') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref } from '@vue/composition-api';
|
||||
import { Collection } from '@/stores/collections/types';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: Object as PropType<Collection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const { deleting, deleteActive, deleteCollection } = useDelete();
|
||||
const { savingManaged, toggleManaged } = useManage();
|
||||
|
||||
return { deleting, deleteActive, deleteCollection, savingManaged, toggleManaged };
|
||||
|
||||
function useDelete() {
|
||||
const deleting = ref(false);
|
||||
const deleteActive = ref(false);
|
||||
|
||||
return { deleting, deleteActive, deleteCollection };
|
||||
|
||||
async function deleteCollection() {
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
await collectionsStore.deleteCollection(props.collection.collection);
|
||||
deleteActive.value = false;
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useManage() {
|
||||
const savingManaged = ref(false);
|
||||
|
||||
return { savingManaged, toggleManaged };
|
||||
|
||||
async function toggleManaged(on: boolean) {
|
||||
savingManaged.value = true;
|
||||
|
||||
try {
|
||||
await collectionsStore.updateCollection(props.collection.collection, {
|
||||
managed: on,
|
||||
});
|
||||
} finally {
|
||||
savingManaged.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-button.delete {
|
||||
--v-button-background-color: var(--danger);
|
||||
}
|
||||
|
||||
.ctx-toggle {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
|
||||
&:hover {
|
||||
--v-icon-color: var(--foreground-normal);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import CollectionOptions from './collection-options.vue';
|
||||
|
||||
export { CollectionOptions };
|
||||
export default CollectionOptions;
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<drawer-detail class="collections-filter" icon="filter_list" :title="$tc('collection', 2)">
|
||||
<div class="type-label label">{{ $t('collections_shown') }}</div>
|
||||
<v-checkbox value="visible" v-model="_value" :label="$t('visible_collections')" />
|
||||
<v-checkbox value="hidden" v-model="_value" :label="$t('hidden_collections')" />
|
||||
<v-checkbox value="unmanaged" v-model="_value" :label="$t('unmanaged_collections')" />
|
||||
<v-checkbox value="system" v-model="_value" :label="$t('system_collections')" />
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(newVal) {
|
||||
emit('input', newVal);
|
||||
},
|
||||
});
|
||||
|
||||
return { _value };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.label {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
import CollectionsFilter from './collections-filter.vue';
|
||||
|
||||
export { CollectionsFilter };
|
||||
export default CollectionsFilter;
|
||||
@@ -6,6 +6,7 @@ import i18n from '@/lang/';
|
||||
import { notEmpty } from '@/utils/is-empty/';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import formatTitle from '@directus/format-title';
|
||||
import notify from '@/utils/notify';
|
||||
|
||||
export const useCollectionsStore = createStore({
|
||||
id: 'collectionsStore',
|
||||
@@ -58,6 +59,46 @@ export const useCollectionsStore = createStore({
|
||||
async dehydrate() {
|
||||
this.reset();
|
||||
},
|
||||
async updateCollection(collection: string, updates: Partial<Collection>) {
|
||||
const { currentProjectKey } = useProjectsStore().state;
|
||||
|
||||
try {
|
||||
await api.patch(`${currentProjectKey}/collections/${collection}`, updates);
|
||||
await this.hydrate();
|
||||
notify({
|
||||
type: 'success',
|
||||
title: i18n.t('update_collection_success'),
|
||||
text: collection,
|
||||
});
|
||||
} catch (error) {
|
||||
notify({
|
||||
type: 'error',
|
||||
title: i18n.t('update_collection_failed'),
|
||||
text: collection,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async deleteCollection(collection: string) {
|
||||
const { currentProjectKey } = useProjectsStore().state;
|
||||
|
||||
try {
|
||||
await api.delete(`${currentProjectKey}/collections/${collection}`);
|
||||
await this.hydrate();
|
||||
notify({
|
||||
type: 'success',
|
||||
title: i18n.t('delete_collection_success'),
|
||||
text: collection,
|
||||
});
|
||||
} catch (error) {
|
||||
notify({
|
||||
type: 'error',
|
||||
title: i18n.t('delete_collection_failed'),
|
||||
text: collection,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getCollection(collectionKey: string) {
|
||||
return (
|
||||
this.state.collections.find(
|
||||
|
||||
Reference in New Issue
Block a user