mirror of
https://github.com/directus/directus.git
synced 2026-02-13 11:05:05 -05:00
Merge branch 'main' into esc-to-close
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { defineModule } from '@/modules/define';
|
||||
import ActivityBrowse from './routes/browse.vue';
|
||||
import ActivityDetail from './routes/detail.vue';
|
||||
import ActivityCollection from './routes/collection.vue';
|
||||
import ActivityItem from './routes/item.vue';
|
||||
|
||||
export default defineModule(({ i18n }) => ({
|
||||
id: 'activity',
|
||||
@@ -9,19 +9,19 @@ export default defineModule(({ i18n }) => ({
|
||||
icon: 'notifications',
|
||||
routes: [
|
||||
{
|
||||
name: 'activity-browse',
|
||||
name: 'activity-collection',
|
||||
path: '/',
|
||||
component: ActivityBrowse,
|
||||
component: ActivityCollection,
|
||||
props: (route) => ({
|
||||
queryFilters: route.query,
|
||||
primaryKey: route.params.primaryKey,
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
name: 'activity-detail',
|
||||
name: 'activity-item',
|
||||
path: ':primaryKey',
|
||||
components: {
|
||||
detail: ActivityDetail,
|
||||
detail: ActivityItem,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_activity_browse'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_activity_collection'))" />
|
||||
</drawer-detail>
|
||||
<layout-drawer-detail @input="layout = $event" :value="layout" />
|
||||
<portal-target name="drawer" />
|
||||
@@ -57,7 +57,7 @@ type Item = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'activity-browse',
|
||||
name: 'activity-collection',
|
||||
components: { ActivityNavigation, FilterDrawerDetail, LayoutDrawerDetail, SearchInput },
|
||||
props: {
|
||||
primaryKey: {
|
||||
@@ -1,19 +1,34 @@
|
||||
<template>
|
||||
<v-list large>
|
||||
<template v-if="customNavItems && customNavItems.length > 0">
|
||||
<v-detail
|
||||
:active="group.accordion === 'always_open' || undefined"
|
||||
:disabled="group.accordion === 'always_open'"
|
||||
:start-open="group.accordion === 'start_open'"
|
||||
:label="group.name"
|
||||
:key="group.name"
|
||||
v-for="group in customNavItems"
|
||||
>
|
||||
<v-list-item :exact="exact" v-for="navItem in group.items" :key="navItem.to" :to="navItem.to">
|
||||
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ navItem.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-detail>
|
||||
<template v-for="(group, index) in customNavItems">
|
||||
<template
|
||||
v-if="
|
||||
(group.name === undefined || group.name === null) &&
|
||||
group.accordion === 'always_open' &&
|
||||
index === 0
|
||||
"
|
||||
>
|
||||
<v-list-item :exact="exact" v-for="navItem in group.items" :key="navItem.to" :to="navItem.to">
|
||||
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ navItem.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-detail
|
||||
:active="group.accordion === 'always_open' || undefined"
|
||||
:disabled="group.accordion === 'always_open'"
|
||||
:start-open="group.accordion === 'start_open'"
|
||||
:label="group.name || null"
|
||||
:key="group.name"
|
||||
>
|
||||
<v-list-item :exact="exact" v-for="navItem in group.items" :key="navItem.to" :to="navItem.to">
|
||||
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ navItem.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-detail>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<v-list-item v-else :exact="exact" v-for="navItem in navItems" :key="navItem.to" :to="navItem.to">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineModule } from '@/modules/define';
|
||||
import CollectionsOverview from './routes/overview.vue';
|
||||
import CollectionsBrowseOrDetail from './routes/browse-or-detail.vue';
|
||||
import CollectionsDetail from './routes/detail.vue';
|
||||
import CollectionsItemNotFound from './routes/not-found.vue';
|
||||
import Overview from './routes/overview.vue';
|
||||
import CollectionOrItem from './routes/collection-or-item.vue';
|
||||
import Item from './routes/item.vue';
|
||||
import ItemNotFound from './routes/not-found.vue';
|
||||
import { NavigationGuard } from 'vue-router';
|
||||
|
||||
const checkForSystem: NavigationGuard = (to, from, next) => {
|
||||
@@ -51,12 +51,12 @@ export default defineModule(({ i18n }) => ({
|
||||
{
|
||||
name: 'collections-overview',
|
||||
path: '/',
|
||||
component: CollectionsOverview,
|
||||
component: Overview,
|
||||
},
|
||||
{
|
||||
name: 'collections-browse',
|
||||
name: 'collections-collection',
|
||||
path: '/:collection',
|
||||
component: CollectionsBrowseOrDetail,
|
||||
component: CollectionOrItem,
|
||||
props: (route) => ({
|
||||
collection: route.params.collection,
|
||||
bookmark: route.query.bookmark,
|
||||
@@ -64,16 +64,16 @@ export default defineModule(({ i18n }) => ({
|
||||
beforeEnter: checkForSystem,
|
||||
},
|
||||
{
|
||||
name: 'collections-detail',
|
||||
name: 'collections-item',
|
||||
path: '/:collection/:primaryKey',
|
||||
component: CollectionsDetail,
|
||||
component: Item,
|
||||
props: true,
|
||||
beforeEnter: checkForSystem,
|
||||
},
|
||||
{
|
||||
name: 'collections-item-not-found',
|
||||
path: '/:collection/*',
|
||||
component: CollectionsItemNotFound,
|
||||
component: ItemNotFound,
|
||||
beforeEnter: checkForSystem,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,22 +2,23 @@
|
||||
<component
|
||||
ref="component"
|
||||
:bookmark="bookmark"
|
||||
:is="isSingle ? 'collections-detail' : 'collections-browse'"
|
||||
:is="isSingleton ? 'item-route' : 'collection-route'"
|
||||
:collection="collection"
|
||||
:singleton="isSingleton"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from '@vue/composition-api';
|
||||
import Vue from 'vue';
|
||||
import CollectionsBrowse from './browse.vue';
|
||||
import CollectionsDetail from './detail.vue';
|
||||
import CollectionRoute from './collection.vue';
|
||||
import ItemRoute from './item.vue';
|
||||
import { useCollectionsStore } from '@/stores/';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CollectionsBrowse,
|
||||
CollectionsDetail,
|
||||
CollectionRoute,
|
||||
ItemRoute,
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
@@ -33,12 +34,12 @@ export default defineComponent({
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const component = ref<Vue>();
|
||||
|
||||
const isSingle = computed(() => {
|
||||
const isSingleton = computed(() => {
|
||||
const collectionInfo = collectionsStore.getCollection(props.collection);
|
||||
return !!collectionInfo?.meta?.singleton === true;
|
||||
});
|
||||
|
||||
return { component, isSingle };
|
||||
return { component, isSingleton };
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if ((this as any).$refs?.component?.navigationGuard) {
|
||||
@@ -209,7 +209,7 @@
|
||||
class="page-description"
|
||||
v-html="
|
||||
marked(
|
||||
$t('page_help_collections_browse', {
|
||||
$t('page_help_collections_collection', {
|
||||
collection: currentCollection.name,
|
||||
})
|
||||
)
|
||||
@@ -261,7 +261,7 @@ type Item = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'collections-browse',
|
||||
name: 'collections-collection',
|
||||
components: {
|
||||
CollectionsNavigation,
|
||||
CollectionsNotFound,
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<collections-not-found v-if="error || (collectionInfo.meta.singleton === true && primaryKey !== null)" />
|
||||
<collections-not-found
|
||||
v-if="error || (collectionInfo.meta && collectionInfo.meta.singleton === true && primaryKey !== null)"
|
||||
/>
|
||||
|
||||
<private-view v-else :title="title">
|
||||
<template #title v-if="collectionInfo.meta.singleton === true">
|
||||
<template #title v-if="collectionInfo.meta && collectionInfo.meta.singleton === true">
|
||||
<h1 class="type-title">
|
||||
{{ collectionInfo.name }}
|
||||
</h1>
|
||||
@@ -21,7 +23,14 @@
|
||||
</template>
|
||||
|
||||
<template #title-outer:prepend>
|
||||
<v-button v-if="collectionInfo.meta.singleton === true" class="header-icon" rounded icon secondary disabled>
|
||||
<v-button
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton === true"
|
||||
class="header-icon"
|
||||
rounded
|
||||
icon
|
||||
secondary
|
||||
disabled
|
||||
>
|
||||
<v-icon :name="collectionInfo.icon" />
|
||||
</v-button>
|
||||
|
||||
@@ -41,7 +50,7 @@
|
||||
|
||||
<template #headline>
|
||||
<v-breadcrumb
|
||||
v-if="collectionInfo.meta.singleton === true"
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton === true"
|
||||
:items="[{ name: $t('collections'), to: '/collections' }]"
|
||||
/>
|
||||
<v-breadcrumb v-else :items="breadcrumb" />
|
||||
@@ -57,7 +66,7 @@
|
||||
v-tooltip.bottom="deleteAllowed ? $t('delete') : $t('not_allowed')"
|
||||
:disabled="item === null || deleteAllowed !== true"
|
||||
@click="on"
|
||||
v-if="collectionInfo.meta.singleton === false"
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton === false"
|
||||
>
|
||||
<v-icon name="delete" outline />
|
||||
</v-button>
|
||||
@@ -91,7 +100,7 @@
|
||||
v-tooltip.bottom="archiveTooltip"
|
||||
@click="on"
|
||||
:disabled="item === null || archiveAllowed !== true"
|
||||
v-if="collectionInfo.meta.singleton === false"
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton === false"
|
||||
>
|
||||
<v-icon :name="isArchived ? 'unarchive' : 'archive'" outline />
|
||||
</v-button>
|
||||
@@ -123,7 +132,7 @@
|
||||
|
||||
<template #append-outer>
|
||||
<save-options
|
||||
v-if="collectionInfo.meta.singleton !== true"
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton !== true"
|
||||
:disabled="hasEdits === false"
|
||||
@save-and-stay="saveAndStay"
|
||||
@save-and-add-new="saveAndAddNew"
|
||||
@@ -164,17 +173,27 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_collections_detail'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_collections_item'))" />
|
||||
</drawer-detail>
|
||||
<revisions-drawer-detail
|
||||
v-if="collectionInfo.meta.singleton === false && isBatch === false && isNew === false"
|
||||
v-if="
|
||||
collectionInfo.meta &&
|
||||
collectionInfo.meta.singleton === false &&
|
||||
isBatch === false &&
|
||||
isNew === false
|
||||
"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
ref="revisionsDrawerDetail"
|
||||
@revert="refresh"
|
||||
/>
|
||||
<comments-drawer-detail
|
||||
v-if="collectionInfo.meta.singleton === false && isBatch === false && isNew === false"
|
||||
v-if="
|
||||
collectionInfo.meta &&
|
||||
collectionInfo.meta.singleton === false &&
|
||||
isBatch === false &&
|
||||
isNew === false
|
||||
"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
@@ -224,6 +243,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
singleton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const form = ref<HTMLElement>();
|
||||
@@ -359,7 +382,7 @@ export default defineComponent({
|
||||
if (saveAllowed.value === false || hasEdits.value === false) return;
|
||||
|
||||
await save();
|
||||
router.push(`/collections/${props.collection}`);
|
||||
if (props.singleton === false) router.push(`/collections/${props.collection}`);
|
||||
}
|
||||
|
||||
async function saveAndStay() {
|
||||
@@ -1,21 +1,16 @@
|
||||
<template>
|
||||
<v-divider v-if="section.divider" />
|
||||
<v-list-group v-else-if="section.children" :dense="dense">
|
||||
<v-list-group v-else-if="section.children" :dense="dense" :multiple="false" :value="section.to">
|
||||
<template #activator>
|
||||
<v-list-item-icon v-if="section.icon !== undefined"><v-icon :name="section.icon" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-text>{{ section.name }}</v-list-item-text>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
<navigation-list-item
|
||||
v-for="(childSection, index) in section.children"
|
||||
:key="index"
|
||||
:section="childSection"
|
||||
dense
|
||||
/>
|
||||
<navigation-list-item v-for="(child, index) in section.children" :key="index" :section="child" dense />
|
||||
</v-list-group>
|
||||
|
||||
<v-list-item v-else :to="`/docs${section.to}`" :dense="dense">
|
||||
<v-list-item v-else :to="`/docs${section.to}`" :dense="dense" :value="section.to">
|
||||
<v-list-item-icon v-if="section.icon !== undefined"><v-icon :name="section.icon" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-text>{{ section.name }}</v-list-item-text>
|
||||
@@ -24,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { Link, Group } from '@directus/docs';
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
@@ -1,18 +1,65 @@
|
||||
<template>
|
||||
<v-list large>
|
||||
<v-list large :multiple="false" v-model="selection" :mandatory="false">
|
||||
<navigation-item v-for="item in navSections" :key="item.name" :section="item" />
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, computed, watch, ref } from '@vue/composition-api';
|
||||
import NavigationItem from './navigation-item.vue';
|
||||
import { nav } from '@directus/docs';
|
||||
|
||||
function spreadPath(path: string) {
|
||||
const sections = path.substr(1).split('/');
|
||||
if (sections.length === 0) return [];
|
||||
|
||||
const paths: string[] = ['/' + sections[0]];
|
||||
|
||||
for (let i = 1; i < sections.length; i++) {
|
||||
paths.push(paths[i - 1] + '/' + sections[i]);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { NavigationItem },
|
||||
setup() {
|
||||
return { navSections: nav.app };
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
default: '/docs',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const _selection = ref<string[] | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.path,
|
||||
(newPath) => {
|
||||
if (newPath === null) return;
|
||||
_selection.value = spreadPath(newPath.replace('/docs', ''));
|
||||
}
|
||||
);
|
||||
|
||||
const selection = computed({
|
||||
get() {
|
||||
if (_selection.value === null && props.path !== null)
|
||||
_selection.value = spreadPath(props.path.replace('/docs', ''));
|
||||
return _selection.value || [];
|
||||
},
|
||||
set(newSelection: string[]) {
|
||||
if (newSelection.length === 0) {
|
||||
_selection.value = [];
|
||||
} else {
|
||||
if (_selection.value && _selection.value.includes(newSelection[0])) {
|
||||
_selection.value = _selection.value.filter((s) => s !== newSelection[0]);
|
||||
} else {
|
||||
_selection.value = spreadPath(newSelection[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { navSections: nav.app, selection };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -31,19 +31,19 @@ export default defineModule(({ i18n }) => {
|
||||
for (const doc of directory.children) {
|
||||
if (doc.type === 'file') {
|
||||
routes.push({
|
||||
path: '/' + doc.path.replace('.md', ''),
|
||||
path: '/' + doc.path.replace('.md', '').replaceAll('\\', '/'),
|
||||
component: StaticDocs,
|
||||
});
|
||||
} else if (doc.type === 'directory') {
|
||||
routes.push({
|
||||
path: '/' + doc.path,
|
||||
redirect: '/' + doc.children![0].path.replace('.md', ''),
|
||||
});
|
||||
if (doc.path && doc.children && doc.children.length > 0)
|
||||
routes.push({
|
||||
path: '/' + doc.path.replaceAll('\\', '/'),
|
||||
redirect: '/' + doc.children![0].path.replace('.md', '').replaceAll('\\', '/'),
|
||||
});
|
||||
|
||||
routes.push(...parseRoutes(doc));
|
||||
}
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<private-view :title="$t('page_not_found')">
|
||||
<template #navigation>
|
||||
<docs-navigation />
|
||||
<docs-navigation :path="path" />
|
||||
</template>
|
||||
|
||||
<div class="not-found">
|
||||
@@ -13,12 +13,25 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import DocsNavigation from '../components/navigation.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NotFound',
|
||||
components: { DocsNavigation },
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
next((vm: any) => {
|
||||
vm.path = to.path;
|
||||
});
|
||||
},
|
||||
async beforeRouteUpdate(to, from, next) {
|
||||
this.path = to.path;
|
||||
next();
|
||||
},
|
||||
setup() {
|
||||
const path = ref<string | null>(null);
|
||||
return { path };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<template #navigation>
|
||||
<docs-navigation />
|
||||
<docs-navigation :path="path" />
|
||||
</template>
|
||||
|
||||
<div class="docs-content selectable">
|
||||
@@ -55,16 +55,18 @@ export default defineComponent({
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
const md = await getMarkdownForPath(to.path);
|
||||
|
||||
next((vm) => {
|
||||
(vm as any).markdown = md;
|
||||
next((vm: any) => {
|
||||
vm.markdown = md;
|
||||
vm.path = to.path;
|
||||
});
|
||||
},
|
||||
async beforeRouteUpdate(to, from, next) {
|
||||
this.markdown = await getMarkdownForPath(to.path);
|
||||
|
||||
this.path = to.path;
|
||||
next();
|
||||
},
|
||||
setup() {
|
||||
const path = ref<string | null>(null);
|
||||
const markdown = ref('');
|
||||
const view = ref<Vue>();
|
||||
|
||||
@@ -83,7 +85,7 @@ export default defineComponent({
|
||||
view.value?.$data.contentEl?.scrollTo({ top: 0 });
|
||||
});
|
||||
|
||||
return { markdown, title, markdownWithoutTitle, view, marked };
|
||||
return { markdown, title, markdownWithoutTitle, view, marked, path };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -41,11 +41,25 @@
|
||||
<dd>{{ file.checksum }}</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="user">
|
||||
<div v-if="user_created">
|
||||
<dt>{{ $t('owner') }}</dt>
|
||||
<dd>
|
||||
<user-popover :user="user.id">
|
||||
<router-link :to="user.link">{{ user.name }}</router-link>
|
||||
<user-popover :user="user_created.id">
|
||||
<router-link :to="user_created.link">{{ user_created.name }}</router-link>
|
||||
</user-popover>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="modificationDate">
|
||||
<dt>{{ $t('modified') }}</dt>
|
||||
<dd>{{ modificationDate }}</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="user_modified">
|
||||
<dt>{{ $t('edited_by') }}</dt>
|
||||
<dd>
|
||||
<user-popover :user="user_modified.id">
|
||||
<router-link :to="user_modified.link">{{ user_modified.name }}</router-link>
|
||||
</user-popover>
|
||||
</dd>
|
||||
</div>
|
||||
@@ -89,7 +103,7 @@
|
||||
|
||||
<v-divider />
|
||||
|
||||
<div class="page-description" v-html="marked($t('page_help_files_detail'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_files_item'))" />
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
@@ -123,14 +137,15 @@ export default defineComponent({
|
||||
return bytes(props.file.filesize, { decimalPlaces: 2, unitSeparator: ' ' }); // { locale: i18n.locale.split('-')[0] }
|
||||
});
|
||||
|
||||
const { creationDate } = useCreationDate();
|
||||
const { user } = useUser();
|
||||
const { creationDate, modificationDate } = useDates();
|
||||
const { userCreated, userModified } = useUser();
|
||||
const { folder } = useFolder();
|
||||
|
||||
return { readableMimeType, size, creationDate, user, folder, marked };
|
||||
return { readableMimeType, size, creationDate, modificationDate, userCreated, userModified, folder, marked };
|
||||
|
||||
function useCreationDate() {
|
||||
function useDates() {
|
||||
const creationDate = ref<string | null>(null);
|
||||
const modificationDate = ref<string | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.file,
|
||||
@@ -141,11 +156,18 @@ export default defineComponent({
|
||||
new Date(props.file.uploaded_on),
|
||||
String(i18n.t('date-fns_date_short'))
|
||||
);
|
||||
|
||||
if (props.file.modified_on) {
|
||||
modificationDate.value = await localizedFormat(
|
||||
new Date(props.file.modified_on),
|
||||
String(i18n.t('date-fns_date_short'))
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return { creationDate };
|
||||
return { creationDate, modificationDate };
|
||||
}
|
||||
|
||||
function useUser() {
|
||||
@@ -156,11 +178,12 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const user = ref<User | null>(null);
|
||||
const userCreated = ref<User | null>(null);
|
||||
const userModified = ref<User | null>(null);
|
||||
|
||||
watch(() => props.file, fetchUser, { immediate: true });
|
||||
|
||||
return { user };
|
||||
return { userCreated, userModified };
|
||||
|
||||
async function fetchUser() {
|
||||
if (!props.file) return null;
|
||||
@@ -177,11 +200,27 @@ export default defineComponent({
|
||||
|
||||
const { id, first_name, last_name, role } = response.data.data;
|
||||
|
||||
user.value = {
|
||||
userCreated.value = {
|
||||
id: props.file.uploaded_by,
|
||||
name: first_name + ' ' + last_name,
|
||||
link: `/users/${id}`,
|
||||
};
|
||||
|
||||
if (props.file.modified_by) {
|
||||
const response = await api.get(`/users/${props.file.modified_by}`, {
|
||||
params: {
|
||||
fields: ['id', 'first_name', 'last_name', 'role'],
|
||||
},
|
||||
});
|
||||
|
||||
const { id, first_name, last_name, role } = response.data.data;
|
||||
|
||||
userModified.value = {
|
||||
id: props.file.modified_by,
|
||||
name: first_name + ' ' + last_name,
|
||||
link: `/users/${id}`,
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineModule } from '@/modules/define';
|
||||
import FilesBrowse from './routes/browse.vue';
|
||||
import FilesDetail from './routes/detail.vue';
|
||||
import FilesAddNew from './routes/add-new.vue';
|
||||
import Collection from './routes/collection.vue';
|
||||
import Item from './routes/item.vue';
|
||||
import AddNew from './routes/add-new.vue';
|
||||
|
||||
export default defineModule(({ i18n }) => ({
|
||||
id: 'files',
|
||||
@@ -9,9 +9,9 @@ export default defineModule(({ i18n }) => ({
|
||||
icon: 'folder',
|
||||
routes: [
|
||||
{
|
||||
name: 'files-browse',
|
||||
name: 'files-collection',
|
||||
path: '/',
|
||||
component: FilesBrowse,
|
||||
component: Collection,
|
||||
props: (route) => ({
|
||||
queryFilters: route.query,
|
||||
}),
|
||||
@@ -20,36 +20,36 @@ export default defineModule(({ i18n }) => ({
|
||||
path: '+',
|
||||
name: 'add-file',
|
||||
components: {
|
||||
addNew: FilesAddNew,
|
||||
addNew: AddNew,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/all',
|
||||
component: FilesBrowse,
|
||||
component: Collection,
|
||||
props: () => ({
|
||||
special: 'all',
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/mine',
|
||||
component: FilesBrowse,
|
||||
component: Collection,
|
||||
props: () => ({
|
||||
special: 'mine',
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/recent',
|
||||
component: FilesBrowse,
|
||||
component: Collection,
|
||||
props: () => ({
|
||||
special: 'recent',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'files-detail',
|
||||
name: 'files-item',
|
||||
path: '/:primaryKey',
|
||||
component: FilesDetail,
|
||||
component: Item,
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="title">
|
||||
<private-view :title="title" :class="{ dragging }">
|
||||
<template #headline v-if="breadcrumb">
|
||||
<v-breadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
@@ -128,11 +128,18 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_files_browse'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_files_collection'))" />
|
||||
</drawer-detail>
|
||||
<layout-drawer-detail @input="layout = $event" :value="layout" />
|
||||
<portal-target name="drawer" />
|
||||
</template>
|
||||
|
||||
<template v-if="showDropEffect">
|
||||
<div class="drop-border top" />
|
||||
<div class="drop-border right" />
|
||||
<div class="drop-border bottom" />
|
||||
<div class="drop-border left" />
|
||||
</template>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
@@ -152,16 +159,18 @@ import FolderPicker from '../components/folder-picker.vue';
|
||||
import emitter, { Events } from '@/events';
|
||||
import router from '@/router';
|
||||
import Vue from 'vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useNotificationsStore, useUserStore } from '@/stores';
|
||||
import { subDays } from 'date-fns';
|
||||
import useFolders from '../composables/use-folders';
|
||||
import useEventListener from '@/composables/use-event-listener';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'files-browse',
|
||||
name: 'files-collection',
|
||||
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail, AddFolder, SearchInput, FolderPicker },
|
||||
props: {
|
||||
queryFilters: {
|
||||
@@ -179,6 +188,7 @@ export default defineComponent({
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
|
||||
const { layout, layoutOptions, layoutQuery, filters, searchQuery } = usePreset(ref('directus_files'));
|
||||
const { batchLink } = useLinks();
|
||||
@@ -242,6 +252,13 @@ export default defineComponent({
|
||||
onMounted(() => emitter.on(Events.upload, refresh));
|
||||
onUnmounted(() => emitter.off(Events.upload, refresh));
|
||||
|
||||
const { onDragEnter, onDragLeave, onDrop, onDragOver, showDropEffect, dragging } = useFileUpload();
|
||||
|
||||
useEventListener(window, 'dragenter', onDragEnter);
|
||||
useEventListener(window, 'dragover', onDragOver);
|
||||
useEventListener(window, 'dragleave', onDragLeave);
|
||||
useEventListener(window, 'drop', onDrop);
|
||||
|
||||
return {
|
||||
batchDelete,
|
||||
batchLink,
|
||||
@@ -264,6 +281,11 @@ export default defineComponent({
|
||||
selectedFolder,
|
||||
refresh,
|
||||
clearFilters,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
showDropEffect,
|
||||
onDrop,
|
||||
dragging,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
@@ -381,6 +403,133 @@ export default defineComponent({
|
||||
filters.value = [];
|
||||
searchQuery.value = null;
|
||||
}
|
||||
|
||||
function useFileUpload() {
|
||||
const showDropEffect = ref(false);
|
||||
|
||||
let dragNotificationID: string;
|
||||
let fileUploadNotificationID: string;
|
||||
|
||||
const dragCounter = ref(0);
|
||||
|
||||
const dragging = computed(() => dragCounter.value > 0);
|
||||
|
||||
return { onDragEnter, onDragLeave, onDrop, onDragOver, showDropEffect, dragging };
|
||||
|
||||
function enableDropEffect() {
|
||||
showDropEffect.value = true;
|
||||
|
||||
dragNotificationID = notificationsStore.add({
|
||||
title: i18n.t('drop_to_upload'),
|
||||
icon: 'cloud_upload',
|
||||
type: 'info',
|
||||
persist: true,
|
||||
closeable: false,
|
||||
});
|
||||
}
|
||||
|
||||
function disableDropEffect() {
|
||||
showDropEffect.value = false;
|
||||
|
||||
if (dragNotificationID) {
|
||||
notificationsStore.remove(dragNotificationID);
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnter(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value++;
|
||||
|
||||
const isDropzone = event.target && (event.target as HTMLElement).getAttribute?.('data-dropzone') === '';
|
||||
|
||||
if (dragCounter.value === 1 && showDropEffect.value === false && isDropzone === false) {
|
||||
enableDropEffect();
|
||||
}
|
||||
|
||||
if (isDropzone) {
|
||||
disableDropEffect();
|
||||
dragCounter.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onDragLeave(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value--;
|
||||
|
||||
if (dragCounter.value === 0) {
|
||||
disableDropEffect();
|
||||
}
|
||||
|
||||
if (event.target && (event.target as HTMLElement).getAttribute?.('data-dropzone') === '') {
|
||||
enableDropEffect();
|
||||
dragCounter.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function onDrop(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
showDropEffect.value = false;
|
||||
|
||||
dragCounter.value = 0;
|
||||
|
||||
if (dragNotificationID) {
|
||||
notificationsStore.remove(dragNotificationID);
|
||||
}
|
||||
|
||||
const files = [...event.dataTransfer.files];
|
||||
|
||||
fileUploadNotificationID = notificationsStore.add({
|
||||
title: i18n.tc('upload_file_indeterminate', files.length, {
|
||||
done: 0,
|
||||
total: files.length,
|
||||
}),
|
||||
type: 'info',
|
||||
persist: true,
|
||||
closeable: false,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
await uploadFiles(files, {
|
||||
preset: {
|
||||
folder: props.queryFilters?.folder || null,
|
||||
},
|
||||
onProgressChange: (progress) => {
|
||||
const percentageDone = progress.reduce((val, cur) => (val += cur)) / progress.length;
|
||||
|
||||
const total = files.length;
|
||||
const done = progress.filter((p) => p === 100).length;
|
||||
|
||||
notificationsStore.update(fileUploadNotificationID, {
|
||||
title: i18n.tc('upload_file_indeterminate', files.length, {
|
||||
done,
|
||||
total,
|
||||
}),
|
||||
loading: false,
|
||||
progress: percentageDone,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
notificationsStore.remove(fileUploadNotificationID);
|
||||
emitter.emit(Events.upload);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -414,4 +563,52 @@ export default defineComponent({
|
||||
.layout {
|
||||
--layout-offset-top: 64px;
|
||||
}
|
||||
|
||||
.drop-border {
|
||||
position: fixed;
|
||||
z-index: 500;
|
||||
background-color: var(--primary);
|
||||
|
||||
&.top,
|
||||
&.bottom {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&.left,
|
||||
&.right {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dragging {
|
||||
::v-deep * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
::v-deep [data-dropzone] {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -111,7 +111,7 @@
|
||||
<files-navigation :current-folder="item && item.folder" />
|
||||
</template>
|
||||
|
||||
<div class="file-detail">
|
||||
<div class="file-item">
|
||||
<file-preview
|
||||
v-if="isBatch === false && item"
|
||||
:src="fileSrc"
|
||||
@@ -200,7 +200,7 @@ type Values = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'files-detail',
|
||||
name: 'files-item',
|
||||
beforeRouteLeave(to, from, next) {
|
||||
const self = this as any;
|
||||
const hasEdits = Object.keys(self.edits).length > 0;
|
||||
@@ -274,6 +274,8 @@ export default defineComponent({
|
||||
'checksum',
|
||||
'uploaded_by',
|
||||
'uploaded_on',
|
||||
'modified_by',
|
||||
'modified_on',
|
||||
'duration',
|
||||
'folder',
|
||||
'charset',
|
||||
@@ -287,9 +289,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const to = computed(() => {
|
||||
if(item.value && item.value?.folder) return `/files?folder=${item.value.folder}`
|
||||
else return '/files'
|
||||
})
|
||||
if (item.value && item.value?.folder) return `/files?folder=${item.value.folder}`;
|
||||
else return '/files';
|
||||
});
|
||||
|
||||
const { formFields } = useFormFields(fieldsFiltered);
|
||||
|
||||
@@ -332,7 +334,7 @@ export default defineComponent({
|
||||
selectedFolder,
|
||||
fileSrc,
|
||||
form,
|
||||
to
|
||||
to,
|
||||
};
|
||||
|
||||
function changeCacheBuster() {
|
||||
@@ -449,7 +451,7 @@ export default defineComponent({
|
||||
--v-button-color-hover: var(--primary);
|
||||
}
|
||||
|
||||
.file-detail {
|
||||
.file-item {
|
||||
padding: var(--content-padding);
|
||||
padding-bottom: var(--content-padding-bottom);
|
||||
}
|
||||
@@ -26,12 +26,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs } from '@vue/composition-api';
|
||||
import { defineComponent, toRefs, computed } from '@vue/composition-api';
|
||||
import { i18n } from '@/lang';
|
||||
import { version } from '../../../../package.json';
|
||||
import { useProjectInfo } from '../composables/use-project-info';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { parsedInfo } = useProjectInfo();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
icon: 'public',
|
||||
@@ -61,20 +64,33 @@ export default defineComponent({
|
||||
},
|
||||
];
|
||||
|
||||
const externalItems = [
|
||||
{
|
||||
icon: 'bug_report',
|
||||
name: i18n.t('report_bug'),
|
||||
href: 'https://github.com/directus/next/issues/new?body=%23%23%23+Project+Details%0A%60%60%60%0ADirectus+Version:+'+version+'%0AEnvironment:+Development%0AOS:+Mac%0ADatabase:+MySQL+5.2%0A%60%60%60',
|
||||
outline: true,
|
||||
},
|
||||
{
|
||||
icon: 'new_releases',
|
||||
name: i18n.t('request_feature'),
|
||||
href: 'https://github.com/directus/next/discussions/new',
|
||||
outline: true,
|
||||
},
|
||||
];
|
||||
const externalItems = computed(() => {
|
||||
const debugInfo = `<!-- Please put a detailed explanation of the problem here. -->
|
||||
|
||||
---
|
||||
|
||||
### Project details
|
||||
Directus Version: ${parsedInfo.value?.directus.version}
|
||||
Environment: ${process.env.NODE_ENV}
|
||||
OS: ${parsedInfo.value?.os.type} ${parsedInfo.value?.os.version}
|
||||
Node: ${parsedInfo.value?.node.version}
|
||||
`;
|
||||
|
||||
return [
|
||||
{
|
||||
icon: 'bug_report',
|
||||
name: i18n.t('report_bug'),
|
||||
href: `https://github.com/directus/next/issues/new?body=${encodeURIComponent(debugInfo)}`,
|
||||
outline: true,
|
||||
},
|
||||
{
|
||||
icon: 'new_releases',
|
||||
name: i18n.t('request_feature'),
|
||||
href: 'https://github.com/directus/next/discussions/new',
|
||||
outline: true,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
return { version, navItems, externalItems };
|
||||
},
|
||||
|
||||
66
app/src/modules/settings/composables/use-project-info.ts
Normal file
66
app/src/modules/settings/composables/use-project-info.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ref, computed } from '@vue/composition-api';
|
||||
import prettyMS from 'pretty-ms';
|
||||
import bytes from 'bytes';
|
||||
import api from '@/api';
|
||||
|
||||
type ServerInfo = {
|
||||
directus: {
|
||||
version: string;
|
||||
};
|
||||
node: {
|
||||
version: string;
|
||||
uptime: number;
|
||||
};
|
||||
os: {
|
||||
type: string;
|
||||
version: string;
|
||||
uptime: number;
|
||||
totalmem: number;
|
||||
};
|
||||
};
|
||||
|
||||
export function useProjectInfo() {
|
||||
const info = ref<ServerInfo>();
|
||||
const loading = ref(false);
|
||||
const error = ref<any>();
|
||||
|
||||
const parsedInfo = computed(() => {
|
||||
if (!info.value) return null;
|
||||
|
||||
return {
|
||||
directus: {
|
||||
version: info.value.directus.version,
|
||||
},
|
||||
node: {
|
||||
version: info.value.node.version,
|
||||
uptime: prettyMS(info.value.node.uptime * 1000),
|
||||
},
|
||||
os: {
|
||||
type: info.value.os.type,
|
||||
version: info.value.os.version,
|
||||
uptime: prettyMS(info.value.os.uptime * 1000),
|
||||
totalmem: bytes(info.value.os.totalmem),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (!info.value) {
|
||||
fetchInfo();
|
||||
}
|
||||
|
||||
return { info, parsedInfo, loading, error };
|
||||
|
||||
async function fetchInfo() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await api.get('/server/info');
|
||||
info.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
import { defineModule } from '@/modules/define';
|
||||
import SettingsProject from './routes/project/project.vue';
|
||||
import SettingsCollections from './routes/data-model/collections/collections.vue';
|
||||
import SettingsNewCollection from './routes/data-model/new-collection.vue';
|
||||
import SettingsFields from './routes/data-model/fields/fields.vue';
|
||||
import SettingsFieldDetail from './routes/data-model/field-detail/field-detail.vue';
|
||||
import SettingsRolesBrowse from './routes/roles/browse.vue';
|
||||
import SettingsRolesPublicDetail from './routes/roles/public-detail.vue';
|
||||
import SettingsRolesPermissionsDetail from './routes/roles/permissions-detail/permissions-detail.vue';
|
||||
import SettingsRolesDetail from './routes/roles/detail/detail.vue';
|
||||
import SettingsPresetsBrowse from './routes/presets/browse/browse.vue';
|
||||
import SettingsPresetsDetail from './routes/presets/detail.vue';
|
||||
import SettingsWebhooksBrowse from './routes/webhooks/browse.vue';
|
||||
import SettingsWebhooksDetail from './routes/webhooks/detail.vue';
|
||||
import SettingsNewRole from './routes/roles/add-new.vue';
|
||||
import SettingsNotFound from './routes/not-found.vue';
|
||||
import Project from './routes/project/project.vue';
|
||||
import Collections from './routes/data-model/collections/collections.vue';
|
||||
import NewCollection from './routes/data-model/new-collection.vue';
|
||||
import Fields from './routes/data-model/fields/fields.vue';
|
||||
import FieldDetail from './routes/data-model/field-detail/field-detail.vue';
|
||||
import RolesCollection from './routes/roles/collection.vue';
|
||||
import RolesPublicItem from './routes/roles/public-item.vue';
|
||||
import RolesPermissionsDetail from './routes/roles/permissions-detail/permissions-detail.vue';
|
||||
import RolesItem from './routes/roles/item/item.vue';
|
||||
import PresetsCollection from './routes/presets/collection/collection.vue';
|
||||
import PresetsItem from './routes/presets/item.vue';
|
||||
import WebhooksCollection from './routes/webhooks/collection.vue';
|
||||
import WebhooksItem from './routes/webhooks/item.vue';
|
||||
import NewRole from './routes/roles/add-new.vue';
|
||||
import NotFound from './routes/not-found.vue';
|
||||
import api from '@/api';
|
||||
import { useCollection } from '@/composables/use-collection';
|
||||
import { ref } from '@vue/composition-api';
|
||||
import { useCollectionsStore, useFieldsStore } from '@/stores';
|
||||
|
||||
export default defineModule(({ i18n }) => ({
|
||||
id: 'settings',
|
||||
@@ -31,18 +32,23 @@ export default defineModule(({ i18n }) => ({
|
||||
{
|
||||
name: 'settings-project',
|
||||
path: '/project',
|
||||
component: SettingsProject,
|
||||
component: Project,
|
||||
},
|
||||
{
|
||||
name: 'settings-collections',
|
||||
path: '/data-model',
|
||||
component: SettingsCollections,
|
||||
component: Collections,
|
||||
beforeEnter(to, from, next) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
collectionsStore.hydrate();
|
||||
next();
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '+',
|
||||
name: 'settings-add-new',
|
||||
components: {
|
||||
add: SettingsNewCollection,
|
||||
add: NewCollection,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -50,14 +56,17 @@ export default defineModule(({ i18n }) => ({
|
||||
{
|
||||
name: 'settings-fields',
|
||||
path: '/data-model/:collection',
|
||||
component: SettingsFields,
|
||||
component: Fields,
|
||||
async beforeEnter(to, from, next) {
|
||||
const { info } = useCollection(ref(to.params.collection));
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
if (!info.value?.meta) {
|
||||
await api.patch(`/collections/${to.params.collection}`, { meta: {} });
|
||||
}
|
||||
|
||||
fieldsStore.hydrate();
|
||||
|
||||
next();
|
||||
},
|
||||
props: (route) => ({
|
||||
@@ -70,78 +79,78 @@ export default defineModule(({ i18n }) => ({
|
||||
path: ':field',
|
||||
name: 'settings-fields-field',
|
||||
components: {
|
||||
field: SettingsFieldDetail,
|
||||
field: FieldDetail,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'settings-roles-browse',
|
||||
name: 'settings-roles-collection',
|
||||
path: '/roles',
|
||||
component: SettingsRolesBrowse,
|
||||
component: RolesCollection,
|
||||
children: [
|
||||
{
|
||||
path: '+',
|
||||
name: 'settings-add-new-role',
|
||||
components: {
|
||||
add: SettingsNewRole,
|
||||
add: NewRole,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/roles/public',
|
||||
component: SettingsRolesPublicDetail,
|
||||
component: RolesPublicItem,
|
||||
props: true,
|
||||
children: [
|
||||
{
|
||||
path: ':permissionKey',
|
||||
components: {
|
||||
permissionsDetail: SettingsRolesPermissionsDetail,
|
||||
permissionsDetail: RolesPermissionsDetail,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'settings-roles-detail',
|
||||
name: 'settings-roles-item',
|
||||
path: '/roles/:primaryKey',
|
||||
component: SettingsRolesDetail,
|
||||
component: RolesItem,
|
||||
props: true,
|
||||
children: [
|
||||
{
|
||||
path: ':permissionKey',
|
||||
components: {
|
||||
permissionsDetail: SettingsRolesPermissionsDetail,
|
||||
permissionsDetail: RolesPermissionsDetail,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'settings-presets-browse',
|
||||
name: 'settings-presets-collection',
|
||||
path: '/presets',
|
||||
component: SettingsPresetsBrowse,
|
||||
component: PresetsCollection,
|
||||
},
|
||||
{
|
||||
name: 'settings-presets-detail',
|
||||
name: 'settings-presets-item',
|
||||
path: '/presets/:id',
|
||||
component: SettingsPresetsDetail,
|
||||
component: PresetsItem,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'settings-webhooks-browse',
|
||||
name: 'settings-webhooks-collection',
|
||||
path: '/webhooks',
|
||||
component: SettingsWebhooksBrowse,
|
||||
component: WebhooksCollection,
|
||||
},
|
||||
{
|
||||
name: 'settings-webhooks-detail',
|
||||
name: 'settings-webhooks-item',
|
||||
path: '/webhooks/:primaryKey',
|
||||
component: SettingsWebhooksDetail,
|
||||
component: WebhooksItem,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'settings-not-found',
|
||||
path: '*',
|
||||
component: SettingsNotFound,
|
||||
component: NotFound,
|
||||
},
|
||||
],
|
||||
preRegisterCheck: (user) => {
|
||||
|
||||
@@ -22,13 +22,22 @@
|
||||
v-model="fieldData.meta.display_options"
|
||||
/>
|
||||
|
||||
<component v-model="fieldData" :is="`display-options-${selectedDisplay.id}`" v-else />
|
||||
<component
|
||||
v-model="fieldData.meta.display_options"
|
||||
:collection="collection"
|
||||
:field-data="fieldData"
|
||||
:relations="relations"
|
||||
:new-fields="newFields"
|
||||
:new-collections="newCollections"
|
||||
:is="`display-options-${selectedDisplay.id}`"
|
||||
v-else
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { defineComponent, computed, toRefs } from '@vue/composition-api';
|
||||
import { getDisplays } from '@/displays';
|
||||
import { getInterfaces } from '@/interfaces';
|
||||
import { FancySelectItem } from '@/components/v-fancy-select/types';
|
||||
@@ -42,6 +51,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const displays = getDisplays();
|
||||
@@ -95,7 +108,9 @@ export default defineComponent({
|
||||
return displays.value.find((display) => display.id === state.fieldData.meta.display);
|
||||
});
|
||||
|
||||
return { fieldData: state.fieldData, selectItems, selectedDisplay };
|
||||
const { fieldData, relations, newCollections, newFields } = toRefs(state);
|
||||
|
||||
return { fieldData, selectItems, selectedDisplay, relations, newCollections, newFields };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="type-title">{{ $t('schema_field_title') }}</h2>
|
||||
|
||||
<div class="form">
|
||||
<div class="field half-left" v-if="fieldData.meta">
|
||||
<div class="label type-label">{{ $t('readonly') }}</div>
|
||||
<v-checkbox v-model="fieldData.meta.readonly" :label="$t('disabled_editing_value')" block />
|
||||
</div>
|
||||
|
||||
<div class="field half-right" v-if="fieldData.meta">
|
||||
<div class="label type-label">{{ $t('hidden') }}</div>
|
||||
<v-checkbox v-model="fieldData.meta.hidden" :label="$t('hidden_on_detail')" block />
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<div class="label type-label">{{ $t('note') }}</div>
|
||||
<v-input v-model="fieldData.meta.note" :placeholder="$t('add_note')" />
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<div class="label type-label">{{ $t('translations') }}</div>
|
||||
<interface-repeater
|
||||
v-model="fieldData.meta.translations"
|
||||
:template="'{{ translation }} ({{ language }})'"
|
||||
:fields="[
|
||||
{
|
||||
field: 'language',
|
||||
type: 'string',
|
||||
name: $t('language'),
|
||||
meta: {
|
||||
interface: 'system-language',
|
||||
width: 'half',
|
||||
},
|
||||
schema: {
|
||||
default_value: 'en-US',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'translation',
|
||||
type: 'string',
|
||||
name: $t('translation'),
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'half',
|
||||
options: {
|
||||
placeholder: 'Enter a translation...',
|
||||
},
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import useSync from '@/composables/use-sync';
|
||||
import { types } from '@/types';
|
||||
import i18n from '@/lang';
|
||||
import { state } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
isExisting: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
return {
|
||||
fieldData: state.fieldData,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.type-title {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form {
|
||||
--v-form-vertical-gap: 32px;
|
||||
--v-form-horizontal-gap: 32px;
|
||||
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
--v-input-font-family: var(--family-monospace);
|
||||
}
|
||||
|
||||
.required {
|
||||
--v-icon-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
@@ -178,6 +178,7 @@ export default defineComponent({
|
||||
interface: 'one-to-many',
|
||||
},
|
||||
});
|
||||
state.relations[0].one_field = state.relations[0].one_collection;
|
||||
} else {
|
||||
state.newFields = state.newFields.filter((field: any) => field.$type !== 'corresponding');
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field half">
|
||||
<div class="label type-label">
|
||||
{{ $t('type') }}
|
||||
<v-icon class="required" sup name="star" />
|
||||
@@ -34,12 +34,54 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<div class="label type-label">{{ $t('note') }}</div>
|
||||
<v-input v-model="fieldData.meta.note" :placeholder="$t('add_note')" />
|
||||
</div>
|
||||
<template v-if="['decimal', 'float'].includes(fieldData.type) === false">
|
||||
<div class="field half" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('length') }}</div>
|
||||
<v-input
|
||||
type="number"
|
||||
:placeholder="fieldData.type !== 'string' ? $t('not_available_for_type') : '255'"
|
||||
:disabled="isExisting || fieldData.type !== 'string'"
|
||||
v-model="fieldData.schema.max_length"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="field full" v-if="fieldData.schema">
|
||||
<template v-else>
|
||||
<div class="field half" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('precision_scale') }}</div>
|
||||
<div class="precision-scale">
|
||||
<v-input type="number" :placeholder="10" v-model="fieldData.schema.precision" />
|
||||
|
||||
<v-input type="number" :placeholder="5" v-model="fieldData.schema.scale" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="['uuid', 'date', 'time', 'datetime', 'timestamp'].includes(fieldData.type)">
|
||||
<div class="field half-left">
|
||||
<div class="label type-label">{{ $t('on_create') }}</div>
|
||||
<v-select :items="onCreateOptions" v-model="onCreateValue" />
|
||||
</div>
|
||||
|
||||
<div class="field half-right">
|
||||
<div class="label type-label">{{ $t('on_update') }}</div>
|
||||
<v-select :items="onUpdateOptions" v-model="onUpdateValue" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- @TODO see https://github.com/directus/next/issues/639
|
||||
|
||||
<div class="field half-left" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('unique') }}</div>
|
||||
<v-checkbox
|
||||
:label="$t('value_unique')"
|
||||
:input-value="fieldData.schema.is_unique === false"
|
||||
@change="fieldData.schema.is_unique = !$event"
|
||||
block
|
||||
/>
|
||||
</div> -->
|
||||
|
||||
<div class="field full" v-if="fieldData.schema && fieldData.schema.is_primary_key !== true">
|
||||
<div class="label type-label">{{ $t('default_value') }}</div>
|
||||
<v-input
|
||||
v-if="['string', 'uuid'].includes(fieldData.type)"
|
||||
@@ -82,29 +124,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="['uuid', 'date', 'time', 'datetime', 'timestamp'].includes(fieldData.type)">
|
||||
<div class="field">
|
||||
<div class="label type-label">{{ $t('on_create') }}</div>
|
||||
<v-select :items="onCreateOptions" v-model="onCreateValue" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="label type-label">{{ $t('on_update') }}</div>
|
||||
<v-select :items="onUpdateOptions" v-model="onUpdateValue" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="field" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('length') }}</div>
|
||||
<v-input
|
||||
type="number"
|
||||
:placeholder="fieldData.type !== 'string' ? $t('not_available_for_type') : '255'"
|
||||
:disabled="isExisting || fieldData.type !== 'string'"
|
||||
v-model="fieldData.schema.max_length"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="fieldData.schema">
|
||||
<div class="field half-left" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('required') }}</div>
|
||||
<v-checkbox
|
||||
:input-value="fieldData.schema.is_nullable === false"
|
||||
@@ -113,58 +133,6 @@
|
||||
block
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="fieldData.meta">
|
||||
<div class="label type-label">{{ $t('readonly') }}</div>
|
||||
<v-checkbox v-model="fieldData.meta.readonly" :label="$t('disabled_editing_value')" block />
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="fieldData.meta">
|
||||
<div class="label type-label">{{ $t('hidden') }}</div>
|
||||
<v-checkbox v-model="fieldData.meta.hidden" :label="$t('hidden_on_detail')" block />
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<div class="label type-label">{{ $t('translations') }}</div>
|
||||
<interface-repeater
|
||||
v-model="fieldData.meta.translations"
|
||||
:template="'{{ translation }} ({{ locale }})'"
|
||||
:fields="[
|
||||
{
|
||||
field: 'locale',
|
||||
type: 'string',
|
||||
name: $t('language'),
|
||||
meta: {
|
||||
interface: 'system-language',
|
||||
width: 'half',
|
||||
},
|
||||
schema: {
|
||||
default_value: 'en-US',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'translation',
|
||||
type: 'string',
|
||||
name: $t('translation'),
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'half',
|
||||
options: {
|
||||
placeholder: 'Enter a translation...',
|
||||
},
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@todo add unique when the API supports it
|
||||
|
||||
<div class="field">
|
||||
<div class="label type-label">{{ $t('unique') }}</div>
|
||||
<v-input v-model="fieldData.schema.unique" />
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -252,7 +220,7 @@ export default defineComponent({
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const typesWithLabels = computed(() => {
|
||||
return fieldTypes
|
||||
return fieldTypes;
|
||||
});
|
||||
|
||||
const typeDisabled = computed(() => {
|
||||
@@ -422,35 +390,17 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.type-title {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
grid-gap: 32px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
--v-form-vertical-gap: 32px;
|
||||
--v-form-horizontal-gap: 32px;
|
||||
|
||||
.field {
|
||||
grid-column: 1 / span 2;
|
||||
|
||||
@include breakpoint(small) {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.full {
|
||||
grid-column: 1 / span 2;
|
||||
|
||||
@include breakpoint(small) {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 8px;
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
@@ -460,4 +410,10 @@ export default defineComponent({
|
||||
.required {
|
||||
--v-icon-color: var(--primary);
|
||||
}
|
||||
|
||||
.precision-scale {
|
||||
display: grid;
|
||||
grid-gap: 12px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
:type="localType"
|
||||
/>
|
||||
|
||||
<setup-field
|
||||
v-if="currentTab[0] === 'field'"
|
||||
:is-existing="field !== '+'"
|
||||
:collection="collection"
|
||||
:type="localType"
|
||||
/>
|
||||
|
||||
<setup-relationship
|
||||
v-if="currentTab[0] === 'relationship'"
|
||||
:is-existing="field !== '+'"
|
||||
@@ -99,12 +106,13 @@ import { defineComponent, onMounted, ref, computed, reactive, PropType, watch, t
|
||||
import SetupTabs from './components/tabs.vue';
|
||||
import SetupActions from './components/actions.vue';
|
||||
import SetupSchema from './components/schema.vue';
|
||||
import SetupField from './components/field.vue';
|
||||
import SetupRelationship from './components/relationship.vue';
|
||||
import SetupTranslations from './components/translations.vue';
|
||||
import SetupInterface from './components/interface.vue';
|
||||
import SetupDisplay from './components/display.vue';
|
||||
import { i18n } from '@/lang';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, cloneDeep } from 'lodash';
|
||||
import api from '@/api';
|
||||
import { Relation, Collection } from '@/types';
|
||||
import { useFieldsStore, useRelationsStore, useCollectionsStore } from '@/stores/';
|
||||
@@ -121,6 +129,7 @@ export default defineComponent({
|
||||
SetupTabs,
|
||||
SetupActions,
|
||||
SetupSchema,
|
||||
SetupField,
|
||||
SetupRelationship,
|
||||
SetupTranslations,
|
||||
SetupInterface,
|
||||
@@ -198,6 +207,11 @@ export default defineComponent({
|
||||
value: 'schema',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
text: i18n.tc('field', 1),
|
||||
value: 'field',
|
||||
disabled: interfaceDisplayDisabled(),
|
||||
},
|
||||
{
|
||||
text: i18n.t('interface'),
|
||||
value: 'interface',
|
||||
@@ -285,11 +299,20 @@ export default defineComponent({
|
||||
async function saveField() {
|
||||
saving.value = true;
|
||||
|
||||
const fieldData = cloneDeep(state.fieldData);
|
||||
|
||||
// You can't alter PK columns in most database drivers. If this field is the PK, remove `schema` so we don't
|
||||
// accidentally try altering the column
|
||||
|
||||
if (fieldData.schema?.is_primary_key === true) {
|
||||
delete fieldData.schema;
|
||||
}
|
||||
|
||||
try {
|
||||
if (props.field !== '+') {
|
||||
await api.patch(`/fields/${props.collection}/${props.field}`, state.fieldData);
|
||||
await api.patch(`/fields/${props.collection}/${props.field}`, fieldData);
|
||||
} else {
|
||||
await api.post(`/fields/${props.collection}`, state.fieldData);
|
||||
await api.post(`/fields/${props.collection}`, fieldData);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
@@ -341,7 +364,7 @@ export default defineComponent({
|
||||
});
|
||||
} else {
|
||||
notify({
|
||||
title: i18n.t('field_create_success', { field: state.fieldData.field }),
|
||||
title: i18n.t('field_create_success', { field: fieldData.field }),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import { getInterfaces } from '@/interfaces';
|
||||
import { getDisplays } from '@/displays';
|
||||
import { InterfaceConfig } from '@/interfaces/types';
|
||||
import { DisplayConfig } from '@/displays/types';
|
||||
import { Field } from '@/types';
|
||||
import { Field, localTypes } from '@/types';
|
||||
import Vue from 'vue';
|
||||
|
||||
const fieldsStore = useFieldsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
@@ -24,11 +25,7 @@ let availableDisplays: ComputedRef<DisplayConfig[]>;
|
||||
|
||||
export { state, availableInterfaces, availableDisplays, initLocalStore, clearLocalStore };
|
||||
|
||||
function initLocalStore(
|
||||
collection: string,
|
||||
field: string,
|
||||
type: 'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation' | 'translations'
|
||||
) {
|
||||
function initLocalStore(collection: string, field: string, type: typeof localTypes[number]) {
|
||||
const interfaces = getInterfaces();
|
||||
const displays = getDisplays();
|
||||
|
||||
@@ -40,6 +37,8 @@ function initLocalStore(
|
||||
default_value: undefined,
|
||||
max_length: undefined,
|
||||
is_nullable: true,
|
||||
precision: null,
|
||||
scale: null,
|
||||
},
|
||||
meta: {
|
||||
hidden: false,
|
||||
@@ -90,8 +89,8 @@ function initLocalStore(
|
||||
availableDisplays = computed(() =>
|
||||
displays.value.filter((display) => {
|
||||
const matchesType = display.types.includes(state.fieldData?.type || 'alias');
|
||||
const matchesRelation = true;
|
||||
return matchesType && matchesRelation;
|
||||
let matchesLocalType = display.localTypes?.includes(type);
|
||||
return matchesType && (matchesLocalType === undefined || matchesLocalType);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -518,9 +517,9 @@ function initLocalStore(
|
||||
many_collection: '',
|
||||
many_field: '',
|
||||
many_primary: '',
|
||||
one_collection: type === 'files' ? 'directus_files' : '',
|
||||
one_collection: '',
|
||||
one_field: null,
|
||||
one_primary: type === 'files' ? 'id' : '',
|
||||
one_primary: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -592,6 +591,13 @@ function initLocalStore(
|
||||
}
|
||||
);
|
||||
|
||||
if (type === 'files') {
|
||||
Vue.nextTick(() => {
|
||||
state.relations[1].one_collection = 'directus_files';
|
||||
state.relations[1].one_primary = 'id';
|
||||
});
|
||||
}
|
||||
|
||||
if (type !== 'translations') {
|
||||
let stop: WatchStopHandle;
|
||||
|
||||
@@ -700,7 +706,7 @@ function initLocalStore(
|
||||
}
|
||||
|
||||
function fieldExists(collection: string, field: string) {
|
||||
return collectionExists(collection) && fieldsStore.getField(collection, field) !== null;
|
||||
return collectionExists(collection) && !!fieldsStore.getField(collection, field);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<div class="collections-detail">
|
||||
<div class="collections-item">
|
||||
<div class="fields">
|
||||
<h2 class="title type-label">
|
||||
{{ $t('fields_and_layout') }}
|
||||
@@ -173,7 +173,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.collections-detail {
|
||||
.collections-item {
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0;
|
||||
padding-bottom: var(--content-padding-bottom);
|
||||
|
||||
@@ -26,19 +26,25 @@
|
||||
<v-tabs-items v-model="currentTab">
|
||||
<v-tab-item value="collection">
|
||||
<h2 class="type-title">{{ $t('creating_collection_info') }}</h2>
|
||||
<div class="type-label">
|
||||
{{ $t('name') }}
|
||||
<v-icon class="required" v-tooltip="$t('required')" name="star" sup />
|
||||
</div>
|
||||
<v-input
|
||||
autofocus
|
||||
class="monospace"
|
||||
v-model="collectionName"
|
||||
db-safe
|
||||
:placeholder="$t('a_unique_table_name')"
|
||||
/>
|
||||
<v-divider />
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="type-label">
|
||||
{{ $t('name') }}
|
||||
<v-icon class="required" v-tooltip="$t('required')" name="star" sup />
|
||||
</div>
|
||||
<v-input
|
||||
autofocus
|
||||
class="monospace"
|
||||
v-model="collectionName"
|
||||
db-safe
|
||||
:placeholder="$t('a_unique_table_name')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="type-label">{{ $t('singleton') }}</div>
|
||||
<v-checkbox block :label="$t('singleton_label')" v-model="singleton" />
|
||||
</div>
|
||||
<v-divider class="full" />
|
||||
<div>
|
||||
<div class="type-label">{{ $t('primary_key_field') }}</div>
|
||||
<v-input
|
||||
@@ -73,9 +79,14 @@
|
||||
<v-tab-item value="system">
|
||||
<h2 class="type-title">{{ $t('creating_collection_system') }}</h2>
|
||||
<div class="grid system">
|
||||
<div class="field" v-for="(info, field) in systemFields" :key="field">
|
||||
<div v-for="(info, field) in systemFields" :key="field">
|
||||
<div class="type-label">{{ $t(info.label) }}</div>
|
||||
<v-input v-model="info.name" class="monospace" :class="{active: info.enabled}" @click.native="info.enabled = true">
|
||||
<v-input
|
||||
v-model="info.name"
|
||||
class="monospace"
|
||||
:class="{ active: info.enabled }"
|
||||
@click.native="info.enabled = true"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox v-model="info.enabled" />
|
||||
</template>
|
||||
@@ -124,6 +135,7 @@ export default defineComponent({
|
||||
const currentTab = ref(['collection']);
|
||||
|
||||
const collectionName = ref(null);
|
||||
const singleton = ref(false);
|
||||
const primaryKeyFieldName = ref('id');
|
||||
const primaryKeyFieldType = ref<'auto_int' | 'uuid' | 'manual'>('auto_int');
|
||||
|
||||
@@ -184,6 +196,7 @@ export default defineComponent({
|
||||
collectionName,
|
||||
saveError,
|
||||
saving,
|
||||
singleton,
|
||||
};
|
||||
|
||||
async function save() {
|
||||
@@ -198,6 +211,7 @@ export default defineComponent({
|
||||
archive_field: archiveField.value,
|
||||
archive_value: archiveValue.value,
|
||||
unarchive_value: unarchiveValue.value,
|
||||
singleton: singleton.value,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -412,22 +426,14 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.type-title {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.type-label {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.v-divider {
|
||||
margin: 48px 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-gap: 48px 36px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
.system {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<div class="presets-browse">
|
||||
<div class="presets-collection">
|
||||
<v-info
|
||||
center
|
||||
type="warning"
|
||||
@@ -302,7 +302,7 @@ export default defineComponent({
|
||||
--v-button-color-hover: var(--danger);
|
||||
}
|
||||
|
||||
.presets-browse {
|
||||
.presets-collection {
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0;
|
||||
}
|
||||
@@ -13,8 +13,7 @@
|
||||
|
||||
<v-divider />
|
||||
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_presets_browse'))" />
|
||||
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_presets_collection'))" />
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<div class="preset-detail">
|
||||
<div class="preset-item">
|
||||
<v-form
|
||||
:fields="fields"
|
||||
:loading="loading"
|
||||
@@ -77,12 +77,16 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_presets_detail'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_presets_item'))" />
|
||||
</drawer-detail>
|
||||
|
||||
<div class="layout-drawer">
|
||||
<portal-target name="drawer" />
|
||||
</div>
|
||||
<portal-target class="layout-drawer" name="drawer" />
|
||||
|
||||
<drawer-detail class="layout-drawer" icon="layers" :title="$t('layout_options')">
|
||||
<div class="layout-options">
|
||||
<portal-target name="layout-options" class="portal-contents" />
|
||||
</div>
|
||||
</drawer-detail>
|
||||
</template>
|
||||
</private-view>
|
||||
</template>
|
||||
@@ -191,6 +195,9 @@ export default defineComponent({
|
||||
editsParsed.role = edits.value.scope.substring(5);
|
||||
} else if (edits.value.scope.startsWith('user_')) {
|
||||
editsParsed.user = edits.value.scope.substring(5);
|
||||
} else {
|
||||
editsParsed.role = null;
|
||||
editsParsed.user = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +431,7 @@ export default defineComponent({
|
||||
return options;
|
||||
});
|
||||
|
||||
const systemCollectionWhiteList = ['directus_users', 'directus_files'];
|
||||
const systemCollectionWhiteList = ['directus_users', 'directus_files', 'directus_activity'];
|
||||
|
||||
const fields = computed(() => [
|
||||
{
|
||||
@@ -510,6 +517,8 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.header-icon {
|
||||
--v-button-background-color: var(--warning-25);
|
||||
--v-button-color: var(--warning);
|
||||
@@ -524,7 +533,7 @@ export default defineComponent({
|
||||
--v-button-color-hover: var(--danger);
|
||||
}
|
||||
|
||||
.preset-detail {
|
||||
.preset-item {
|
||||
padding: var(--content-padding);
|
||||
padding-bottom: var(--content-padding-bottom);
|
||||
}
|
||||
@@ -542,6 +551,21 @@ export default defineComponent({
|
||||
--drawer-detail-icon-color: var(--warning);
|
||||
--drawer-detail-color: var(--warning);
|
||||
--drawer-detail-color-active: var(--warning);
|
||||
--v-form-vertical-gap: 24px;
|
||||
}
|
||||
|
||||
.portal-contents {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.layout-options ::v-deep {
|
||||
--v-form-vertical-gap: 24px;
|
||||
|
||||
.type-label {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
.subdued {
|
||||
@@ -45,66 +45,13 @@ import { version } from '../../../../../../package.json';
|
||||
import bytes from 'bytes';
|
||||
import prettyMS from 'pretty-ms';
|
||||
import api from '@/api';
|
||||
|
||||
type ServerInfo = {
|
||||
directus: {
|
||||
version: string;
|
||||
};
|
||||
node: {
|
||||
version: string;
|
||||
uptime: number;
|
||||
};
|
||||
os: {
|
||||
type: string;
|
||||
version: string;
|
||||
uptime: number;
|
||||
totalmem: number;
|
||||
};
|
||||
};
|
||||
import { useProjectInfo } from '../../../composables/use-project-info';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const info = ref<ServerInfo>();
|
||||
const loading = ref(false);
|
||||
const error = ref<any>();
|
||||
const { parsedInfo } = useProjectInfo();
|
||||
|
||||
const parsedInfo = computed(() => {
|
||||
if (!info.value) return null;
|
||||
|
||||
return {
|
||||
directus: {
|
||||
version: info.value.directus.version,
|
||||
},
|
||||
node: {
|
||||
version: info.value.node.version,
|
||||
uptime: prettyMS(info.value.node.uptime * 1000),
|
||||
},
|
||||
os: {
|
||||
type: info.value.os.type,
|
||||
version: info.value.os.version,
|
||||
uptime: prettyMS(info.value.os.uptime * 1000),
|
||||
totalmem: bytes(info.value.os.totalmem),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
fetchInfo();
|
||||
|
||||
return { parsedInfo, loading, error, marked };
|
||||
|
||||
async function fetchInfo() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await api.get('/server/info');
|
||||
info.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
return { parsedInfo, marked };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_roles_browse'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_roles_collection'))" />
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
@@ -75,7 +75,7 @@ type Role = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'roles-browse',
|
||||
name: 'roles-collection',
|
||||
components: { SettingsNavigation, ValueNull },
|
||||
props: {},
|
||||
setup() {
|
||||
@@ -102,6 +102,14 @@ export default defineComponent({
|
||||
async function setFullAccess() {
|
||||
saving.value = true;
|
||||
|
||||
// If this collection isn't "managed" yet, make sure to add it to directus_collections first
|
||||
// before trying to associate any permissions with it
|
||||
if (props.collection.meta === null) {
|
||||
await api.patch(`/collections/${props.collection.collection}`, {
|
||||
meta: {},
|
||||
});
|
||||
}
|
||||
|
||||
if (props.permission) {
|
||||
try {
|
||||
await api.patch(`/permissions/${props.permission.id}`, {
|
||||
@@ -148,6 +156,14 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function openPermissions() {
|
||||
// If this collection isn't "managed" yet, make sure to add it to directus_collections first
|
||||
// before trying to associate any permissions with it
|
||||
if (props.collection.meta === null) {
|
||||
await api.patch(`/collections/${props.collection.collection}`, {
|
||||
meta: {},
|
||||
});
|
||||
}
|
||||
|
||||
if (props.permission) {
|
||||
router.push(`/settings/roles/${props.role || 'public'}/${props.permission.id}`);
|
||||
} else {
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<v-divider />
|
||||
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_roles_detail'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_roles_item'))" />
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
@@ -90,7 +90,7 @@ type Values = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'roles-detail',
|
||||
name: 'roles-item',
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoDrawerDetail, PermissionsOverview },
|
||||
props: {
|
||||
primaryKey: {
|
||||
@@ -22,10 +22,10 @@ import { defineComponent, computed, toRefs, ref } from '@vue/composition-api';
|
||||
|
||||
import SettingsNavigation from '../../components/navigation.vue';
|
||||
import router from '@/router';
|
||||
import PermissionsOverview from './detail/components/permissions-overview.vue';
|
||||
import PermissionsOverview from './item/components/permissions-overview.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'roles-detail',
|
||||
name: 'roles-item',
|
||||
components: { SettingsNavigation, PermissionsOverview },
|
||||
props: {
|
||||
permissionKey: {
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_webhooks_browse'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_webhooks_collection'))" />
|
||||
</drawer-detail>
|
||||
<layout-drawer-detail />
|
||||
<portal-target name="drawer" />
|
||||
@@ -111,7 +111,7 @@ type Item = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'webhooks-browse',
|
||||
name: 'webhooks-collection',
|
||||
components: { SettingsNavigation, LayoutDrawerDetail, SearchInput },
|
||||
setup(props) {
|
||||
const layoutRef = ref<LayoutComponent | null>(null);
|
||||
@@ -179,7 +179,7 @@ export default defineComponent({
|
||||
filters.value = [];
|
||||
searchQuery.value = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_webhooks_detail'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_settings_webhooks_item'))" />
|
||||
</drawer-detail>
|
||||
<revisions-drawer-detail v-if="isNew === false" collection="directus_webhooks" :primary-key="primaryKey" />
|
||||
</template>
|
||||
@@ -83,7 +83,7 @@ type Values = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'webhooks-detail',
|
||||
name: 'webhooks-item',
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, SaveOptions },
|
||||
props: {
|
||||
primaryKey: {
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<v-divider />
|
||||
|
||||
<div class="page-description" v-html="marked($t('page_help_users_detail'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_users_item'))" />
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineModule } from '@/modules/define';
|
||||
|
||||
import UsersBrowse from './routes/browse.vue';
|
||||
import UsersDetail from './routes/detail.vue';
|
||||
import Collection from './routes/collection.vue';
|
||||
import Item from './routes/item.vue';
|
||||
|
||||
export default defineModule(({ i18n }) => ({
|
||||
id: 'users',
|
||||
@@ -9,17 +9,17 @@ export default defineModule(({ i18n }) => ({
|
||||
icon: 'people_alt',
|
||||
routes: [
|
||||
{
|
||||
name: 'users-browse-all',
|
||||
name: 'users-collection',
|
||||
path: '/',
|
||||
component: UsersBrowse,
|
||||
component: Collection,
|
||||
props: (route) => ({
|
||||
queryFilters: route.query,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'users-detail',
|
||||
name: 'users-item',
|
||||
path: '/:primaryKey',
|
||||
component: UsersDetail,
|
||||
component: Item,
|
||||
props: (route) => ({
|
||||
primaryKey: route.params.primaryKey,
|
||||
preset: route.query,
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
<div class="page-description" v-html="marked($t('page_help_users_browse'))" />
|
||||
<div class="page-description" v-html="marked($t('page_help_users_collection'))" />
|
||||
</drawer-detail>
|
||||
<layout-drawer-detail @input="layout = $event" :value="layout" />
|
||||
<portal-target name="drawer" />
|
||||
@@ -119,7 +119,7 @@ type Item = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'users-browse',
|
||||
name: 'users-collection',
|
||||
components: { UsersNavigation, LayoutDrawerDetail, SearchInput },
|
||||
props: {
|
||||
queryFilters: {
|
||||
@@ -53,7 +53,7 @@
|
||||
v-tooltip.bottom="archiveTooltip"
|
||||
@click="on"
|
||||
:disabled="item === null || archiveAllowed !== true"
|
||||
v-if="collectionInfo.meta.singleton === false"
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton === false"
|
||||
>
|
||||
<v-icon :name="isArchived ? 'unarchive' : 'archive'" outline />
|
||||
</v-button>
|
||||
@@ -98,7 +98,7 @@
|
||||
<users-navigation :current-role="(item && item.role) || (preset && preset.role)" />
|
||||
</template>
|
||||
|
||||
<div class="user-detail">
|
||||
<div class="user-item">
|
||||
<div class="user-box" v-if="isNew === false">
|
||||
<div class="avatar">
|
||||
<v-skeleton-loader v-if="loading || previewLoading" />
|
||||
@@ -185,7 +185,7 @@ type Values = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'users-detail',
|
||||
name: 'users-item',
|
||||
beforeRouteLeave(to, from, next) {
|
||||
const self = this as any;
|
||||
const hasEdits = Object.keys(self.edits).length > 0;
|
||||
@@ -483,7 +483,7 @@ export default defineComponent({
|
||||
--v-button-background-color: var(--background-normal);
|
||||
}
|
||||
|
||||
.user-detail {
|
||||
.user-item {
|
||||
padding: var(--content-padding);
|
||||
padding-bottom: var(--content-padding-bottom);
|
||||
}
|
||||
Reference in New Issue
Block a user