Merge branch 'main' into relational-sort

This commit is contained in:
Nitwel
2020-10-19 18:16:08 +02:00
174 changed files with 1497 additions and 1456 deletions

2
api/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-beta.13",
"version": "9.0.0-rc.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-beta.13",
"version": "9.0.0-rc.0",
"license": "GPL-3.0-only",
"homepage": "https://github.com/directus/next#readme",
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",

View File

@@ -1,9 +1,9 @@
export const drivers = {
sqlite3: 'SQLite',
mysql: 'MySQL / MariaDB / Aurora',
pg: 'PostgreSQL / Redshift',
oracledb: 'Oracle Database',
mssql: 'Microsoft SQL Server',
mysql: 'MySQL / MariaDB / Aurora',
sqlite3: 'SQLite (Beta)',
oracledb: 'Oracle Database (Alpha)',
mssql: 'Microsoft SQL Server (Alpha)',
};
export function getDriverForClient(client: string): keyof typeof drivers | null {

View File

@@ -215,7 +215,7 @@ router.get(
'/oauth',
asyncHandler(async (req, res, next) => {
const providers = toArray(env.OAUTH_PROVIDERS);
res.locals.payload = { data: providers.length > 0 ? providers : null };
res.locals.payload = { data: env.OAUTH_PROVIDERS ? providers : null };
return next();
}),
respond

View File

@@ -49,7 +49,7 @@ router.get(
router.get(
'/me',
asyncHandler(async (req, res, next) => {
if (!req.accountability?.user || !req.accountability?.role) {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}

View File

@@ -58,11 +58,21 @@ router.get(
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
const service = new UsersService({ accountability: req.accountability });
const item = await service.readByKey(req.accountability.user, req.sanitizedQuery);
try {
const item = await service.readByKey(req.accountability.user, req.sanitizedQuery);
res.locals.payload = { data: item || null };
} catch (error) {
if (error instanceof ForbiddenException) {
res.locals.payload = { data: { id: req.accountability.user } };
return next();
}
throw error;
}
res.locals.payload = { data: item || null };
return next();
}),
respond

View File

@@ -101,7 +101,7 @@ fields:
display: user
- collection: directus_files
field: modified_on
interface: dateTime
interface: datetime
locked: true
special: date-updated
width: half
@@ -111,4 +111,4 @@ fields:
display: datetime
- collection: directus_files
field: created_by
display: user
display: user

View File

@@ -1,12 +1,16 @@
import { Filter, Accountability } from '../types';
import { deepMap } from './deep-map';
import { toArray } from '../utils/to-array';
export function parseFilter(filter: Filter, accountability: Accountability | null) {
return deepMap(filter, (val: any, key: string) => {
if (val === 'true') return true;
if (val === 'false') return false;
if (key === '_in' || key === '_nin') return val.split(',').filter((val: any) => val);
if (key === '_in' || key === '_nin') {
if (typeof val === 'string' && val.includes(',')) return val.split(',');
else return toArray(val);
}
if (val === '$NOW') return new Date();
if (val === '$CURRENT_USER') return accountability?.user || null;

View File

@@ -1,6 +1,7 @@
import { Accountability, Query, Sort, Filter, Meta } from '../types';
import logger from '../logger';
import { parseFilter } from '../utils/parse-filter';
import { flatten } from 'lodash';
export function sanitizeQuery(
rawQuery: Record<string, any>,
@@ -75,6 +76,9 @@ function sanitizeFields(rawFields: any) {
if (typeof rawFields === 'string') fields = rawFields.split(',');
else if (Array.isArray(rawFields)) fields = rawFields as string[];
// Case where array item includes CSV (fe fields[]=id,name):
fields = flatten(fields.map((field) => (field.includes(',') ? field.split(',') : field)));
return fields;
}

View File

@@ -15,7 +15,7 @@ const querySchema = Joi.object({
offset: Joi.number(),
page: Joi.number(),
single: Joi.boolean(),
meta: Joi.array().items(Joi.string().valid('total_count', 'result_count')),
meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')),
search: Joi.string(),
export: Joi.string().valid('json', 'csv'),
deep: Joi.link('#query'),

164
app/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@directus/app",
"version": "9.0.0-beta.13",
"version": "9.0.0-rc.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -6601,51 +6601,6 @@
"tslint": "^5.20.1",
"webpack": "^4.0.0",
"yorkie": "^2.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"fork-ts-checker-webpack-plugin-v5": {
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
"dev": true,
"optional": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@types/json-schema": "^7.0.5",
"chalk": "^4.1.0",
"cosmiconfig": "^6.0.0",
"deepmerge": "^4.2.2",
"fs-extra": "^9.0.0",
"memfs": "^3.1.2",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
}
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"dev": true,
"optional": true,
"requires": {
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
}
}
}
},
"@vue/cli-plugin-unit-jest": {
@@ -6785,17 +6740,6 @@
"unique-filename": "^1.1.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -6879,18 +6823,6 @@
"graceful-fs": "^4.1.6"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -7004,18 +6936,6 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.8",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -11744,6 +11664,51 @@
}
}
},
"fork-ts-checker-webpack-plugin-v5": {
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
"dev": true,
"optional": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@types/json-schema": "^7.0.5",
"chalk": "^4.1.0",
"cosmiconfig": "^6.0.0",
"deepmerge": "^4.2.2",
"fs-extra": "^9.0.0",
"memfs": "^3.1.2",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"dev": true,
"optional": true,
"requires": {
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
}
}
}
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@@ -20377,6 +20342,43 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.8",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
}
}
},
"vue-router": {
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.6.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/app",
"version": "9.0.0-beta.13",
"version": "9.0.0-rc.0",
"private": false,
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
"author": "Rijk van Zanten <rijk@rngr.org>",

View File

@@ -14,15 +14,7 @@
</template>
</v-info>
<router-view v-else-if="!hydrating && appAccess" />
<v-info v-else-if="appAccess === false" center :title="$t('no_app_access')" type="danger" icon="block">
{{ $t('no_app_access_copy') }}
<template #append>
<v-button to="/logout">Switch User</v-button>
</template>
</v-info>
<router-view v-else-if="!hydrating" />
<portal-target name="dialog-outlet" transition="transition-dialog" multiple />
<portal-target name="menu-outlet" transition="transition-bounce" multiple />
@@ -48,7 +40,7 @@ export default defineComponent({
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const { hydrating, drawerOpen } = toRefs(appStore.state);
const { hydrating, sidebarOpen } = toRefs(appStore.state);
const brandStyle = computed(() => {
return {
@@ -73,9 +65,9 @@ export default defineComponent({
if (newWidth === oldWidth) return;
if (newWidth >= 1424) {
if (drawerOpen.value === false) drawerOpen.value = true;
if (sidebarOpen.value === false) sidebarOpen.value = true;
} else {
if (drawerOpen.value === true) drawerOpen.value = false;
if (sidebarOpen.value === true) sidebarOpen.value = false;
}
},
{ immediate: true }
@@ -88,7 +80,7 @@ export default defineComponent({
document.body.classList.remove('light');
document.body.classList.remove('auto');
if (newUser !== undefined && newUser !== null) {
if (newUser !== undefined && newUser !== null && newUser.theme) {
document.body.classList.add(newUser.theme);
} else {
// Default to light mode
@@ -108,11 +100,6 @@ export default defineComponent({
return settingsStore.state?.settings?.custom_css || '';
});
const appAccess = computed(() => {
if (!userStore.state.currentUser) return true;
return userStore.state.currentUser?.role?.app_access || false;
});
const error = computed(() => appStore.state.error);
/**
@@ -124,7 +111,7 @@ export default defineComponent({
axios,
});
return { hydrating, brandStyle, appAccess, error, customCSS };
return { hydrating, brandStyle, error, customCSS };
},
});
</script>

View File

@@ -22,8 +22,7 @@ import VInput from './v-input/';
import VItemGroup, { VItem } from './v-item-group';
import VList, { VListGroup, VListItem, VListItemContent, VListItemHint, VListItemIcon, VListItemText } from './v-list/';
import VMenu from './v-menu/';
import VModal from './v-modal/';
import VModalHeading from './v-modal/v-modal-heading.vue';
import VDrawer from './v-drawer/';
import VNotice from './v-notice/';
import VOverlay from './v-overlay/';
import VPagination from './v-pagination/';
@@ -73,8 +72,7 @@ Vue.component('v-list-item-text', VListItemText);
Vue.component('v-list-item', VListItem);
Vue.component('v-list', VList);
Vue.component('v-menu', VMenu);
Vue.component('v-modal', VModal);
Vue.component('v-modal-heading', VModalHeading);
Vue.component('v-drawer', VDrawer);
Vue.component('v-notice', VNotice);
Vue.component('v-overlay', VOverlay);
Vue.component('v-pagination', VPagination);
@@ -104,14 +102,14 @@ Vue.component('transition-expand', TransitionExpand);
import RenderDisplay from '@/views/private/components/render-display';
import RenderTemplate from '@/views/private/components/render-template';
import DrawerDetail from '@/views/private/components/drawer-detail/';
import FilterDrawerDetail from '@/views/private/components/filter-drawer-detail';
import SidebarDetail from '@/views/private/components/sidebar-detail/';
import FilterSidebarDetail from '@/views/private/components/filter-sidebar-detail';
import UserPopover from '@/views/private/components/user-popover';
import ValueNull from '@/views/private/components/value-null';
Vue.component('render-display', RenderDisplay);
Vue.component('render-template', RenderTemplate);
Vue.component('filter-drawer-detail', FilterDrawerDetail);
Vue.component('drawer-detail', DrawerDetail);
Vue.component('filter-sidebar-detail', FilterSidebarDetail);
Vue.component('sidebar-detail', SidebarDetail);
Vue.component('user-popover', UserPopover);
Vue.component('value-null', ValueNull);

View File

@@ -5,24 +5,34 @@
</template>
<style lang="scss">
/** @NOTE this is not scoped on purpose. The children are outsisde of the tree (portal) */
/** @NOTE this is not scoped on purpose. The children are outside of the tree (portal) */
.dialog-enter-active,
.dialog-leave-active {
transition: opacity var(--slow) var(--transition);
& > *:not(.v-overlay) {
&.center > *:not(.v-overlay) {
transform: translateY(0px);
transition: transform var(--slow) var(--transition-in);
}
&.right > *:not(.v-overlay) {
transform: translateX(0px);
transition: transform var(--slow) var(--transition-in);
}
}
.dialog-enter,
.dialog-leave-to {
opacity: 0;
& > *:not(.v-overlay) {
&.center > *:not(.v-overlay) {
transform: translateY(50px);
transition: transform var(--slow) var(--transition-out);
}
&.right > *:not(.v-overlay) {
transform: translateX(50px);
transition: transform var(--slow) var(--transition-out);
}
}
</style>

View File

@@ -3,7 +3,7 @@
<slot name="activator" v-bind="{ on: () => (_active = true) }" />
<portal to="dialog-outlet">
<div v-if="_active" class="container" :class="[className]" :key="id">
<div v-if="_active" class="container" :class="[className, placement]" :key="id">
<v-overlay active absolute @click="emitToggle" />
<slot />
</div>
@@ -30,6 +30,11 @@ export default defineComponent({
type: Boolean,
default: false,
},
placement: {
type: String,
default: 'center',
validator: (val: string) => ['center', 'right'].includes(val),
},
},
setup(props, { emit }) {
const dialog = ref<HTMLElement | null>(null);
@@ -92,11 +97,32 @@ export default defineComponent({
left: 0;
z-index: 500;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
transition: opacity var(--medium) var(--transition);
::v-deep > * {
z-index: 2;
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
}
&.center {
align-items: center;
justify-content: center;
&.nudge > ::v-deep *:not(:first-child) {
animation: nudge 200ms;
}
}
&.right {
align-items: center;
justify-content: flex-end;
&.nudge > ::v-deep *:not(:first-child) {
transform-origin: right;
animation: shake 200ms;
}
}
::v-deep .v-card {
--v-card-min-width: 540px;
@@ -112,15 +138,6 @@ export default defineComponent({
.v-overlay {
--v-overlay-z-index: 1;
}
&.nudge {
animation: nudge 200ms;
}
::v-deep > * {
z-index: 2;
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
}
}
@keyframes nudge {
@@ -136,4 +153,18 @@ export default defineComponent({
transform: scale(1);
}
}
@keyframes shake {
0% {
transform: scaleX(1);
}
50% {
transform: scaleX(0.98);
}
100% {
transform: scaleX(1);
}
}
</style>

View File

@@ -0,0 +1,4 @@
import VDrawer from './v-drawer.vue';
export { VDrawer };
export default VDrawer;

View File

@@ -1,20 +1,23 @@
<template>
<v-dialog v-model="_active" @esc="$emit('esc')" :persistent="persistent">
<v-dialog v-model="_active" @esc="$emit('cancel')" :persistent="persistent" placement="right">
<template #activator="{ on }">
<slot name="activator" v-bind="{ on }" />
</template>
<article class="v-modal" :class="{ 'form-width': formWidth }">
<header class="header">
<v-icon class="menu-toggle" name="menu" @click="sidebarActive = !sidebarActive" />
<h2 class="title">{{ title }}</h2>
<slot name="subtitle">
<p v-if="subtitle" class="subtitle">{{ subtitle }}</p>
</slot>
<div class="spacer" />
<slot name="header:append" />
</header>
<div class="content" :class="{ 'no-padding': noPadding }">
<article class="v-drawer">
<v-button
v-if="showCancel"
class="cancel"
@click="$emit('cancel')"
icon
rounded
secondary
v-tooltip.bottom="$t('cancel')"
>
<v-icon name="close" />
</v-button>
<div class="content">
<v-overlay v-if="$slots.sidebar" absolute :active="sidebarActive" @click="sidebarActive = false" />
<nav
v-if="$slots.sidebar"
@@ -25,20 +28,40 @@
<slot name="sidebar" />
</nav>
<main ref="mainEl" class="main">
<header-bar :title="title">
<template #headline>
<slot name="subtitle">
<p v-if="subtitle" class="subtitle">{{ subtitle }}</p>
</slot>
</template>
<template #title-outer:prepend>
<v-button class="header-icon" rounded icon secondary disabled>
<v-icon :name="icon" />
</v-button>
</template>
<template #actions:prepend><slot name="actions:prepend" /></template>
<template #actions><slot name="actions" /></template>
<template #title:append><slot name="header:append" /></template>
</header-bar>
<slot />
</main>
</div>
<footer class="footer" v-if="$slots.footer || $scopedSlots.footer">
<slot name="footer" v-bind="{ close: () => (_active = false) }" />
</footer>
</article>
</v-dialog>
</template>
<script lang="ts">
import { defineComponent, ref, computed, provide } from '@vue/composition-api';
import HeaderBar from '@/views/private/components/header-bar/header-bar.vue';
export default defineComponent({
components: {
HeaderBar,
},
model: {
prop: 'active',
event: 'toggle',
@@ -60,18 +83,12 @@ export default defineComponent({
type: Boolean,
default: false,
},
noPadding: {
type: Boolean,
default: false,
},
formWidth: {
// If the modal is used to just render a form, it needs to be a little smaller to
// allow the form to be rendered in it's correct full size
type: Boolean,
default: false,
icon: {
type: String,
default: 'box',
},
},
setup(props, { emit }) {
setup(props, { emit, listeners }) {
const sidebarActive = ref(false);
const localActive = ref(false);
@@ -89,63 +106,47 @@ export default defineComponent({
},
});
return { sidebarActive, _active, mainEl };
const showCancel = computed(() => {
return listeners.hasOwnProperty('cancel');
});
return { sidebarActive, _active, mainEl, showCancel };
},
});
</script>
<style>
body {
--v-modal-max-width: 916px;
--v-drawer-max-width: 856px;
}
</style>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-modal {
.v-drawer {
position: relative;
display: flex;
flex-direction: column;
width: calc(100% - 16px);
max-width: var(--v-modal-max-width);
height: calc(100% - 16px);
max-height: 800px;
max-width: var(--v-drawer-max-width);
height: 100%;
background-color: var(--background-page);
border-radius: 4px;
.cancel {
position: absolute;
top: 32px;
left: -76px;
}
.spacer {
flex-grow: 1;
}
.header {
display: flex;
flex-shrink: 0;
align-items: center;
height: 60px;
padding: 0 16px;
border-bottom: 2px solid var(--background-normal);
.title {
margin-right: 12px;
font-size: 16px;
}
.subtitle {
color: var(--foreground-subdued);
font-size: 16px;
}
.menu-toggle {
margin-right: 8px;
@include breakpoint(medium) {
display: none;
}
}
@include breakpoint(medium) {
padding: 0 24px;
}
.header-icon {
--v-button-background-color: var(--background-normal);
--v-button-background-color-activated: var(--background-normal);
--v-button-background-color-hover: var(--background-normal-alt);
--v-button-color-disabled: var(--foreground-normal);
}
.content {
@@ -190,45 +191,20 @@ body {
}
.main {
--content-padding: 16px;
--content-padding-bottom: 32px;
flex-grow: 1;
padding: 16px 16px 32px;
overflow: auto;
@include breakpoint(medium) {
padding: 32px;
--content-padding: 32px;
}
}
&.no-padding .main {
padding: 0px;
}
}
.footer {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: flex-end;
height: 60px;
padding: 0 16px;
border-top: 2px solid var(--background-normal);
::v-deep > *:not(:last-child) {
margin-right: 8px;
}
@include breakpoint(medium) {
padding: 0 24px;
}
}
@include breakpoint(medium) {
width: calc(100% - 64px);
height: calc(100% - 64px);
}
}
.form-width {
--v-modal-max-width: 856px;
}
</style>

View File

@@ -139,10 +139,10 @@ export default defineComponent({
const gridClass = computed<string | null>(() => {
if (el.value === null) return null;
if (width.value > 588 && width.value <= 792) {
return 'grid';
} else {
if (width.value > 792) {
return 'grid with-fill';
} else {
return 'grid';
}
return null;

View File

@@ -161,7 +161,7 @@ export default defineComponent({
}
}
if (props.slug === true) {
if (props.dbSafe === true) {
const dbSafeCharacters = 'abcdefghijklmnopqrstuvwxyz01234567890-_~ '.split('');
const isAllowed = dbSafeCharacters.includes(key) || systemKeys.includes(key);

View File

@@ -1,4 +0,0 @@
import VModal from './v-modal.vue';
export { VModal };
export default VModal;

View File

@@ -1,73 +0,0 @@
# Modal
A modal is basically an elaborate pre-configured dialog. It supports an optional left sidebar that allows for easier tab usage.
## Usage
```html
<v-modal title="My Modal" v-modal="active">
Hello, world!
</v-modal>
```
```html
<v-modal title="My Modal">
<template #activator="{ on }">
<v-button @click="on">Open modal</v-button>
</template>
Hello, world!
</v-modal>
```
```html
<v-modal title="My Modal" v-model="active">
<template #activator="{ on }">
<v-button @click="on">Open modal</v-button>
</template>
<template #sidebar>
<v-tabs vertical>
<v-tab>Hello</v-tab>
<v-tab>Page 2</v-tab>
<v-tab>Page 3</v-tab>
</v-tabs>
</template>
<v-tabs-items>
<v-tab-item>Hello, world!</v-tab-item>
<v-tab-item>I'm page 2!</v-tab-item>
<v-tab-item>I'm page 3!</v-tab-item>
</v-tabs-items>
<template #footer="{ close }">
<v-button @click="close">Close modal</v-button>
</template>
</v-modal>
```
## Props
| Prop | Description | Default |
|--------------|-----------------------------------------------------------------|---------|
| `title`* | Title for the modal | |
| `subtitle` | Optional subtitle for the modal | |
| `active` | If the modal is active. Used in `v-model` | `false` |
| `persistent` | Prevent the user from exiting the modal by clicking the overlay | `false` |
## Events
| Event | Description | Value |
|----------|--------------------------|-----------|
| `toggle` | Sync the `v-model` value | `boolean` |
## Slots
| Slot | Description | Data |
|-------------|--------------------------------------------------------|-------------------------|
| _default_ | Modal content | |
| `activator` | Element to enable the modal | `{ on: () => void }` |
| `sidebar` | Sidebar content for the modal. Often used for `v-tabs` | |
| `footer` | Footer content. Often used for action buttons | `{ close: () => void }` |
## CSS Variables
n/a

View File

@@ -1,34 +0,0 @@
<template>
<div class="v-modal-heading">
<div class="type-title">{{ heading }}</div>
<div v-if="subheading" class="subheading">{{ subheading }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
export default defineComponent({
props: {
heading: {
type: String,
required: true,
},
subheading: {
type: String,
default: null,
},
},
});
</script>
<style lang="scss" scoped>
.v-modal-heading {
margin-bottom: 48px;
.subheading {
margin-top: 4px;
color: var(--foreground-subdued);
}
}
</style>

View File

@@ -1,84 +0,0 @@
import withPadding from '../../../.storybook/decorators/with-padding';
import readme from './readme.md';
import { defineComponent, ref } from '@vue/composition-api';
export default {
title: 'Components / Modal',
parameters: {
notes: readme,
},
decorators: [withPadding],
};
export const basic = () =>
defineComponent({
setup() {
const active = ref(false);
return { active };
},
template: `
<div>
<v-modal
v-model="active"
title="Creating New Collection"
subtitle="called Customers"
>
<template #activator="{ on }">
<v-button @click="on">Enable modal</v-button>
</template>
<p>Hello world!</p>
<template #footer="{ close }">
<v-button @click="close">Close modal</v-button>
</template>
</v-modal>
<portal-target name="outlet" />
</div>
`,
});
export const withNav = () =>
defineComponent({
setup() {
const active = ref(false);
const current = ref(['hello']);
return { active, current };
},
template: `
<div>
<v-modal
v-model="active"
title="Creating New Collection"
subtitle="called Customers"
>
<template #activator="{ on }">
<v-button @click="on">Enable modal</v-button>
</template>
<template #sidebar>
<v-tabs v-model="current" vertical>
<v-tab value="hello">Hello</v-tab>
<v-tab value="introduce">Modal</v-tab>
</v-tabs>
</template>
<v-tabs-items v-model="current">
<v-tab-item value="hello">
<p>Hello world!</p>
</v-tab-item>
<v-tab-item value="introduce">
<p>I'm a modal with tabs</p>
</v-tab-item>
</v-tabs-items>
<template #footer="{ close }">
<v-button @click="close">Close modal</v-button>
</template>
</v-modal>
<portal-target name="outlet" />
</div>
`,
});

View File

@@ -1,24 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VModal from './v-modal.vue';
import VDialog from '@/components/v-dialog/';
import VIcon from '@/components/v-icon/';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-dialog', VDialog);
localVue.component('v-icon', VIcon);
describe('Components / Modal', () => {
it('Renders', () => {
const component = shallowMount(VModal, {
localVue,
propsData: {
title: 'My Modal',
},
});
expect(component.isVueInstance()).toBe(true);
});
});

View File

@@ -52,7 +52,7 @@
</v-list>
</v-menu>
<modal-collection
<drawer-collection
collection="directus_files"
:active="activeDialog === 'choose'"
@update:active="activeDialog = null"
@@ -88,12 +88,12 @@
<script lang="ts">
import { defineComponent, ref, computed, watch } from '@vue/composition-api';
import uploadFiles from '@/utils/upload-files';
import ModalCollection from '@/views/private/components/modal-collection';
import DrawerCollection from '@/views/private/components/drawer-collection';
import api from '@/api';
import useItem from '@/composables/use-item';
export default defineComponent({
components: { ModalCollection },
components: { DrawerCollection },
props: {
multiple: {
type: Boolean,

View File

@@ -57,11 +57,11 @@ export async function hydrate(stores = useStores()) {
*/
await userStore.hydrate();
setLanguage((userStore.state.currentUser?.language as Language) || 'en-US');
await Promise.all(stores.filter(({ id }) => id !== 'userStore').map((store) => store.hydrate?.()));
await registerModules();
if (userStore.state.currentUser?.role) {
await setLanguage((userStore.state.currentUser?.language as Language) || 'en-US');
await Promise.all(stores.filter(({ id }) => id !== 'userStore').map((store) => store.hydrate?.()));
await registerModules();
}
} catch (error) {
appStore.state.error = error;
} finally {

View File

@@ -79,7 +79,7 @@
</v-card>
</v-dialog>
<modal-collection
<drawer-collection
collection="directus_files"
:active="activeDialog === 'choose'"
@update:active="activeDialog = null"
@@ -112,7 +112,7 @@
<script lang="ts">
import { defineComponent, ref, watch, computed } from '@vue/composition-api';
import ModalCollection from '@/views/private/components/modal-collection';
import DrawerCollection from '@/views/private/components/drawer-collection';
import api from '@/api';
import readableMimeType from '@/utils/readable-mime-type';
import getRootPath from '@/utils/get-root-path';
@@ -124,7 +124,7 @@ type FileInfo = {
};
export default defineComponent({
components: { ModalCollection },
components: { DrawerCollection },
props: {
value: {
type: String,

View File

@@ -34,7 +34,7 @@
</v-button>
</div>
<modal-item
<drawer-item
v-if="!disabled"
:active="editModalActive"
:collection="relationInfo.junctionCollection"
@@ -46,7 +46,7 @@
@update:active="cancelEdit"
/>
<modal-collection
<drawer-collection
v-if="!disabled"
:active.sync="selectModalActive"
:collection="relationInfo.relationCollection"
@@ -71,8 +71,8 @@
<script lang="ts">
import { defineComponent, ref, computed, toRefs, PropType } from '@vue/composition-api';
import { Header as TableHeader } from '@/components/v-table/types';
import ModalCollection from '@/views/private/components/modal-collection';
import ModalItem from '@/views/private/components/modal-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
import DrawerItem from '@/views/private/components/drawer-item';
import { get } from 'lodash';
import i18n from '@/lang';
@@ -83,7 +83,7 @@ import usePreview from '@/interfaces/many-to-many/use-preview';
import useEdit from '@/interfaces/many-to-many/use-edit';
export default defineComponent({
components: { ModalCollection, ModalItem },
components: { DrawerCollection, DrawerItem },
props: {
primaryKey: {
type: [Number, String],

View File

@@ -91,6 +91,8 @@ export default defineComponent({
};
function setIcon(icon: string | null) {
searchQuery.value = '';
emit('input', icon);
}
},

View File

@@ -42,7 +42,7 @@
</v-button>
</div>
<modal-item
<drawer-item
v-if="!disabled"
:active="editModalActive"
:collection="relationInfo.junctionCollection"
@@ -54,7 +54,7 @@
@update:active="cancelEdit"
/>
<modal-collection
<drawer-collection
v-if="!disabled"
:active.sync="selectModalActive"
:collection="relationCollection.collection"
@@ -68,8 +68,8 @@
<script lang="ts">
import { defineComponent, ref, computed, watch, PropType, toRefs } from '@vue/composition-api';
import ModalItem from '@/views/private/components/modal-item';
import ModalCollection from '@/views/private/components/modal-collection';
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
import { get } from 'lodash';
import useActions from './use-actions';
@@ -80,7 +80,7 @@ import useSelection from './use-selection';
import useSort from './use-sort';
export default defineComponent({
components: { ModalItem, ModalCollection },
components: { DrawerItem, DrawerCollection },
props: {
value: {
type: Array as PropType<(number | string | Record<string, any>)[] | null>,

View File

@@ -82,7 +82,7 @@
</v-list>
</v-menu>
<modal-item
<drawer-item
v-if="!disabled"
:active.sync="editModalActive"
:collection="relatedCollection.collection"
@@ -91,7 +91,7 @@
@input="stageEdits"
/>
<modal-collection
<drawer-collection
v-if="!disabled"
:active.sync="selectModalActive"
:collection="relatedCollection.collection"
@@ -107,8 +107,8 @@ import { useCollectionsStore, useRelationsStore } from '@/stores/';
import useCollection from '@/composables/use-collection';
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
import api from '@/api';
import ModalItem from '@/views/private/components/modal-item';
import ModalCollection from '@/views/private/components/modal-collection';
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
/**
* @NOTE
@@ -119,7 +119,7 @@ import ModalCollection from '@/views/private/components/modal-collection';
*/
export default defineComponent({
components: { ModalItem, ModalCollection },
components: { DrawerItem, DrawerCollection },
props: {
value: {
type: [Number, String, Object],

View File

@@ -28,7 +28,3 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
//
</style>

View File

@@ -42,7 +42,7 @@
</v-button>
</div>
<modal-item
<drawer-item
v-if="!disabled"
:active="currentlyEditing !== null"
:collection="relatedCollection.collection"
@@ -52,7 +52,7 @@
@update:active="cancelEdit"
/>
<modal-collection
<drawer-collection
v-if="!disabled"
:active.sync="selectModalActive"
:collection="relatedCollection.collection"
@@ -69,15 +69,14 @@ import { defineComponent, ref, computed, watch, PropType } from '@vue/compositio
import api from '@/api';
import useCollection from '@/composables/use-collection';
import { useCollectionsStore, useRelationsStore, useFieldsStore } from '@/stores/';
import ModalItem from '@/views/private/components/modal-item';
import ModalCollection from '@/views/private/components/modal-collection';
import { Sort } from '@/components/v-table/types';
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
import { Filter, Field } from '@/types';
import { Header } from '@/components/v-table/types';
import { Header, Sort } from '@/components/v-table/types';
import { isEqual, sortBy } from 'lodash';
export default defineComponent({
components: { ModalItem, ModalCollection },
components: { DrawerItem, DrawerCollection },
props: {
value: {
type: Array as PropType<(number | string | Record<string, any>)[] | null>,

View File

@@ -1,46 +1,43 @@
<template>
<div v-if="itemsLoading || languagesLoading" class="loader">
<div v-if="languagesLoading">
<v-skeleton-loader v-for="n in 5" :key="n" />
</div>
<v-item-group v-else scope="translations" class="translations">
<v-item
scope="translations"
class="row"
v-for="(item, index) in languages"
:key="item[languagesPrimaryKeyField.field]"
#default="{ active, toggle }"
<div class="translations" v-else>
<button
v-for="languageItem in languages"
:key="languageItem[languagesPrimaryKeyField]"
@click="startEditing(languageItem[languagesPrimaryKeyField])"
class="language-row"
>
<div class="header" @click="toggle">
<render-template :template="rowTemplate" :collection="languagesCollection" :item="item" />
</div>
<transition-expand>
<div v-if="active">
<div class="form">
<v-divider />
<v-form
:initial-values="existing[index]"
:collection="translationsCollection"
:primary-key="existing[index][translationsPrimaryKeyField.field] || '+'"
:edits="edits[index]"
@input="emitValue($event, existing[index][translationsPrimaryKeyField.field])"
/>
</div>
</div>
</transition-expand>
</v-item>
</v-item-group>
<v-icon class="translate" name="translate" />
<render-template :template="languagesTemplate" :collection="languagesCollection" :item="languageItem" />
<div class="spacer" />
<v-icon class="launch" name="launch" />
</button>
<drawer-item
v-if="editing"
active
:collection="translationsCollection"
:primary-key="editing"
:edits="edits"
@input="stageEdits"
@update:active="cancelEdit"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, toRefs, watch, PropType } from '@vue/composition-api';
import { useCollectionsStore, useRelationsStore, useFieldsStore } from '@/stores/';
import useCollection from '@/composables/use-collection';
import { defineComponent, PropType, computed, ref, watch } from '@vue/composition-api';
import { useRelationsStore } from '@/stores/';
import api from '@/api';
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
import { Relation } from '@/types';
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
import DrawerItem from '@/views/private/components/drawer-item/drawer-item.vue';
export default defineComponent({
components: { DrawerItem },
props: {
collection: {
type: String,
@@ -59,70 +56,63 @@ export default defineComponent({
default: null,
},
value: {
type: Array as PropType<Record<string, any>[]>,
type: Array as PropType<(string | number | Record<string, any>)[]>,
default: () => [],
},
},
setup(props, { emit }) {
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const {
relations,
relationsForField,
translationsRelation,
translationsCollection,
languagesCollection,
languageField,
translationsPrimaryKeyField,
} = useRelation();
languagesRelation,
languagesCollection,
languagesPrimaryKeyField,
translationsLanguageField,
} = useRelations();
const {
languages,
loading: languagesLoading,
error: languagesError,
primaryKeyField: languagesPrimaryKeyField,
template: languagesTemplate,
} = useLanguages();
const { items, loading: itemsLoading, error: itemsError } = useCurrent();
const { existing, edits, emitValue } = useValues();
const rowTemplate = computed(() => {
const { info, primaryKeyField } = useCollection(languagesCollection);
const defaultTemplate = info.value?.meta?.display_template;
return props.template || defaultTemplate || `{{ ${primaryKeyField.value.field} }}`;
});
const { startEditing, editing, edits, stageEdits, cancelEdit } = useEdits();
return {
relations,
relationsForField,
translationsRelation,
translationsCollection,
languagesCollection,
languagesRelation,
languages,
languagesLoading,
languagesError,
languagesTemplate,
languagesCollection,
languagesPrimaryKeyField,
items,
itemsLoading,
itemsError,
existing,
languagesLoading,
startEditing,
translationsLanguageField,
editing,
stageEdits,
cancelEdit,
edits,
emitValue,
rowTemplate,
translationsPrimaryKeyField,
};
function useRelation() {
const relations = computed(() => {
function useRelations() {
const relationsForField = computed(() => {
return relationsStore.getRelationsForField(props.collection, props.field);
});
const translationsRelation = computed(() => {
if (!relations.value || relations.value.length === 0) return null;
if (!relationsForField.value) return null;
return (
relations.value.find((relation: Relation) => {
return relation.one_collection === props.collection && relation.one_field === props.field;
}) || null
relationsForField.value.find(
(relation: Relation) =>
relation.one_collection === props.collection && relation.one_field === props.field
) || null
);
});
@@ -132,16 +122,15 @@ export default defineComponent({
});
const translationsPrimaryKeyField = computed(() => {
return fieldsStore.getPrimaryKeyFieldForCollection(translationsCollection.value);
if (!translationsRelation.value) return null;
return translationsRelation.value.many_primary;
});
const languagesRelation = computed(() => {
if (!relations.value || relations.value.length === 0) return null;
if (!relationsForField.value) return null;
return (
relations.value.find((relation: Relation) => {
return relation.one_collection !== props.collection && relation.one_field !== props.field;
}) || null
relationsForField.value.find((relation: Relation) => relation !== translationsRelation.value) ||
null
);
});
@@ -150,49 +139,51 @@ export default defineComponent({
return languagesRelation.value.one_collection;
});
const languageField = computed(() => {
const languagesPrimaryKeyField = computed(() => {
if (!languagesRelation.value) return null;
return languagesRelation.value.one_primary;
});
const translationsLanguageField = computed(() => {
if (!languagesRelation.value) return null;
return languagesRelation.value.many_field;
});
return {
relations,
relationsForField,
translationsRelation,
translationsCollection,
languagesCollection,
languageField,
translationsPrimaryKeyField,
languagesRelation,
languagesCollection,
languagesPrimaryKeyField,
translationsLanguageField,
};
}
function useLanguages() {
const languages = ref<Record<string, any> | null>(null);
const languages = ref();
const loading = ref(false);
const error = ref(null);
const error = ref<any>(null);
const { primaryKeyField } = useCollection(languagesCollection);
const template = computed(() => {
if (!languagesPrimaryKeyField.value) return '';
return props.template || `{{ ${languagesPrimaryKeyField.value} }}`;
});
watch(languagesCollection, fetchLanguages, { immediate: true });
return { languages, loading, error, primaryKeyField };
return { languages, loading, error, template };
async function fetchLanguages() {
if (!languagesCollection.value) return;
const fields = getFieldsFromTemplate(template.value);
loading.value = true;
// const fields = getFieldsFromTemplate(props.template);
const fields = ['*'];
if (fields.includes(primaryKeyField.value.field) === false) {
fields.push(primaryKeyField.value.field);
}
try {
const response = await api.get(`/items/${languagesCollection.value}`, {
params: {
fields: fields,
limit: -1,
},
});
const response = await api.get(`/items/${languagesCollection.value}`, { params: { fields } });
languages.value = response.data.data;
} catch (err) {
error.value = err;
@@ -202,100 +193,135 @@ export default defineComponent({
}
}
function useCurrent() {
function useEdits() {
const keyMap = ref<Record<string, string | number>[]>();
const loading = ref(false);
const items = ref<any[]>([]);
const error = ref(null);
const error = ref<any>(null);
watch(
() => props.primaryKey,
(newKey) => {
if (newKey !== null && newKey !== '+') {
fetchCurrent();
}
},
{
immediate: true,
const editing = ref<boolean | string | number>(false);
const edits = ref<Record<string, any>>();
const existingPrimaryKeys = computed(() => {
return (props.value || [])
.map((value) => {
if (typeof value === 'string' || typeof value === 'number') return value;
return value[translationsPrimaryKeyField.value];
})
.filter((key) => key);
});
watch(() => props.value, fetchKeyMap, { immediate: true });
return { startEditing, editing, edits, stageEdits, cancelEdit };
function startEditing(language: string | number) {
edits.value = {
[translationsLanguageField.value]: language,
};
const existingEdits = (props.value || []).find((val) => {
if (typeof val === 'string' || typeof val === 'number') return false;
return val[translationsLanguageField.value] === language;
});
if (existingEdits) {
edits.value = {
...edits.value,
...(existingEdits as Record<string, any>),
};
}
);
return { loading, items, error };
const primaryKey =
keyMap.value?.find((record) => record[translationsLanguageField.value] === language)?.[
translationsPrimaryKeyField.value
] || '+';
if (primaryKey !== '+') {
edits.value = {
...edits.value,
[translationsPrimaryKeyField.value]: primaryKey,
};
}
editing.value = primaryKey;
}
async function fetchKeyMap() {
if (!props.value) return;
if (keyMap.value) return;
const collection = translationsRelation.value.many_collection;
const fields = [translationsPrimaryKeyField.value, translationsLanguageField.value];
async function fetchCurrent() {
loading.value = true;
try {
const response = await api.get(`/items/${props.collection}/${props.primaryKey}`, {
const response = await api.get(`/items/${collection}`, {
params: {
fields: props.field + '.*',
fields,
filter: {
[translationsPrimaryKeyField.value]: {
_in: existingPrimaryKeys.value,
},
},
},
});
items.value = response.data.data[props.field];
keyMap.value = response.data.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
}
function useValues() {
const existing = computed(() => {
if (!languages.value) return [];
function stageEdits(edits: any) {
const editedLanguage = edits[translationsLanguageField.value];
return languages.value.map((language: any) => {
const existing =
items.value.find(
(item) => item[languageField.value] === language[languagesPrimaryKeyField.value.field]
) || {};
return existing;
const languageAlreadyEdited = !!(props.value || []).find((val) => {
if (typeof val === 'string' || typeof val === 'number') return false;
return val[translationsLanguageField.value] === editedLanguage;
});
});
const edits = computed(() => {
if (!languages.value) return [];
return languages.value.map((language: any) => {
const edits =
(props.value || []).find(
(edit) => edit[languageField.value] === language[languagesPrimaryKeyField.value.field]
) || {};
edits[languageField.value] = language[languagesPrimaryKeyField.value.field];
return edits;
});
});
return { existing, edits, emitValue };
function emitValue(newEdit: any, existingPrimaryKey: undefined | string | number) {
const currentEdits = [...(props.value || [])];
if (existingPrimaryKey) {
newEdit = {
...newEdit,
[translationsPrimaryKeyField.value.field]: existingPrimaryKey,
};
}
if (currentEdits.some((edit) => edit[languageField.value] === newEdit[languageField.value])) {
if (languageAlreadyEdited === true) {
emit(
'input',
currentEdits.map((edit) => {
if (edit[languageField.value] === newEdit[languageField.value]) {
return newEdit;
props.value.map((val) => {
if (typeof val === 'string' || typeof val === 'number') return val;
if (val[translationsLanguageField.value] === editedLanguage) {
return edits;
}
return edit;
return val;
})
);
} else {
currentEdits.push(newEdit);
emit('input', currentEdits);
if (editing.value === '+') {
emit('input', [...(props.value || []), edits]);
} else {
emit(
'input',
props.value.map((val) => {
if (typeof val === 'string' || typeof val === 'number') {
if (val === editing.value) return edits;
} else {
if (val[translationsPrimaryKeyField.value] === editing.value) return edits;
}
return val;
})
);
}
}
editing.value = false;
}
function cancelEdit() {
edits.value = {};
editing.value = false;
}
}
},
@@ -303,41 +329,35 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/type-styles.scss';
.language-row {
--v-icon-color: var(--foreground-subdued);
.loader .v-skeleton-loader + .v-skeleton-loader {
margin-top: 8px;
}
.header {
display: flex;
align-items: center;
width: 100%;
padding: 12px;
cursor: pointer;
}
.row {
text-align: left;
background-color: var(--background-subdued);
border-radius: var(--border-radius);
& + .row {
& + & {
margin-top: 8px;
}
}
.v-divider {
margin-bottom: 12px;
}
.translate {
margin-right: 12px;
}
.form {
--v-form-vertical-gap: 24px;
--v-form-horizontal-gap: 12px;
.spacer {
flex-grow: 1;
}
padding: 12px;
padding-top: 0;
.launch {
transition: color var(--fast) var(--transition);
}
::v-deep .type-label {
@include type-text;
&:hover .launch {
--v-icon-color: var(--foreground-normal);
}
}
</style>

View File

@@ -72,7 +72,7 @@
</v-list>
</v-menu>
<modal-item
<drawer-item
:active.sync="editModalActive"
collection="directus_users"
:primary-key="currentPrimaryKey"
@@ -81,7 +81,7 @@
v-if="!disabled"
/>
<modal-collection
<drawer-collection
:active.sync="selectModalActive"
collection="directus_users"
:selection="selection"
@@ -95,11 +95,11 @@
import { defineComponent, computed, ref, watch, PropType } from '@vue/composition-api';
import useCollection from '@/composables/use-collection';
import api from '@/api';
import ModalItem from '@/views/private/components/modal-item';
import ModalCollection from '@/views/private/components/modal-collection';
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
export default defineComponent({
components: { ModalItem, ModalCollection },
components: { DrawerItem, DrawerCollection },
props: {
value: {
type: String,

View File

@@ -1,5 +1,9 @@
/* stylelint-disable font-family-no-missing-generic-family-keyword */
.tox {
font-family: var(--family-sans-serif);
}
.tox .tox-tbtn {
margin: 2px 2px 4px 0;
color: var(--foreground-normal);
@@ -138,8 +142,9 @@ body.dark .tox .tox-toolbar__overflow {
}
.tox .tox-dialog__header {
padding: 16px 24px 0 24px;
padding: 20px;
color: var(--foreground-normal);
font-size: 16px;
background-color: var(--background-page);
}
@@ -156,18 +161,61 @@ body.dark .tox .tox-toolbar__overflow {
}
.tox .tox-textfield,
.tox .tox-listboxfield .tox-listbox,
.tox .tox-toolbar-textfield,
.tox .tox-selectfield select,
.tox .tox-textarea {
padding: 12px;
color: var(--foreground-normal);
font-family: monospace;
font-weight: 500;
font-size: 14px;
font-family: var(--family-sans-serif);
background-color: var(--background-page);
border: 2px solid var(--border-normal);
border-radius: var(--border-radius);
transition: var(--fast) var(--transition);
}
.tox .tox-textarea {
font-family: var(--family-monospace);
}
.tox .tox-textfield:focus,
.tox .tox-listboxfield .tox-listbox:focus,
.tox .tox-toolbar-textfield:focus,
.tox .tox-selectfield select:focus,
.tox .tox-textarea:focus {
background-color: var(--background-page);
}
.tox .tox-menu {
box-sizing: border-box;
padding: 4px !important;
color: var(--foreground-normal);
font-family: var(--family-sans-serif);
background-color: var(--background-subdued);
border: 2px solid var(--border-normal);
border-radius: var(--border-radius);
}
.tox .tox-collection__item {
border-radius: var(--border-radius);
}
.tox .tox-collection--list .tox-collection__item {
color: var(--foreground-normal);
}
.tox .tox-collection--list .tox-collection__item--active {
color: var(--foreground-normal) !important;
background-color: var(--background-page) !important;
}
.tox .tox-collection--list .tox-collection__item--enabled {
color: var(--foreground-normal);
background-color: var(--background-page);
}
.tox .tox-textfield:focus,
.tox .tox-selectfield select:focus,
.tox .tox-textarea:focus {
@@ -184,6 +232,7 @@ body.dark .tox .tox-toolbar__overflow {
color: var(--white);
font-weight: 500;
font-size: 16px;
font-family: var(--family-sans-serif);
line-height: 19px;
background-color: var(--primary);
border: 2px solid var(--primary);
@@ -228,12 +277,45 @@ body.dark .tox .tox-toolbar__overflow {
}
.tox .tox-form__group {
margin-top: 24px;
margin-bottom: 24px;
}
.tox .tox-label,
.tox .tox-toolbar-label {
margin-bottom: 10px;
margin-bottom: 4px;
color: var(--foreground-normal);
font-size: 14px;
font-weight: 500;
font-size: 16px;
}
.tox .tox-dialog__body-nav-item {
box-sizing: border-box;
width: 100%;
margin-bottom: 4px;
padding: 12px 16px;
color: var(--foreground-normal);
font-weight: 500;
font-size: 14px;
border-bottom: none;
border-radius: var(--border-radius);
transition: var(--fast) var(--transition);
transition-property: background-color, color;
}
.tox .tox-dialog__body-nav-item:hover {
background-color: var(--background-normal-alt);
}
.tox .tox-dialog__body-nav-item--active {
background-color: var(--background-normal-alt);
}
.tox .tox-dialog__body-nav-item--active:focus {
background-color: var(--background-normal-alt);
}
@media screen and (max-width: 767px) {
.tox .tox-dialog__body-nav-item {
text-align: center;
}
}

View File

@@ -162,8 +162,8 @@
"create_field": "Create Field",
"update_field": "Update Field",
"creating_new_field": "{collection}: New Field",
"updating_field_field": "{collection}: \"{field}\" Field",
"creating_new_field": "New Field ({collection})",
"updating_field_field": "{field} ({collection})",
"within_collection": "Within {collection}",
"field_standard": "Standard",
@@ -919,7 +919,7 @@
"scope": "Scope",
"layout": "Layout",
"editing_file": "Editing File: {title}",
"changes_are_immediate_and_permanent": "Changes are immediate and permanent",
"changes_are_permanent": "Changes are permanent",
"preset_name_placeholder": "Name of bookmark...",
"editing_preset": "Editing Preset",

View File

@@ -46,8 +46,8 @@
</v-detail>
</portal>
<portal to="drawer">
<filter-drawer-detail v-model="_filters" :collection="collection" :loading="loading" />
<portal to="sidebar">
<filter-sidebar-detail v-model="_filters" :collection="collection" :loading="loading" />
</portal>
<portal to="actions:prepend">

View File

@@ -49,8 +49,8 @@
</div>
</portal>
<portal to="drawer">
<filter-drawer-detail v-model="_filters" :collection="collection" :loading="loading" />
<portal to="sidebar">
<filter-sidebar-detail v-model="_filters" :collection="collection" :loading="loading" />
</portal>
<portal to="actions:prepend">

View File

@@ -31,12 +31,12 @@
<router-view name="detail" :primary-key="primaryKey" />
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<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" />
</sidebar-detail>
<layout-sidebar-detail @input="layout = $event" :value="layout" />
<portal-target name="sidebar" />
</template>
</private-view>
</template>
@@ -47,8 +47,8 @@ import ActivityNavigation from '../components/navigation.vue';
import { i18n } from '@/lang';
import usePreset from '@/composables/use-preset';
import marked from 'marked';
import FilterDrawerDetail from '@/views/private/components/filter-drawer-detail';
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
import FilterSidebarDetail from '@/views/private/components/filter-sidebar-detail';
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
import SearchInput from '@/views/private/components/search-input';
import { nanoid } from 'nanoid';
@@ -58,7 +58,7 @@ type Item = {
export default defineComponent({
name: 'activity-collection',
components: { ActivityNavigation, FilterDrawerDetail, LayoutDrawerDetail, SearchInput },
components: { ActivityNavigation, FilterSidebarDetail, LayoutSidebarDetail, SearchInput },
props: {
primaryKey: {
type: String,

View File

@@ -1,14 +1,14 @@
<template>
<v-modal active title="Activity Item" @toggle="close" @esc="close">
<v-drawer active title="Activity Item" @toggle="close" @cancel="close">
<v-progress-circular indeterminate v-if="loading" />
<template v-else-if="error">
<div class="content" v-else-if="error">
<v-notice type="danger">
{{ error }}
</v-notice>
</template>
</div>
<template v-else>
<div class="content" v-else>
<!-- @TODO add final design -->
<p class="type-label">User:</p>
<user-popover v-if="item.user" :user="item.user.id">
@@ -32,17 +32,18 @@
<p class="type-label">Item:</p>
<p>{{ item.item }}</p>
</template>
</div>
<template #footer>
<v-button v-if="openItemLink" :to="openItemLink">
<v-icon name="launch" left />
{{ $t('open') }}
<template #actions>
<v-button v-if="openItemLink" :to="openItemLink" icon rounded v-tooltip.bottom="$t('open')">
<v-icon name="launch" />
</v-button>
<v-button to="/activity">{{ $t('done') }}</v-button>
<v-button to="/activity" icon rounded v-tooltip.bottom="$t('done')">
<v-icon name="check" />
</v-button>
</template>
</v-modal>
</v-drawer>
</template>
<script lang="ts">
@@ -137,4 +138,10 @@ export default defineComponent({
.type-label:not(:first-child) {
margin-top: 24px;
}
.content {
padding: var(--content-padding);
padding-top: 0;
padding-bottom: var(--content-padding);
}
</style>

View File

@@ -203,8 +203,8 @@
</template>
</component>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div
class="page-description"
v-html="
@@ -215,10 +215,10 @@
)
"
/>
</drawer-detail>
<layout-drawer-detail @input="layout = $event" :value="layout" />
<portal-target name="drawer" />
<export-drawer-detail
</sidebar-detail>
<layout-sidebar-detail @input="layout = $event" :value="layout" />
<portal-target name="sidebar" />
<export-sidebar-detail
:layout-query="layoutQuery"
:search-query="searchQuery"
:collection="currentCollection"
@@ -247,8 +247,8 @@ import { LayoutComponent } from '@/layouts/types';
import CollectionsNotFound from './not-found.vue';
import useCollection from '@/composables/use-collection';
import usePreset from '@/composables/use-preset';
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
import ExportDrawerDetail from '@/views/private/components/export-drawer-detail';
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
import ExportSidebarDetail from '@/views/private/components/export-sidebar-detail';
import SearchInput from '@/views/private/components/search-input';
import BookmarkAdd from '@/views/private/components/bookmark-add';
import BookmarkEdit from '@/views/private/components/bookmark-edit';
@@ -265,8 +265,8 @@ export default defineComponent({
components: {
CollectionsNavigation,
CollectionsNotFound,
LayoutDrawerDetail,
ExportDrawerDetail,
LayoutSidebarDetail,
ExportSidebarDetail,
SearchInput,
BookmarkAdd,
BookmarkEdit,
@@ -591,14 +591,14 @@ export default defineComponent({
--v-button-background-color-hover: var(--background-normal-alt);
}
.layout {
--layout-offset-top: 64px;
}
.header-icon {
--v-button-color-disabled: var(--foreground-normal);
}
.layout {
--layout-offset-top: 64px;
}
.bookmark-controls {
.add,
.save,

View File

@@ -176,10 +176,10 @@
</v-card>
</v-dialog>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_collections_item'))" />
</drawer-detail>
</sidebar-detail>
<revisions-drawer-detail
v-if="
collectionInfo.meta &&
@@ -192,7 +192,7 @@
ref="revisionsDrawerDetail"
@revert="refresh"
/>
<comments-drawer-detail
<comments-sidebar-detail
v-if="
collectionInfo.meta &&
collectionInfo.meta.singleton === false &&
@@ -215,7 +215,7 @@ import router from '@/router';
import CollectionsNotFound from './not-found.vue';
import useCollection from '@/composables/use-collection';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import CommentsDrawerDetail from '@/views/private/components/comments-drawer-detail';
import CommentsSidebarDetail from '@/views/private/components/comments-sidebar-detail';
import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
import i18n from '@/lang';
@@ -236,7 +236,7 @@ export default defineComponent({
CollectionsNavigation,
CollectionsNotFound,
RevisionsDrawerDetail,
CommentsDrawerDetail,
CommentsSidebarDetail,
SaveOptions,
},
props: {

View File

@@ -28,10 +28,10 @@
</template>
</v-info>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_collections_overview'))" />
</drawer-detail>
</sidebar-detail>
</template>
</private-view>
</template>

View File

@@ -338,10 +338,14 @@ export default defineComponent({
table tr {
margin: 0;
padding: 0;
background-color: white;
background-color: var(--background-normal);
border-top: 1px solid var(--background-normal);
}
table thead tr {
background-color: var(--background-normal-alt);
}
table tr:nth-child(2n) {
background-color: var(--background-page);
}

View File

@@ -16,10 +16,10 @@
<markdown>{{ markdownWithoutTitle }}</markdown>
</div>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_docs_global'))" />
</drawer-detail>
</sidebar-detail>
</template>
</private-view>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<drawer-detail icon="info_outline" :title="$t('file_details')" close>
<sidebar-detail icon="info_outline" :title="$t('file_details')" close>
<dl v-if="file">
<div v-if="file.type">
<dt>{{ $t('type') }}</dt>
@@ -41,11 +41,11 @@
<dd>{{ file.checksum }}</dd>
</div>
<div v-if="user_created">
<div v-if="userCreated">
<dt>{{ $t('owner') }}</dt>
<dd>
<user-popover :user="user_created.id">
<router-link :to="user_created.link">{{ user_created.name }}</router-link>
<user-popover :user="userCreated.id">
<router-link :to="userCreated.link">{{ userCreated.name }}</router-link>
</user-popover>
</dd>
</div>
@@ -55,11 +55,11 @@
<dd>{{ modificationDate }}</dd>
</div>
<div v-if="user_modified">
<div v-if="userModified">
<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 :user="userModified.id">
<router-link :to="userModified.link">{{ userModified.name }}</router-link>
</user-popover>
</dd>
</div>
@@ -104,7 +104,7 @@
<v-divider />
<div class="page-description" v-html="marked($t('page_help_files_item'))" />
</drawer-detail>
</sidebar-detail>
</template>
<script lang="ts">

View File

@@ -126,12 +126,12 @@
<router-view name="addNew" :preset="queryFilters" @upload="refresh" />
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<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" />
</sidebar-detail>
<layout-sidebar-detail @input="layout = $event" :value="layout" />
<portal-target name="sidebar" />
</template>
<template v-if="showDropEffect">
@@ -150,8 +150,8 @@ import { i18n } from '@/lang';
import api from '@/api';
import { LayoutComponent } from '@/layouts/types';
import usePreset from '@/composables/use-preset';
import FilterDrawerDetail from '@/views/private/components/filter-drawer-detail';
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
import FilterSidebarDetail from '@/views/private/components/filter-sidebar-detail';
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
import AddFolder from '../components/add-folder.vue';
import SearchInput from '@/views/private/components/search-input';
import marked from 'marked';
@@ -171,7 +171,7 @@ type Item = {
export default defineComponent({
name: 'files-collection',
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail, AddFolder, SearchInput, FolderPicker },
components: { FilesNavigation, FilterSidebarDetail, LayoutSidebarDetail, AddFolder, SearchInput, FolderPicker },
props: {
queryFilters: {
type: Object as PropType<Record<string, string>>,
@@ -506,9 +506,11 @@ export default defineComponent({
});
await uploadFiles(files, {
preset: {
folder: props.queryFilters?.folder || null,
},
preset: props.queryFilters?.folder
? {
folder: props.queryFilters.folder,
}
: {},
onProgressChange: (progress) => {
const percentageDone = progress.reduce((val, cur) => (val += cur)) / progress.length;

View File

@@ -155,15 +155,15 @@
</v-card>
</v-dialog>
<template #drawer>
<file-info-drawer-detail :file="item" @move-folder="moveToDialogActive = true" />
<template #sidebar>
<file-info-sidebar-detail :file="item" @move-folder="moveToDialogActive = true" />
<revisions-drawer-detail
v-if="isBatch === false && isNew === false"
collection="directus_files"
:primary-key="primaryKey"
ref="revisionsDrawerDetail"
/>
<comments-drawer-detail
<comments-sidebar-detail
v-if="isBatch === false && isNew === false"
collection="directus_files"
:primary-key="primaryKey"
@@ -178,7 +178,7 @@ import FilesNavigation from '../components/navigation.vue';
import { i18n } from '@/lang';
import router from '@/router';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import CommentsDrawerDetail from '@/views/private/components/comments-drawer-detail';
import CommentsSidebarDetail from '@/views/private/components/comments-sidebar-detail';
import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
import FilePreview from '@/views/private/components/file-preview';
@@ -187,7 +187,7 @@ import { nanoid } from 'nanoid';
import FileLightbox from '@/views/private/components/file-lightbox';
import { useFieldsStore } from '@/stores/';
import { Field } from '@/types';
import FileInfoDrawerDetail from '../components/file-info-drawer-detail.vue';
import FileInfoSidebarDetail from '../components/file-info-sidebar-detail.vue';
import useFormFields from '@/composables/use-form-fields';
import FolderPicker from '../components/folder-picker.vue';
import api from '@/api';
@@ -216,12 +216,12 @@ export default defineComponent({
components: {
FilesNavigation,
RevisionsDrawerDetail,
CommentsDrawerDetail,
CommentsSidebarDetail,
SaveOptions,
FilePreview,
ImageEditor,
FileLightbox,
FileInfoDrawerDetail,
FileInfoSidebarDetail,
FolderPicker,
FilesNotFound,
},

View File

@@ -40,7 +40,7 @@
<v-icon
class="icon"
:class="{
hidden: item.meta && item.meta.hidden || false,
hidden: (item.meta && item.meta.hidden) || false,
system: item.collection.startsWith('directus_'),
unmanaged: item.meta === null && item.collection.startsWith('directus_') === false,
}"
@@ -52,7 +52,7 @@
<span
class="collection"
:class="{
hidden: item.meta && item.meta.hidden || false,
hidden: (item.meta && item.meta.hidden) || false,
system: item.collection.startsWith('directus_'),
unmanaged: item.meta === null && item.collection.startsWith('directus_') === false,
}"
@@ -86,10 +86,10 @@
<router-view name="add" />
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_settings_datamodel_collections'))" />
</drawer-detail>
</sidebar-detail>
<collections-filter v-model="activeTypes" />
</template>
</private-view>

View File

@@ -1,11 +1,11 @@
<template>
<drawer-detail class="collections-filter" icon="filter_list" :title="$tc('collection', 2)">
<sidebar-detail class="collections-filter" icon="filter_list" :title="$tc('collection', 2)">
<div class="type-label label">{{ $t('collections_shown') }}</div>
<v-checkbox value="visible" v-model="_value" :label="$t('visible_collections')" />
<v-checkbox value="unmanaged" v-model="_value" :label="$t('unmanaged_collections')" />
<v-checkbox value="hidden" v-model="_value" :label="$t('hidden_collections')" />
<v-checkbox value="system" v-model="_value" :label="$t('system_collections')" />
</drawer-detail>
</sidebar-detail>
</template>
<script lang="ts">

View File

@@ -1,14 +1,18 @@
<template>
<div class="actions">
<v-button secondary @click="$emit('cancel')">
{{ $t('cancel') }}
<v-button
v-if="!isExisting && currentTabIndex < tabs.length - 1"
@click="nextTab"
:disabled="nextDisabled"
icon
rounded
v-tooltip.bottom="$t('next')"
>
<v-icon name="arrow_forward" />
</v-button>
<div class="spacer" />
<v-button v-if="!isExisting && currentTabIndex < tabs.length - 1" @click="nextTab" :disabled="nextDisabled">
{{ $t('next') }}
</v-button>
<v-button v-else @click="$emit('save')" :loading="saving">
{{ $t('save') }}
<v-button v-else @click="$emit('save')" :loading="saving" icon rounded v-tooltip.bottom="$t('save')">
<v-icon name="check" />
</v-button>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<h2 class="type-title">{{ $t('display_setup_title') }}</h2>
<v-notice type="info">{{ $t('display_setup_title') }}</v-notice>
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.meta.display" />
@@ -130,4 +130,8 @@ export default defineComponent({
text-decoration: underline;
}
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<h2 class="type-title">{{ $t('schema_field_title') }}</h2>
<v-notice type="info">{{ $t('schema_field_title') }}</v-notice>
<div class="form">
<div class="field half-left" v-if="fieldData.meta">
@@ -103,4 +103,8 @@ export default defineComponent({
.required {
--v-icon-color: var(--primary);
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<h2 class="type-title">{{ $t('interface_setup_title') }}</h2>
<v-notice type="info">{{ $t('interface_setup_title') }}</v-notice>
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.meta.interface" />
@@ -139,4 +139,8 @@ export default defineComponent({
text-decoration: underline;
}
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,145 +0,0 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_languages') }}</h2>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('translations_collection') }}</div>
<v-input disabled :value="relations[1].many_collection" />
</div>
<div class="field">
<div class="type-label">{{ $t('languages_collection') }}</div>
<v-input :class="{ matches: languagesCollectionExists }" db-safe key="languages-collection" v-model="relations[1].one_collection" :disabled="isExisting" :placeholder="$t('collection') + '...'">
<template #append>
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" :disabled="isExisting" />
</template>
<v-list class="monospace">
<v-list-item
v-for="item in items"
:key="item.value"
:active="relations[1].one_collection === item.value"
:disabled="item.disabled"
@click="relations[1].one_collection = item.value"
>
<v-list-item-content>
{{ item.text }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-input>
</div>
<v-input :value="relations[1].many_field" :placeholder="$t('foreign_key') + '...'"/>
<v-input db-safe :disabled="languagesCollectionExists" v-model="relations[1].one_primary" :placeholder="$t('primary_key') + '...'" />
<v-icon class="arrow" name="arrow_back" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, watch } from '@vue/composition-api';
import { Relation } from '@/types';
import { Field } from '@/types';
import { orderBy } from 'lodash';
import useSync from '@/composables/use-sync';
import { useCollectionsStore, useFieldsStore } from '@/stores';
import i18n from '@/lang';
import { state } from '../store';
export default defineComponent({
props: {
type: {
type: String,
required: true,
},
collection: {
type: String,
required: true,
},
isExisting: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { items } = useRelation();
const languagesCollectionExists = computed(() => {
return !!collectionsStore.getCollection(state.relations[1].one_collection);
});
return {
relations: state.relations,
items,
fieldData: state.fieldData,
languagesCollectionExists,
};
function useRelation() {
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return collection.collection.startsWith('directus_') === false;
}),
['collection'],
['asc']
);
});
const items = computed(() =>
availableCollections.value.map((collection) => ({
text: collection.collection,
value: collection.collection,
}))
);
return { items };
}
},
});
</script>
<style lang="scss" scoped>
.grid {
--v-select-font-family: var(--family-monospace);
--v-input-font-family: var(--family-monospace);
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 32px;
margin-top: 48px;
.v-input.matches {
--v-input-color: var(--primary);
}
.arrow {
--v-icon-color: var(--primary);
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
}
}
.v-list {
--v-list-item-content-font-family: var(--family-monospace);
}
.v-divider {
margin: 48px 0;
}
.type-label {
margin-bottom: 8px;
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_m2m') }}</h2>
<v-notice type="info">{{ $t('configure_m2m') }}</v-notice>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
@@ -452,4 +453,8 @@ export default defineComponent({
transform: translateX(-50%);
}
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_m2o') }}</h2>
<v-notice type="info">{{ $t('configure_m2o') }}</v-notice>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
@@ -255,4 +256,8 @@ export default defineComponent({
.type-label {
margin-bottom: 8px;
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_o2m') }}</h2>
<v-notice type="info">{{ $t('configure_o2m') }}</v-notice>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
@@ -380,4 +381,8 @@ export default defineComponent({
transform: translateX(-50%);
}
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,6 +1,8 @@
<template>
<div>
<h2 class="type-title">{{ $t('schema_setup_title') }}</h2>
<v-notice type="info">
{{ $t('schema_setup_title') }}
</v-notice>
<div class="form">
<div class="field">
@@ -392,10 +394,6 @@ export default defineComponent({
@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;
@@ -416,4 +414,8 @@ export default defineComponent({
grid-gap: 12px;
grid-template-columns: 1fr 1fr;
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_m2m') }}</h2>
<v-notice type="info">{{ $t('configure_m2m') }}</v-notice>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
@@ -349,4 +350,8 @@ export default defineComponent({
.v-list {
--v-list-item-content-font-family: var(--family-monospace);
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -28,11 +28,11 @@
</v-card>
</v-dialog>
<v-modal
<v-drawer
v-else
:active="true"
@toggle="cancelField"
@esc="cancelField"
@cancel="cancelField"
:title="
field === '+'
? $t('creating_new_field', { collection: collectionInfo.name })
@@ -45,49 +45,51 @@
<setup-tabs :current.sync="currentTab" :tabs="tabs" :type="localType" />
</template>
<setup-schema
v-if="currentTab[0] === 'schema'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<div class="content">
<setup-schema
v-if="currentTab[0] === 'schema'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-field
v-if="currentTab[0] === 'field'"
:is-existing="field !== '+'"
:collection="collection"
: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 !== '+'"
:collection="collection"
:type="localType"
/>
<setup-relationship
v-if="currentTab[0] === 'relationship'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-translations
v-if="currentTab[0] === 'translations'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-translations
v-if="currentTab[0] === 'translations'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-interface
v-if="currentTab[0] === 'interface'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-interface
v-if="currentTab[0] === 'interface'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-display
v-if="currentTab[0] === 'display'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-display
v-if="currentTab[0] === 'display'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
</div>
<template #footer>
<template #actions>
<setup-actions
:saving="saving"
:collection="collection"
@@ -98,7 +100,7 @@
@cancel="cancelField"
/>
</template>
</v-modal>
</v-drawer>
</template>
<script lang="ts">
@@ -404,4 +406,10 @@ export default defineComponent({
color: var(--primary);
}
}
.content {
padding: var(--content-padding);
padding-top: 0;
padding-bottom: var(--content-padding);
}
</style>

View File

@@ -474,7 +474,7 @@ function initLocalStore(collection: string, field: string, type: typeof localTyp
name: 'German',
},
{
code: 'fr-Fr',
code: 'fr-FR',
name: 'French',
},
{

View File

@@ -49,7 +49,15 @@
<template #input>
<div class="label">
<span class="name" v-tooltip="field.name">{{ field.field }}</span>
<span class="name" v-tooltip="field.name">
{{ field.field }}
<v-icon
name="star"
class="required"
sup
v-if="field.schema && field.schema.is_nullable === false"
/>
</span>
<span v-if="field.meta" class="interface">{{ interfaceName }}</span>
<span v-else class="interface">{{ $t('db_only_click_to_configure') }}</span>
</div>
@@ -90,7 +98,7 @@
</v-list-item-content>
</v-list-item>
<v-list-item @click="duplicateActive = true">
<v-list-item v-if="duplicable" @click="duplicateActive = true">
<v-list-item-icon>
<v-icon name="content_copy" />
</v-list-item-icon>
@@ -217,7 +225,15 @@ export default defineComponent({
const editActive = ref(false);
const { deleteActive, deleting, deleteField } = useDeleteField();
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating } = useDuplicate();
const {
duplicateActive,
duplicateName,
collections,
duplicateTo,
saveDuplicate,
duplicating,
duplicable,
} = useDuplicate();
const interfaceName = computed(() => {
return interfaces.value.find((inter) => inter.id === props.field.meta?.interface)?.name;
@@ -248,6 +264,7 @@ export default defineComponent({
localType,
translationsCollection,
translationsFieldsCount,
duplicable,
};
function setWidth(width: string) {
@@ -288,6 +305,13 @@ export default defineComponent({
);
const duplicateTo = ref(props.field.collection);
const duplicable = computed(() => {
return (
['o2m', 'm2m', 'm2o', 'files', 'file', 'm2a'].includes(props.field.type) === false &&
props.field.schema?.is_primary_key === false
);
});
return {
duplicateActive,
duplicateName,
@@ -295,6 +319,7 @@ export default defineComponent({
duplicateTo,
saveDuplicate,
duplicating,
duplicable,
};
async function saveDuplicate() {
@@ -505,4 +530,8 @@ export default defineComponent({
--v-button-background-color: var(--danger);
--v-button-background-color-hover: var(--danger-125);
}
.required {
color: var(--primary);
}
</style>

View File

@@ -73,10 +73,10 @@
/>
</div>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_settings_datamodel_fields'))" />
</drawer-detail>
</sidebar-detail>
</template>
</private-view>
</template>

View File

@@ -1,10 +1,10 @@
<template>
<v-modal
<v-drawer
:title="$t('creating_new_collection')"
:active="true"
class="new-collection"
persistent
@esc="$router.push('/settings/data-model')"
@cancel="$router.push('/settings/data-model')"
>
<v-dialog :active="saveError !== null" @toggle="saveError = null" @esc="saveError = null">
<v-card class="selectable">
@@ -29,9 +29,10 @@
</v-tabs>
</template>
<v-tabs-items v-model="currentTab">
<v-tabs-items class="content" v-model="currentTab">
<v-tab-item value="collection">
<h2 class="type-title">{{ $t('creating_collection_info') }}</h2>
<v-notice type="info">{{ $t('creating_collection_info') }}</v-notice>
<div class="grid">
<div>
<div class="type-label">
@@ -83,7 +84,8 @@
</div>
</v-tab-item>
<v-tab-item value="system">
<h2 class="type-title">{{ $t('creating_collection_system') }}</h2>
<v-notice type="info">{{ $t('creating_collection_system') }}</v-notice>
<div class="grid system">
<div v-for="(info, field) in systemFields" :key="field">
<div class="type-label">{{ $t(info.label) }}</div>
@@ -106,23 +108,29 @@
</v-tab-item>
</v-tabs-items>
<template #footer>
<v-button secondary to="/settings/data-model">
{{ $t('cancel') }}
</v-button>
<div class="spacer" />
<template #actions>
<v-button
:disabled="!collectionName || collectionName.length === 0"
v-if="currentTab[0] === 'collection'"
@click="currentTab = ['system']"
v-tooltip.bottom="$t('next')"
icon
rounded
>
{{ $t('next') }}
<v-icon name="arrow_forward" />
</v-button>
<v-button v-if="currentTab[0] === 'system'" @click="save" :loading="saving">
{{ $t('finish_setup') }}
<v-button
v-if="currentTab[0] === 'system'"
@click="save"
:loading="saving"
v-tooltip.bottom="$t('finish_setup')"
icon
rounded
>
<v-icon name="check" />
</v-button>
</template>
</v-modal>
</v-drawer>
</template>
<script lang="ts">
@@ -469,4 +477,14 @@ export default defineComponent({
.required {
color: var(--primary);
}
.content {
padding: var(--content-padding);
padding-top: 0;
padding-bottom: var(--content-padding);
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -84,8 +84,8 @@
</v-table>
</div>
<template #drawer>
<presets-info-drawer-detail />
<template #sidebar>
<presets-info-sidebar-detail />
</template>
</private-view>
</template>
@@ -102,7 +102,7 @@ import { getLayouts } from '@/layouts';
import { TranslateResult } from 'vue-i18n';
import router from '@/router';
import ValueNull from '@/views/private/components/value-null';
import PresetsInfoDrawerDetail from './components/presets-info-drawer-detail.vue';
import PresetsInfoSidebarDetail from './components/presets-info-sidebar-detail.vue';
type PresetRaw = {
id: number;
@@ -122,7 +122,7 @@ type Preset = {
};
export default defineComponent({
components: { SettingsNavigation, ValueNull, PresetsInfoDrawerDetail },
components: { SettingsNavigation, ValueNull, PresetsInfoSidebarDetail },
setup() {
const layouts = getLayouts();
const collectionsStore = useCollectionsStore();

View File

@@ -1,5 +1,5 @@
<template>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<dl>
<div>
<dt>{{ $t('bookmarks') }}</dt>
@@ -14,7 +14,7 @@
<v-divider />
<div class="page-description" v-html="marked($t('page_help_settings_presets_collection'))" />
</drawer-detail>
</sidebar-detail>
</template>
<script lang="ts">
@@ -39,7 +39,7 @@ export default defineComponent({
try {
const response = await api.get(`/presets`, {
params: {
[`filter[bookmark][_nnull]`]: 1,
[`filter[bookmark][_nnull]`]: true,
fields: ['id'],
meta: 'filter_count,total_count',
},

View File

@@ -75,18 +75,18 @@
</div>
</div>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_settings_presets_item'))" />
</drawer-detail>
</sidebar-detail>
<portal-target class="layout-drawer" name="drawer" />
<portal-target class="layout-sidebar" name="sidebar" />
<drawer-detail class="layout-drawer" icon="layers" :title="$t('layout_options')">
<sidebar-detail class="layout-sidebar" icon="layers" :title="$t('layout_options')">
<div class="layout-options">
<portal-target name="layout-options" class="portal-contents" />
</div>
</drawer-detail>
</sidebar-detail>
</template>
</private-view>
</template>
@@ -547,10 +547,10 @@ export default defineComponent({
overflow: auto;
}
.layout-drawer {
--drawer-detail-icon-color: var(--warning);
--drawer-detail-color: var(--warning);
--drawer-detail-color-active: var(--warning);
.layout-sidebar {
--sidebar-detail-icon-color: var(--warning);
--sidebar-detail-color: var(--warning);
--sidebar-detail-color-active: var(--warning);
--v-form-vertical-gap: 24px;
}

View File

@@ -1,5 +1,5 @@
<template>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<dl v-if="parsedInfo">
<div>
<dt>{{ $t('directus_version') }}</dt>
@@ -34,7 +34,7 @@
<v-divider />
<div class="page-description" v-html="marked($t('page_help_settings_project'))" />
</drawer-detail>
</sidebar-detail>
</template>
<script lang="ts">

View File

@@ -21,8 +21,8 @@
<v-form :initial-values="initialValues" v-model="edits" :fields="fields" :primary-key="1" />
</div>
<template #drawer>
<project-info-drawer-detail />
<template #sidebar>
<project-info-sidebar-detail />
</template>
</private-view>
</template>
@@ -32,11 +32,11 @@ import { defineComponent, ref, computed } from '@vue/composition-api';
import SettingsNavigation from '../../components/navigation.vue';
import useCollection from '@/composables/use-collection';
import { useSettingsStore } from '@/stores';
import ProjectInfoDrawerDetail from './components/project-info-drawer-detail.vue';
import ProjectInfoSidebarDetail from './components/project-info-sidebar-detail.vue';
import { clone } from 'lodash';
export default defineComponent({
components: { SettingsNavigation, ProjectInfoDrawerDetail },
components: { SettingsNavigation, ProjectInfoSidebarDetail },
setup() {
const settingsStore = useSettingsStore();

View File

@@ -18,10 +18,10 @@
<settings-navigation />
</template>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_settings_roles_collection'))" />
</drawer-detail>
</sidebar-detail>
</template>
<div class="roles">

View File

@@ -136,15 +136,15 @@ export default defineComponent({
loading.value = true;
try {
const response = await api.get('/permissions', {
params: {
filter: {
role: {
_eq: props.role,
},
},
},
});
const params: any = { filter: { role: {} } };
if (props.role === null) {
params.filter.role = { _null: true };
} else {
params.filter.role = { _eq: props.role };
}
const response = await api.get('/permissions', params);
permissions.value = response.data.data;
} catch (err) {

View File

@@ -1,5 +1,5 @@
<template>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<dl v-if="!isNew && role">
<div>
<dt>{{ $t('primary_key') }}</dt>
@@ -10,7 +10,7 @@
<v-divider />
<div class="page-description" v-html="marked($t('page_help_settings_roles_item'))" />
</drawer-detail>
</sidebar-detail>
</template>
<script lang="ts">

View File

@@ -71,8 +71,8 @@
/>
</div>
<template #drawer>
<role-info-drawer-detail :role="item" />
<template #sidebar>
<role-info-sidebar-detail :role="item" />
<revisions-drawer-detail collection="directus_roles" :primary-key="primaryKey" />
</template>
</private-view>
@@ -86,7 +86,7 @@ import router from '@/router';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import useItem from '@/composables/use-item';
import { useUserStore } from '@/stores/';
import RoleInfoDrawerDetail from './components/role-info-drawer-detail.vue';
import RoleInfoSidebarDetail from './components/role-info-sidebar-detail.vue';
import PermissionsOverview from './components/permissions-overview.vue';
type Values = {
@@ -95,7 +95,7 @@ type Values = {
export default defineComponent({
name: 'roles-item',
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoDrawerDetail, PermissionsOverview },
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoSidebarDetail, PermissionsOverview },
props: {
primaryKey: {
type: String,

View File

@@ -1,6 +1,8 @@
<template>
<div class="actions">
<v-button @click="save" :loading="loading">{{ $t('save') }}</v-button>
<v-button @click="save" :loading="loading" icon rounded v-tooltip.bottom="$t('save')">
<v-icon name="check" />
</v-button>
</div>
</template>

View File

@@ -1,13 +1,14 @@
<template>
<div>
<v-modal-heading
:heading="
<v-notice type="info">
{{
$t('fields_for_role', {
role: role ? role.name : $t('public'),
action: $t(permission.action).toLowerCase(),
})
"
/>
}}
</v-notice>
<p class="type-label">{{ $tc('field', 0) }}</p>
<interface-checkboxes v-model="fields" type="json" :choices="fieldsInCollection" />
</div>
@@ -83,4 +84,8 @@ export default defineComponent({
.type-label {
margin-bottom: 8px;
}
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,13 +1,14 @@
<template>
<div>
<v-modal-heading
:heading="
<v-notice type="info">
{{
$t('permissions_for_role', {
action: $t(permission.action).toLowerCase(),
role: role ? role.name : $t('public'),
})
"
/>
}}
</v-notice>
<interface-code v-model="permissions" language="json" type="json" />
</div>
</template>
@@ -47,3 +48,9 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,13 +1,13 @@
<template>
<div>
<v-modal-heading
:heading="
<v-notice type="info">
{{
$t('presets_for_role', {
action: $t(permission.action).toLowerCase(),
role: role ? role.name : $t('public'),
})
"
/>
}}
</v-notice>
<interface-code v-model="presets" language="json" type="json" />
</div>
</template>
@@ -47,3 +47,9 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,13 +1,14 @@
<template>
<div>
<v-modal-heading
:heading="
<v-notice type="info">
{{
$t('validation_for_role', {
action: $t(permission.action).toLowerCase(),
role: role ? role.name : $t('public'),
})
"
/>
}}
</v-notice>
<interface-code v-model="validation" language="json" type="json" />
</div>
</template>
@@ -47,3 +48,9 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
.v-notice {
margin-bottom: 36px;
}
</style>

View File

@@ -1,20 +1,20 @@
<template>
<v-modal :title="modalTitle" :active="true" class="new-collection" persistent>
<v-drawer :title="modalTitle" :active="true" class="new-collection" persistent>
<template #sidebar v-if="!loading">
<tabs :current-tab.sync="currentTab" :tabs="tabs" />
</template>
<template v-if="!loading">
<div class="content" v-if="!loading">
<permissions v-if="currentTab[0] === 'permissions'" :permission.sync="permission" :role="role" />
<fields v-if="currentTab[0] === 'fields'" :permission.sync="permission" :role="role" />
<validation v-if="currentTab[0] === 'validation'" :permission.sync="permission" :role="role" />
<presets v-if="currentTab[0] === 'presets'" :permission.sync="permission" :role="role" />
</template>
</div>
<template #footer v-if="!loading">
<template #actions v-if="!loading">
<actions :role-key="roleKey" :permission="permission" @refresh="$emit('refresh', +permissionKey)" />
</template>
</v-modal>
</v-drawer>
</template>
<script lang="ts">
@@ -149,3 +149,11 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
.content {
padding: var(--content-padding);
padding-top: 0;
padding-bottom: var(--content-padding);
}
</style>

View File

@@ -85,12 +85,12 @@
</template>
</component>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_settings_webhooks_collection'))" />
</drawer-detail>
<layout-drawer-detail />
<portal-target name="drawer" />
</sidebar-detail>
<layout-sidebar-detail />
<portal-target name="sidebar" />
</template>
</private-view>
</template>
@@ -98,7 +98,7 @@
<script lang="ts">
import { defineComponent, computed, ref } from '@vue/composition-api';
import SettingsNavigation from '../../components/navigation.vue';
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
import marked from 'marked';
import { LayoutComponent } from '@/layouts/types';
import { usePreset } from '@/composables/use-preset';
@@ -112,7 +112,7 @@ type Item = {
export default defineComponent({
name: 'webhooks-collection',
components: { SettingsNavigation, LayoutDrawerDetail, SearchInput },
components: { SettingsNavigation, LayoutSidebarDetail, SearchInput },
setup(props) {
const layoutRef = ref<LayoutComponent | null>(null);

View File

@@ -58,10 +58,10 @@
v-model="edits"
/>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<div class="page-description" v-html="marked($t('page_help_settings_webhooks_item'))" />
</drawer-detail>
</sidebar-detail>
<revisions-drawer-detail v-if="isNew === false" collection="directus_webhooks" :primary-key="primaryKey" />
</template>
</private-view>

View File

@@ -1,5 +1,5 @@
<template>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<dl v-if="isNew === false && user">
<div v-if="user.id">
<dt>{{ $t('key') }}</dt>
@@ -32,7 +32,7 @@
<v-divider />
<div class="page-description" v-html="marked($t('page_help_users_item'))" />
</drawer-detail>
</sidebar-detail>
</template>
<script lang="ts">

View File

@@ -91,12 +91,12 @@
</template>
</component>
<template #drawer>
<drawer-detail icon="info_outline" :title="$t('information')" close>
<template #sidebar>
<sidebar-detail icon="info_outline" :title="$t('information')" close>
<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" />
</sidebar-detail>
<layout-sidebar-detail @input="layout = $event" :value="layout" />
<portal-target name="sidebar" />
</template>
</private-view>
</template>
@@ -109,7 +109,7 @@ import { i18n } from '@/lang';
import api from '@/api';
import { LayoutComponent } from '@/layouts/types';
import usePreset from '@/composables/use-preset';
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
import SearchInput from '@/views/private/components/search-input';
import marked from 'marked';
import useNavigation from '../composables/use-navigation';
@@ -120,7 +120,7 @@ type Item = {
export default defineComponent({
name: 'users-collection',
components: { UsersNavigation, LayoutDrawerDetail, SearchInput },
components: { UsersNavigation, LayoutSidebarDetail, SearchInput },
props: {
queryFilters: {
type: Object as PropType<Record<string, string>>,

View File

@@ -143,15 +143,15 @@
</v-card>
</v-dialog>
<template #drawer>
<user-info-drawer-detail :is-new="isNew" :user="item" />
<template #sidebar>
<user-info-sidebar-detail :is-new="isNew" :user="item" />
<revisions-drawer-detail
v-if="isBatch === false && isNew === false"
collection="directus_users"
:primary-key="primaryKey"
ref="revisionsDrawerDetail"
/>
<comments-drawer-detail
<comments-sidebar-detail
v-if="isBatch === false && isNew === false"
collection="directus_users"
:primary-key="primaryKey"
@@ -167,14 +167,14 @@ import UsersNavigation from '../components/navigation.vue';
import { i18n } from '@/lang';
import router from '@/router';
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
import CommentsDrawerDetail from '@/views/private/components/comments-drawer-detail';
import CommentsSidebarDetail from '@/views/private/components/comments-sidebar-detail';
import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
import api from '@/api';
import { useFieldsStore, useUserStore } from '@/stores/';
import useFormFields from '@/composables/use-form-fields';
import { Field } from '@/types';
import UserInfoDrawerDetail from '../components/user-info-drawer-detail.vue';
import UserInfoSidebarDetail from '../components/user-info-sidebar-detail.vue';
import { getRootPath } from '@/utils/get-root-path';
import useShortcut from '@/composables/use-shortcut';
import { isAllowed } from '@/utils/is-allowed';
@@ -198,7 +198,7 @@ export default defineComponent({
return next();
},
components: { UsersNavigation, RevisionsDrawerDetail, SaveOptions, CommentsDrawerDetail, UserInfoDrawerDetail },
components: { UsersNavigation, RevisionsDrawerDetail, SaveOptions, CommentsSidebarDetail, UserInfoSidebarDetail },
props: {
primaryKey: {
type: String,

View File

@@ -3,7 +3,7 @@ import { createStore } from 'pinia';
export const useAppStore = createStore({
id: 'appStore',
state: () => ({
drawerOpen: false,
sidebarOpen: false,
hydrated: false,
hydrating: false,
error: null,

View File

@@ -7,7 +7,7 @@
font-family: var(--family-sans-serif);
font-style: normal;
line-height: 29px;
letter-spacing: -0.8px;
letter-spacing: 0;
@include breakpoint(small) {
font-size: 24px;

View File

@@ -1,4 +0,0 @@
import CommentsDrawerDetail from './comments-drawer-detail.vue';
export { CommentsDrawerDetail };
export default CommentsDrawerDetail;

View File

@@ -1,9 +0,0 @@
# Comments Drawer
Renders an comment timeline in a drawer section meant to be used in the drawer sidebar.
## Usage
```html
<comments-drawer-detail collection="authors" primary-key="15" />
```

View File

@@ -28,7 +28,7 @@
<template #activator="{ toggle, active }">
<v-icon class="more" :class="{ active }" name="more_horiz" @click="toggle" />
<div class="time">
<span class="dot" v-if="activity.revisions.length > 0" v-tooltip="editedOnFormatted" />
<span class="dot" v-tooltip="editedOnFormatted" />
{{ formattedTime }}
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<drawer-detail :title="$t('comments')" icon="chat_bubble_outline" :badge="count || null">
<sidebar-detail :title="$t('comments')" icon="chat_bubble_outline" :badge="count || null">
<comment-input :refresh="refresh" :collection="collection" :primary-key="primaryKey" />
<v-progress-linear indeterminate v-if="loading" />
@@ -15,7 +15,7 @@
<comment-item :refresh="refresh" :activity="item" :key="item.id" />
</template>
</template>
</drawer-detail>
</sidebar-detail>
</template>
<script lang="ts">
@@ -138,7 +138,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.drawer-detail {
.sidebar-detail {
--v-badge-background-color: var(--foreground-normal);
}

View File

@@ -0,0 +1,4 @@
import CommentsSidebarDetail from './comments-sidebar-detail.vue';
export { CommentsSidebarDetail };
export default CommentsSidebarDetail;

View File

@@ -0,0 +1,9 @@
# Comments Sidebar
Renders an comment timeline in a sidebar section meant to be used in the sidebar sidebar.
## Usage
```html
<comments-sidebar-detail collection="authors" primary-key="15" />
```

View File

@@ -1,4 +0,0 @@
import DrawerButton from './drawer-button.vue';
export { DrawerButton };
export default DrawerButton;

View File

@@ -1,5 +1,5 @@
<template>
<v-modal v-model="_active" :title="$t('select_item')" no-padding @esc="cancel">
<v-drawer v-model="_active" :title="$t('select_item')" @cancel="cancel">
<component
:is="`layout-${localLayout}`"
:collection="collection"
@@ -20,11 +20,12 @@
</template>
</component>
<template #footer>
<v-button @click="cancel" secondary>{{ $t('cancel') }}</v-button>
<v-button @click="save">{{ $t('save') }}</v-button>
<template #actions>
<v-button @click="save" icon rounded v-tooltip.bottom="$t('save')">
<v-icon name="check" />
</v-button>
</template>
</v-modal>
</v-drawer>
</template>
<script lang="ts">

Some files were not shown because too many files have changed in this diff Show More