Merge branch 'main' into esc-to-close

This commit is contained in:
rijkvanzanten
2020-10-15 18:38:39 -04:00
362 changed files with 31221 additions and 5484 deletions

View File

@@ -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,
},
},
],

View File

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

View File

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

View File

@@ -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,
},
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
},
],

View File

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

View File

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

View File

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

View 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,7 +90,7 @@ type Values = {
};
export default defineComponent({
name: 'roles-detail',
name: 'roles-item',
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoDrawerDetail, PermissionsOverview },
props: {
primaryKey: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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