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:
Rijk van Zanten
2020-04-14 13:28:39 -04:00
committed by GitHub
parent cec5a09cde
commit 97c8f763ab
11 changed files with 407 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
import CollectionOptions from './collection-options.vue';
export { CollectionOptions };
export default CollectionOptions;

View File

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

View File

@@ -0,0 +1,4 @@
import CollectionsFilter from './collections-filter.vue';
export { CollectionsFilter };
export default CollectionsFilter;

View File

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