Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-06-28 19:18:53 -04:00
73 changed files with 26773 additions and 22322 deletions

View File

@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
<!--
Hi, thank you for taking the time to create an issue! Before continuing, you must first have completed all Troubleshooting Steps:
https://docs.directus.io/getting-started/support/#troubleshooting-steps
If the above steps do not resolve your issue, please complete the following:
1) The issue and what you expected to happen
The _ _ does _ _ when _ _ while it should _ _
2) Exact steps to reproduce this issue
Click this, tap that, see error _ _
3) Your environment:
What version of Directus you are using.
Which DBMS are you using (MySQL 8, Postgres 12, ...).
Which deployment are you using (npx, Docker, ...).
What browser are you using (Chrome 87, Safari 14, ...).
4) Any other relevant information we might need to reproduce this issue
A SQL dump of the setup.
What third party services you rely on (S3, managed database, ...).
-->

View File

@@ -10,7 +10,19 @@ body:
value: 'Before continuing, you must first have completed all [Troubleshooting Steps](https://docs.directus.io/getting-started/support/#troubleshooting-steps)'
- type: markdown
attributes:
value: Please double check if an issue describing this problem doesn't exist already.
value: Please confirm that an issue describing this problem doesn't exist already.
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior. Contributors should be able to follow the steps provided in order to reproduce the bug.
validations:
required: true
- type: input
attributes:
label: What version of Directus are you using?
@@ -47,15 +59,3 @@ body:
description: 'For example: running locally, Docker, PaaS'
validations:
required: true
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior. Contributors should be able to follow the steps provided in order to reproduce the bug.
validations:
required: true

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env node
return require('./dist/cli/index.js');
require('./dist/cli/index.js');

View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"license": "GPL-3.0-only",
"homepage": "https://github.com/directus/directus#readme",
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
@@ -69,15 +69,15 @@
"example.env"
],
"dependencies": {
"@directus/app": "9.0.0-rc.80",
"@directus/drive": "9.0.0-rc.80",
"@directus/drive-azure": "9.0.0-rc.80",
"@directus/drive-gcs": "9.0.0-rc.80",
"@directus/drive-s3": "9.0.0-rc.80",
"@directus/format-title": "9.0.0-rc.80",
"@directus/schema": "9.0.0-rc.80",
"@directus/shared": "9.0.0-rc.80",
"@directus/specs": "9.0.0-rc.80",
"@directus/app": "9.0.0-rc.81",
"@directus/drive": "9.0.0-rc.81",
"@directus/drive-azure": "9.0.0-rc.81",
"@directus/drive-gcs": "9.0.0-rc.81",
"@directus/drive-s3": "9.0.0-rc.81",
"@directus/format-title": "9.0.0-rc.81",
"@directus/schema": "9.0.0-rc.81",
"@directus/shared": "9.0.0-rc.81",
"@directus/specs": "9.0.0-rc.81",
"@godaddy/terminus": "^4.9.0",
"@rollup/plugin-alias": "^3.1.2",
"@rollup/plugin-virtual": "^2.0.3",
@@ -89,7 +89,7 @@
"busboy": "^0.3.1",
"camelcase": "^6.2.0",
"chalk": "^4.1.1",
"commander": "^7.2.0",
"commander": "^8.0.0",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"csv-parser": "^3.0.0",

View File

@@ -2,7 +2,7 @@
/* eslint-disable no-console */
import program from 'commander';
import { program } from 'commander';
import start from '../start';
import bootstrap from './commands/bootstrap';
import count from './commands/count';

View File

@@ -1,5 +1,6 @@
import { Knex } from 'knex';
import logger from '../../logger';
import SchemaInspector from 'knex-schema-inspector';
/**
* Things to keep in mind:
@@ -80,11 +81,23 @@ const updates = [
];
export async function up(knex: Knex): Promise<void> {
const inspector = SchemaInspector(knex);
const foreignKeys = await inspector.foreignKeys();
for (const update of updates) {
for (const constraint of update.constraints) {
const existingForeignKey = foreignKeys.find(
(fk) =>
fk.table === update.table &&
fk.column === constraint.column &&
fk.foreign_key_table === constraint.references.split('.')[0] &&
fk.foreign_key_column === constraint.references.split('.')[1]
);
try {
await knex.schema.alterTable(update.table, (table) => {
table.dropForeign([constraint.column]);
table.dropForeign([constraint.column], existingForeignKey?.constraint_name || undefined);
});
} catch (err) {
logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`);

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_files', (table) => {
table.bigInteger('filesize').nullable().defaultTo(null).alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_files', (table) => {
table.integer('filesize').nullable().defaultTo(null).alter();
});
}

View File

@@ -81,6 +81,7 @@
- id
- first_name
- last_name
- last_page
- email
- password
- location

View File

@@ -47,7 +47,7 @@ const defaults: Record<string, any> = {
CACHE_ENABLED: false,
CACHE_STORE: 'memory',
CACHE_TTL: '10m',
CACHE_TTL: '5m',
CACHE_NAMESPACE: 'system-cache',
CACHE_AUTO_PURGE: false,
CACHE_CONTROL_S_MAXAGE: '0',
@@ -65,7 +65,7 @@ const defaults: Record<string, any> = {
TELEMETRY: true,
ASSETS_CACHE_TTL: '30m',
ASSETS_TRANSFORM_MAX_CONCURRENT: 4,
ASSETS_TRANSFORM_MAX_CONCURRENT: 1,
ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION: 6000,
};

View File

@@ -2,7 +2,7 @@ import express, { Router } from 'express';
import path from 'path';
import { AppExtensionType, Extension, ExtensionType } from '@directus/shared/types';
import {
ensureExtensionsDirs,
ensureExtensionDirs,
generateExtensionsEntry,
getLocalExtensions,
getPackageExtensions,
@@ -31,7 +31,7 @@ let extensions: Extension[] = [];
let extensionBundles: Partial<Record<AppExtensionType, string>> = {};
export async function initializeExtensions(): Promise<void> {
await ensureExtensionsDirs(env.EXTENSIONS_PATH);
await ensureExtensionDirs(env.EXTENSIONS_PATH);
extensions = await getExtensions();
if (!('DIRECTUS_DEV' in process.env)) {
@@ -88,9 +88,10 @@ async function generateExtensionBundles() {
const bundle = await rollup({
input: 'entry',
external: Object.values(sharedDepsMapping),
makeAbsoluteExternalsRelative: false,
plugins: [virtual({ entry }), alias({ entries: internalImports })],
});
const { output } = await bundle.generate({ format: 'es' });
const { output } = await bundle.generate({ format: 'es', compact: true });
bundles[extensionType] = output[0].code;
@@ -102,13 +103,14 @@ async function generateExtensionBundles() {
async function getSharedDepsMapping(deps: string[]) {
const appDir = await fse.readdir(path.join(resolvePackage('@directus/app'), 'dist'));
const adminUrl = env.PUBLIC_URL.endsWith('/') ? env.PUBLIC_URL + 'admin' : env.PUBLIC_URL + '/admin';
const depsMapping: Record<string, string> = {};
for (const dep of deps) {
const depName = appDir.find((file) => dep.replace(/\//g, '_') === file.substring(0, file.indexOf('.')));
if (depName) {
depsMapping[dep] = `${env.PUBLIC_URL}/admin/${depName}`;
depsMapping[dep] = `${adminUrl}/${depName}`;
} else {
logger.warn(`Couldn't find shared extension dependency "${dep}"`);
}

View File

@@ -67,12 +67,7 @@ export class MailService {
html = prettier.format(html as string, { parser: 'html', printWidth: 70, tabWidth: 0 });
}
try {
await this.mailer.sendMail({ ...emailOptions, from, html });
} catch (error) {
logger.warn('[Email] Unexpected error while sending an email:');
logger.warn(error);
}
await this.mailer.sendMail({ ...emailOptions, from, html });
}
private async renderTemplate(template: string, variables: Record<string, any>) {

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/app",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"private": false,
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
"author": "Rijk van Zanten <rijkvanzanten@me.com>",
@@ -28,10 +28,10 @@
},
"gitHead": "24621f3934dc77eb23441331040ed13c676ceffd",
"devDependencies": {
"@directus/docs": "9.0.0-rc.80",
"@directus/extension-sdk": "9.0.0-rc.80",
"@directus/format-title": "9.0.0-rc.80",
"@directus/shared": "9.0.0-rc.80",
"@directus/docs": "9.0.0-rc.81",
"@directus/extension-sdk": "9.0.0-rc.81",
"@directus/format-title": "9.0.0-rc.81",
"@directus/shared": "9.0.0-rc.81",
"@fullcalendar/core": "5.8.0",
"@fullcalendar/daygrid": "5.8.0",
"@fullcalendar/interaction": "5.8.0",
@@ -53,13 +53,13 @@
"@types/mime-types": "2.1.0",
"@types/ms": "0.7.31",
"@types/qrcode": "1.4.0",
"@vitejs/plugin-vue": "1.2.3",
"@vitejs/plugin-vue": "1.2.4",
"@vue/cli-plugin-babel": "4.5.13",
"@vue/cli-plugin-router": "4.5.13",
"@vue/cli-plugin-typescript": "4.5.13",
"@vue/cli-plugin-vuex": "4.5.13",
"@vue/cli-service": "4.5.13",
"@vue/compiler-sfc": "3.1.1",
"@vue/compiler-sfc": "3.1.2",
"axios": "0.21.1",
"base-64": "1.0.0",
"codemirror": "5.62.0",
@@ -71,13 +71,13 @@
"front-matter": "4.0.2",
"html-entities": "2.3.2",
"jsonlint-mod": "1.7.6",
"marked": "2.1.2",
"marked": "2.1.3",
"micromustache": "8.0.3",
"mime": "2.5.2",
"mitt": "2.1.0",
"mitt": "3.0.0",
"nanoid": "3.1.23",
"pinia": "2.0.0-beta.3",
"prettier": "2.3.1",
"prettier": "2.3.2",
"pretty-ms": "7.0.1",
"qrcode": "1.4.4",
"rimraf": "3.0.2",
@@ -85,7 +85,7 @@
"tinymce": "5.8.2",
"typescript": "4.3.4",
"vite": "2.3.8",
"vue": "3.1.1",
"vue": "3.1.2",
"vue-i18n": "9.1.6",
"vue-router": "4.0.10",
"vuedraggable": "4.0.3"

View File

@@ -38,15 +38,16 @@ export const onRequest = (config: AxiosRequestConfig): RequestConfig => {
export const onResponse = (response: AxiosResponse | Response): AxiosResponse | Response => {
const requestsStore = useRequestsStore();
const id = (response.config as RequestConfig).id;
requestsStore.endRequest(id);
const id = (response.config as RequestConfig)?.id;
if (id) requestsStore.endRequest(id);
return response;
};
export const onError = async (error: RequestError): Promise<RequestError> => {
const requestsStore = useRequestsStore();
const id = (error.response.config as RequestConfig).id;
requestsStore.endRequest(id);
const id = (error.response?.config as RequestConfig)?.id;
if (id) requestsStore.endRequest(id);
// If a request fails with the unauthorized error, it either means that your user doesn't have
// access, or that your session doesn't exist / has expired.

View File

@@ -1,9 +1,11 @@
<template>
<div class="v-detail" :class="{ disabled }">
<v-divider @click="internalActive = !internalActive">
<v-icon v-if="!disabled" :name="internalActive ? 'unfold_less' : 'unfold_more'" small />
<slot name="title">{{ label }}</slot>
</v-divider>
<slot name="activator" v-bind="{ active: internalActive, enable, disable, toggle }">
<v-divider @click="internalActive = !internalActive">
<v-icon v-if="!disabled" :name="internalActive ? 'unfold_less' : 'unfold_more'" small />
<slot name="title">{{ label }}</slot>
</v-divider>
</slot>
<transition-expand>
<div v-if="internalActive">
<slot />
@@ -39,6 +41,7 @@ export default defineComponent({
setup(props, { emit }) {
const localActive = ref(props.startOpen);
const internalActive = computed({
get() {
if (props.modelValue !== undefined) {
@@ -52,7 +55,19 @@ export default defineComponent({
},
});
return { internalActive };
return { internalActive, enable, disable, toggle };
function enable() {
internalActive.value = true;
}
function disable() {
internalActive.value = false;
}
function toggle() {
internalActive.value = !internalActive.value;
}
},
});
</script>

View File

@@ -57,7 +57,6 @@ body {
span.wrapper {
display: flex;
margin-right: 16px;
color: var(--v-divider-label-color);
:slotted(.v-icon) {
@@ -67,6 +66,7 @@ body {
}
.type-text {
width: 100%;
color: var(--v-divider-label-color);
font-weight: 600;
transition: color var(--fast) var(--transition);

View File

@@ -70,7 +70,7 @@ import { md } from '@/utils/md';
import FormFieldLabel from './form-field-label.vue';
import FormFieldMenu from './form-field-menu.vue';
import FormFieldInterface from './form-field-interface.vue';
import { ValidationError } from './types';
import { ValidationError } from '@/types';
import { getJSType } from '@/utils/get-js-type';
import { isEqual } from 'lodash';

View File

@@ -1,5 +1,4 @@
import { Field } from '@/types';
import { FilterOperator } from '@directus/shared/types';
export type FormField = DeepPartial<Field> & {
field: string;
@@ -7,12 +6,3 @@ export type FormField = DeepPartial<Field> & {
hideLabel?: boolean;
hideLoader?: boolean;
};
export type ValidationError = {
code: string;
field: string;
type: FilterOperator;
valid?: number | string | (number | string)[];
invalid?: number | string | (number | string)[];
substring?: string;
};

View File

@@ -17,23 +17,44 @@
</div>
</v-notice>
<form-field
v-for="(field, index) in formFields"
:field="field"
:autofocus="index === firstEditableFieldIndex && autofocus"
:key="field.field"
:model-value="(modelValue || {})[field.field]"
:initial-value="(initialValues || {})[field.field]"
:disabled="disabled"
:batch-mode="batchMode"
:batch-active="batchActiveFields.includes(field.field)"
:primary-key="primaryKey"
:loading="loading"
:validation-error="validationErrors.find((err) => err.field === field.field)"
@update:model-value="setValue(field, $event)"
@unset="unsetValue(field)"
@toggle-batch="toggleBatchField(field)"
/>
<template v-for="(field, index) in formFields">
<component
v-if="field.meta?.special?.includes('group')"
:class="field.meta?.width || 'full'"
:is="`interface-${field.meta?.interface || 'group-raw'}`"
:key="field.field"
:field="field"
:fields="getFieldsForGroup(field.meta.id)"
:values="values || {}"
:initial-values="initialValues || {}"
:disabled="disabled"
:batch-mode="batchMode"
:batch-active-fields="batchActiveFields"
:primary-key="primaryKey"
:loading="loading"
:validation-errors="validationErrors"
v-bind="field.meta?.options || {}"
@apply="apply"
/>
<form-field
v-else
:key="field.field"
:field="field"
:autofocus="index === firstEditableFieldIndex && autofocus"
:model-value="(values || {})[field.field]"
:initial-value="(initialValues || {})[field.field]"
:disabled="disabled"
:batch-mode="batchMode"
:batch-active="batchActiveFields.includes(field.field)"
:primary-key="primaryKey"
:loading="loading"
:validation-error="validationErrors.find((err) => err.field === field.field)"
@update:model-value="setValue(field, $event)"
@unset="unsetValue(field)"
@toggle-batch="toggleBatchField(field)"
/>
</template>
</div>
</template>
@@ -42,21 +63,21 @@ import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, ref, provide } from 'vue';
import { useFieldsStore } from '@/stores/';
import { Field, FieldRaw } from '@/types';
import { useElementSize } from '@/composables/use-element-size';
import { clone, cloneDeep } from 'lodash';
import { clone, cloneDeep, isNil, merge, omit } from 'lodash';
import { md } from '@/utils/md';
import FormField from './form-field.vue';
import useFormFields from '@/composables/use-form-fields';
import { ValidationError } from './types';
import { translate } from '@/utils/translate-object-values';
import { ValidationError } from '@/types';
import { useElementSize } from '@/composables/use-element-size';
import FormField from './form-field.vue';
type FieldValues = {
[field: string]: any;
};
export default defineComponent({
emits: ['update:modelValue'],
name: 'v-form',
components: { FormField },
emits: ['update:modelValue'],
props: {
collection: {
type: String,
@@ -99,18 +120,35 @@ export default defineComponent({
type: Boolean,
default: false,
},
group: {
type: Number,
default: null,
},
},
setup(props, { emit }) {
const { t } = useI18n();
const el = ref<Element>();
const fieldsStore = useFieldsStore();
const values = computed(() => {
return Object.assign({}, props.initialValues, props.modelValue);
});
const { formFields, gridClass } = useForm();
const el = ref<Element>();
const { width } = useElementSize(el);
const gridClass = computed<string | null>(() => {
if (el.value === null) return null;
if (width.value > 792) {
return 'grid with-fill';
} else {
return 'grid';
}
});
const { formFields, getFieldsForGroup } = useForm();
const { toggleBatchField, batchActiveFields } = useBatch();
const firstEditableFieldIndex = computed(() => {
@@ -136,9 +174,7 @@ export default defineComponent({
return {
t,
el,
formFields,
gridClass,
values,
setValue,
batchActiveFields,
@@ -147,6 +183,12 @@ export default defineComponent({
md,
unknownValidationErrors,
firstEditableFieldIndex,
isNil,
apply,
el,
gridClass,
omit,
getFieldsForGroup,
};
function useForm() {
@@ -165,36 +207,30 @@ export default defineComponent({
const { formFields } = useFormFields(fields);
const formFieldsParsed = computed(() => {
return translate(
formFields.value.map((field: Field) => {
if (
field.schema?.has_auto_increment === true ||
(field.schema?.is_primary_key === true && props.primaryKey !== '+')
) {
const fieldClone = cloneDeep(field) as any;
if (!fieldClone.meta) fieldClone.meta = {};
fieldClone.meta.readonly = true;
return fieldClone;
}
const blockPrimaryKey = (field: Field) => {
if (
field.schema?.has_auto_increment === true ||
(field.schema?.is_primary_key === true && props.primaryKey !== '+')
) {
const fieldClone = cloneDeep(field) as any;
if (!fieldClone.meta) fieldClone.meta = {};
fieldClone.meta.readonly = true;
return fieldClone;
}
return field;
})
);
return field;
};
return formFields.value.map((field) => blockPrimaryKey(field));
});
const { width } = useElementSize(el);
const formFieldsInGroup = computed(() =>
formFieldsParsed.value.filter(
(field) => field.meta?.group === props.group || (props.group === null && isNil(field.meta?.group))
)
);
const gridClass = computed<string | null>(() => {
if (el.value === null) return null;
if (width.value > 792) {
return 'grid with-fill';
} else {
return 'grid';
}
});
return { formFields: formFieldsParsed, gridClass, isDisabled };
return { formFields: formFieldsInGroup, isDisabled, getFieldsForGroup };
function isDisabled(field: Field) {
return (
@@ -204,6 +240,20 @@ export default defineComponent({
(props.batchMode && batchActiveFields.value.includes(field.field) === false)
);
}
function getFieldsForGroup(group: null | number): Field[] {
const fieldsInGroup: Field[] = formFieldsParsed.value.filter(
(field) => field.meta?.group === group || (group === null && isNil(field.meta))
);
for (const field of fieldsInGroup) {
if (field.meta?.special?.includes('group')) {
fieldsInGroup.push(...getFieldsForGroup(field.meta!.id));
}
}
return fieldsInGroup;
}
}
function setValue(field: Field, value: any) {
@@ -212,6 +262,10 @@ export default defineComponent({
emit('update:modelValue', edits);
}
function apply(updates: { [field: string]: any }) {
emit('update:modelValue', merge({}, props.modelValue, updates));
}
function unsetValue(field: Field) {
if (field.field in (props.modelValue || {})) {
const newEdits = { ...props.modelValue };

View File

@@ -7,6 +7,7 @@ import { Field } from '@/types';
import { getDefaultInterfaceForType } from '@/utils/get-default-interface-for-type';
import { clone, orderBy } from 'lodash';
import { computed, ComputedRef, Ref } from 'vue';
import { translate } from '@/utils/translate-object-values';
export default function useFormFields(fields: Ref<Field[]>): { formFields: ComputedRef<Field[]> } {
const { interfaces } = getInterfaces();
@@ -60,6 +61,8 @@ export default function useFormFields(fields: Ref<Field[]>): { formFields: Compu
formFields = orderBy(formFields, 'meta.sort');
formFields = translate(formFields);
return formFields;
});

View File

@@ -0,0 +1,124 @@
<template>
<v-detail>
<template #activator="{ toggle, active }">
<v-divider
:class="{ margin: icon || title, active }"
:style="{
'--v-divider-label-color': color,
}"
large
:inline-title="inlineTitle"
@click="toggle"
>
<template v-if="icon" #icon><v-icon :name="icon" /></template>
<template v-if="title">
<div class="title">
<span class="name">{{ title }}</span>
<v-icon :name="active ? 'unfold_less' : 'unfold_more'" />
</div>
</template>
</v-divider>
</template>
<v-form
:initial-values="initialValues"
:fields="fields"
:model-value="values"
:primary-key="primaryKey"
:group="field.meta.id"
:validation-errors="validationErrors"
:loading="loading"
@update:model-value="$emit('apply', $event)"
/>
</v-detail>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { Field, ValidationError } from '@/types';
export default defineComponent({
props: {
color: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
inlineTitle: {
type: Boolean,
default: false,
},
field: {
type: Object as PropType<Field>,
required: true,
},
fields: {
type: Array as PropType<Field[]>,
required: true,
},
values: {
type: Object as PropType<Record<string, unknown>>,
required: true,
},
initialValues: {
type: Object as PropType<Record<string, unknown>>,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
batchMode: {
type: Boolean,
default: false,
},
batchActiveFields: {
type: Array as PropType<string[]>,
default: () => [],
},
primaryKey: {
type: [Number, String],
required: true,
},
loading: {
type: Boolean,
default: false,
},
validationErrors: {
type: Array as PropType<ValidationError[]>,
default: () => [],
},
},
});
</script>
<style scoped>
.margin {
margin-top: 20px;
}
.title {
display: flex;
align-items: center;
}
.name {
flex-grow: 1;
}
.v-divider {
cursor: pointer;
}
.v-form {
padding-top: var(--form-vertical-gap);
}
</style>

View File

@@ -0,0 +1,76 @@
import { defineInterface } from '@/interfaces/define';
import InterfaceGroupDivider from './group-divider.vue';
export default defineInterface({
id: 'group-divider',
name: '$t:interfaces.presentation-divider.divider',
description: '$t:interfaces.presentation-divider.description',
icon: 'remove',
component: InterfaceGroupDivider,
hideLabel: true,
hideLoader: true,
types: ['alias'],
groups: ['group'],
options: [
{
field: 'color',
name: '$t:color',
type: 'string',
meta: {
width: 'half',
interface: 'select-color',
},
},
{
field: 'icon',
name: '$t:icon',
type: 'string',
meta: {
width: 'half',
interface: 'select-icon',
},
},
{
field: 'title',
name: '$t:title',
type: 'string',
meta: {
width: 'full',
interface: 'input',
options: {
placeholder: '$t:interfaces.presentation-divider.title_placeholder',
},
},
},
{
field: 'marginTop',
name: '$t:interfaces.presentation-divider.margin_top',
type: 'boolean',
meta: {
width: 'half',
interface: 'boolean',
options: {
label: '$t:interfaces.presentation-divider.margin_top_label',
},
},
schema: {
default_value: false,
},
},
{
field: 'inlineTitle',
name: '$t:interfaces.presentation-divider.inline_title',
type: 'boolean',
meta: {
width: 'half',
interface: 'boolean',
options: {
label: '$t:interfaces.presentation-divider.inline_title_label',
},
},
schema: {
default_value: false,
},
},
],
});

View File

@@ -0,0 +1,67 @@
<template>
<div class="group-raw">
<v-form
:initial-values="initialValues"
:fields="fields"
:model-value="values"
:primary-key="primaryKey"
:group="field.meta.id"
:validation-errors="validationErrors"
:loading="loading"
@update:model-value="$emit('apply', $event)"
/>
</div>
</template>
<script lang="ts">
import { Field } from '@/types';
import { defineComponent, PropType } from 'vue';
import { ValidationError } from '@/types';
export default defineComponent({
emits: ['apply'],
name: 'interface-group-raw',
props: {
field: {
type: Object as PropType<Field>,
required: true,
},
fields: {
type: Array as PropType<Field[]>,
required: true,
},
values: {
type: Object as PropType<Record<string, unknown>>,
required: true,
},
initialValues: {
type: Object as PropType<Record<string, unknown>>,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
batchMode: {
type: Boolean,
default: false,
},
batchActiveFields: {
type: Array as PropType<string[]>,
default: () => [],
},
primaryKey: {
type: [Number, String],
required: true,
},
loading: {
type: Boolean,
default: false,
},
validationErrors: {
type: Array as PropType<ValidationError[]>,
default: () => [],
},
},
});
</script>

View File

@@ -0,0 +1,13 @@
import { defineInterface } from '../define';
import InterfaceGroupRaw from './group-raw.vue';
export default defineInterface({
id: 'group-raw',
name: '$t:interfaces.group-raw.name',
description: '$t:interfaces.group-raw.description',
icon: 'view_in_ar',
component: InterfaceGroupRaw,
groups: ['group'],
types: ['alias'],
options: [],
});

View File

@@ -179,10 +179,6 @@ export default defineComponent({
}
}
.flip-list-move {
transition: transform 0.5s;
}
.ghost .preview {
background-color: var(--primary-alt);
box-shadow: 0 !important;

View File

@@ -16,7 +16,7 @@
</div>
<div class="field half">
<p class="type-label">{{ $t('translations_display_template') }}</p>
<p class="type-label">{{ t('translations_display_template') }}</p>
<v-field-template
:collection="translationsCollection"
v-model="translationsTemplate"

View File

@@ -9,6 +9,10 @@ field_name_translations: ترجمة اسم الحقل
enter_password_to_enable_tfa: أدخل كلمة المرور الخاصة بك لتمكين المصادقة الثنائية
add_field: إضافة حقل
role_name: إسم الدور
branch: فرع
indeterminate: غير محدد
exclusive: حصري
children: اطفال
db_only_click_to_configure: 'فقط قاعدة البيانات: انقر لتعديل الإعدادات '
show_archived_items: أظهر العناصر المؤرشفة
edited: تم تعديل القيمة
@@ -238,14 +242,23 @@ export_data: تصدير البيانات
format: التنسيق
use_current_filters_settings: استخدام التصفية و الإعدادات الحالية
export_collection: 'تصدير {collection}'
last_page: آخر صفحة
last_access: آخر وصول
fill_template: املأ بقيمة النموذج
a_unique_table_name: اسم جدول فريد...
a_unique_column_name: اسم عمود فريد...
enable_custom_values: تمكين القيم المخصصة
submit: إرسال
move_to_folder: أنقل إلى مجلد
move: نقل
system: نظام
add_field_related: إضافة حقل إلى مجموعة ذات صلة
interface: الواجهة
today: اليوم
yesterday: أمس
delete_comment: احذف التعليق
date-fns_time: 'ساعات: دقائق: ثواني'
date-fns_time_no_seconds: 'س:دد ص'
month: شهر
year: سنة
select_all: تحديد الكل
@@ -263,6 +276,7 @@ months:
november: نوفمبر
december: ديسمبر
drag_mode: وضع السحب
original: أصلي
url: الرابط
import: إستيراد
file_details: تفاصيل الملف
@@ -278,6 +292,7 @@ zoom: تكبير/تصغير
download: تنزيل
open: فتح
open_in_new_window: فتح في نافذة جديدة
background_color: لون الخلفية
upload_from_device: استبدال الملف من الجهاز
choose_from_library: اختر ملف من المكتبة
import_from_url: استيراد ملف من رابط
@@ -383,6 +398,7 @@ errors:
INVALID_CREDENTIALS: اسم المستخدم و / أو كلمة المرور خاطئة
INVALID_OTP: كلمة مرور لمرة واحدة خاطئة
INVALID_PAYLOAD: Invalid payload
INVALID_QUERY: طلب غير صحيح
ITEM_NOT_FOUND: لم يتم العثور على العنصر
ROUTE_NOT_FOUND: غير موجود
USER_SUSPENDED: المستخدم موقوف
@@ -402,6 +418,8 @@ back: رجوع
editing_image: تحرير صورة
square: مربع
free: حر
flip_horizontal: اقلب أفقيا
flip_vertical: اقلب عموديا
aspect_ratio: نسبة العرض إلى الارتفاع
rotate: تدوير
all_users: كل المستخدمين
@@ -434,6 +452,7 @@ empty_item: عنصر فارغ
log_in_with: 'تسجيل الدخول باستخدام {provider}'
advanced_filter: تصفية متقدمة
delete_advanced_filter: حذف التصفية
change_advanced_filter_operator: تغيير المشغل
operators:
eq: يساوي
neq: لا يساوي
@@ -446,6 +465,7 @@ operators:
nnull: باطل
contains: يحتوي على
ncontains: لا يحتوي
starts_with: إبدا بـ
between: بين
nbetween: بين
empty: فارغ
@@ -500,12 +520,21 @@ read: قراءة
update: تحديث
select_fields: حدد حقول
format_text: تنسيق النص
image_url: رابط الصورة
media: وسائط الإعلام
width: العرض
height: الارتفاع
source: مصدر
url_placeholder: أدخل عنوان الرابط
display_text: عرض النص
unlimited: غير محدود
open_link_in: افتح الرابط في
wysiwyg_options:
alignleft: محاذاة إلى اليسار
alignright: محاذاة إلى اليمين
backcolor: لون الخلفية
italic: خط مائل
underline: مسطر
codeblock: نص برمجي
link: إضافة/تحرير الرابط
unlink: إزالة الرابط
@@ -653,6 +682,8 @@ fields:
tfa_secret: التحقق بخطوتين
status: حالة
role: الدور
last_page: آخر صفحة
last_access: آخر وصول
directus_settings:
project_name: اسم المشروع
project_url: رابط المشروع
@@ -814,6 +845,8 @@ interfaces:
file-image:
image: صورة
description: حدد أو ارفع صورة
system-interface:
interface: الواجهة
select-dropdown-m2o:
description: حدد عنصر واحد ذو صلة
display_template: عرض القالب

View File

@@ -5,17 +5,22 @@ duplicate_field: Дублиране на поле
half_width: Половин ширина
full_width: Пълна ширина
fill_width: Изпълване по ширина
field_name_translations: Превод на името на полето
field_name_translations: Превод за името на полето
enter_password_to_enable_tfa: Въвеждане на парола за активиране на двуфакторната автентикация
add_field: Добавяне на поле
role_name: Име на роля
db_only_click_to_configure: 'Само в базата данни: Кликнете за конфигурация '
branch: Клон
leaf: Листо
indeterminate: Неопределен
exclusive: Изключително
children: Дъщерен
db_only_click_to_configure: 'Само в базата от данни: Кликнете за конфигурация '
show_archived_items: Включително архивираните
edited: Стойността е редактирана
required: Задължително
required_for_app_access: Изисква се за достъп до приложението
requires_value: Изисква се стойност
create_preset: Създаване на изгледи
create_preset: Създаване на заготовка
create_role: Създаване на роля
create_user: Създаване на потребител
create_webhook: Създаване на уеб-кука
@@ -38,7 +43,7 @@ rename_bookmark: Преименуване на отметка
update_bookmark: Обновяване на отметка
delete_bookmark: Изтриване на отметка
delete_bookmark_copy: >-
Сигурни ли сте, че искате да изтриете "{bookmark}" отметката? Това действие не може да бъде отменено.
Сигурни ли сте, че искате да изтриете "{bookmark}" отметката? Действието не може да бъде отменено.
logoutReason:
SIGN_OUT: Отписан
SESSION_EXPIRED: Сесията е изтекла
@@ -53,16 +58,16 @@ os_version: Версия на ОС
os_uptime: Време от старт на ОС
os_totalmem: Памет на ОС
archive: Архивиране
archive_confirm: Сигурни ли сте, че искате да изтриете този запис?
archive_confirm: Сигурни ли сте, че искате да изтриете записа?
archive_confirm_count: >-
Няма избрани записи | Сигурни ли сте, че искате да архивирате този запис? | Сигурни ли сте, че искате да архивирате тези {count} записа?
Няма избрани записи | Сигурни ли сте, че искате да архивирате записа? | Сигурни ли сте, че искате да архивирате тези {count} записа?
reset_system_permissions_to: 'Нулиране на системните позволения към:'
reset_system_permissions_copy: Това действие може да презапише потребителските позволения които са приложени към системните колекции. Сигурни ли сте?
reset_system_permissions_copy: Действието може да презапише потребителските позволения, които са приложени към системните колекции. Сигурни ли сте?
the_following_are_minimum_permissions: Следните са минималните изисквани позволения за достъп до "Приложението". Може да бъдат добавяни повече, но не и по-малко.
app_access_minimum: Минимум за достъп до приложението
recommended_defaults: Препоръчителни
unarchive: Разархивиране
unarchive_confirm: Сигурни ли сте, че искате да разархивирате този запис?
unarchive_confirm: Сигурни ли сте, че искате да разархивирате записа?
nested_files_folders_will_be_moved: Вложените файлове и папки ще бъдат преместени една папка по-нагоре.
unknown_validation_errors: 'Има грешки при валидацията на следните скрити полета:'
validationError:
@@ -81,14 +86,14 @@ validationError:
null: Стойността трябва да е null
nnull: Стойността не трябва да е null
required: Изисква се стойност
unique: Изисква се уникална стойност
unique: Само уникални стойности
regex: Стойността е неправилно форматирана
all_access: Пълен достъп
no_access: Без достъп
use_custom: Персонализиран
nullable: Може да е null
allow_null_value: Може да е NULL
enter_value_to_replace_nulls: Въвеждане на нова стойност, която да замени NULL в това поле.
enter_value_to_replace_nulls: Въвеждане на нова стойност, която да замени NULL в полето.
field_standard: Стандартно
field_presentation: Презентационни и псевдоними
field_file: Единствен файл
@@ -101,10 +106,10 @@ field_translations: Преводи
item_permissions: Позволения по запис
field_permissions: Позволения по поле
field_validation: Валидация по поле
field_presets: Стойност на полета
field_presets: Стойности на полета
permissions_for_role: 'Записи, които ролята "{role}" има позволение за {action}.'
fields_for_role: 'Полета, които ролята "{role}" има позволение за {action}.'
validation_for_role: 'Правила, които ролята "{role}" трябва да спазва, при {action} на полетo.'
validation_for_role: 'Правила, които ролята "{role}" трябва да спазва, при {action} на полета.'
presets_for_role: 'Стойности за полета по подразбиране, за ролята "{role}".'
presentation_and_aliases: Презентационни и псевдоними
revision_post_update: Ето как ще изглежда записът след промяната...
@@ -118,14 +123,14 @@ field_create_success: 'Създадено поле: "{field}"'
field_update_success: 'Обновено поле: "{field}"'
duplicate_where_to: Къде искате да дублирате полето?
language: Език
global: Глобални
global: Глобален
admins_have_all_permissions: Администраторите имат всички позволения
camera: Камера
exposure: Експозиция
shutter: Затвор
iso: ISO
focal_length: Фокусно разстояние
schema_setup_key: Име на колоната в базата данни, както и име на полето в API
schema_setup_key: Име на колоната в базата от данни, както и име на полето в API
create_field: Създаване на поле
creating_new_field: 'Ново поле ({collection})'
field_in_collection: '{field} ({collection})'
@@ -150,7 +155,7 @@ time: Час
timestamp: Времеви печат
uuid: UUID
hash: Хеш
not_available_for_type: Не е налично за този тип
not_available_for_type: Не е налично за типа
create_translations: Създаване на преводи
auto_refresh: Автоматично опресняване
refresh_interval: Интервал за опресняване
@@ -158,7 +163,7 @@ no_refresh: Да не се опреснява
refresh_interval_seconds: Моментално опресняване | Всяка секунда | Всеки {seconds} секунди
refresh_interval_minutes: Всяка минута | Всеки {minutes} минути
auto_generate: Автоматично генериране
this_will_auto_setup_fields_relations: Автоматично настройване на задължителните полета и релации.
this_will_auto_setup_fields_relations: Автоматично настройване и генериране на задължителните полета и релации.
click_here: Натиснете тук
to_manually_setup_translations: за ръчна настройка на преводи.
click_to_manage_translated_fields: >-
@@ -205,7 +210,7 @@ edit_raw_value: Редактиране в суров вид
enter_raw_value: Въвеждане на стойността в суров вид...
clear_value: Изчистване на стойност
reset_to_default: Възстановяване към начално състояние
undo_changes: Отмяна на промените
undo_changes: Отмяна и напускане
notifications: Известия
show_all_activity: Показване на цялата активност
page_not_found: Страницата не е намерена
@@ -216,7 +221,7 @@ display: Показване
settings_update_success: Настройките са обновени
title: Заглавие
revision_delta_created: Създаден
revision_delta_created_externally: Външно създаден
revision_delta_created_externally: Създадено отвън
revision_delta_updated: '1 поле е обновено | {count} полета са обновени'
revision_delta_deleted: Изтрито
revision_delta_reverted: Възстановен
@@ -231,7 +236,7 @@ item_create_success: Създаден запис | Създадени запис
item_update_success: Обновен запис | Обновени записи
item_delete_success: Изтрит запис | Изтрити записи
this_collection: Тази колекция
related_collection: Релационна колекция
related_collection: Релативна колекция
related_collections: Релационни колекции
translations_collection: Колекция с преводи
languages_collection: Колекция с езици
@@ -249,7 +254,7 @@ submit: Изпращане
move_to_folder: Преместване в папка
move: Преместване
system: Система
add_field_related: Добавяне на поле към релационната колекция
add_field_related: Добавяне на поле към релативната колекция
interface: Интерфейс
today: Днес
yesterday: Вчера
@@ -312,7 +317,7 @@ creating_new_collection: Създаване на колекция
created_by: Създадено от
created_on: Създаден на
creating_collection_info: Именуване на колекцията и настройка на уникално ключово поле...
creating_collection_system: Включване и применуване на някои от следните незадължителни полета.
creating_collection_system: Включване и преименуване на някои от следните незадължителни полета.
auto_increment_integer: Автоматично следващо число
generated_uuid: Генериран UUID
manual_string: Ръчно въвеждане на стойност
@@ -321,9 +326,9 @@ save_and_stay: Запазване и оставане
save_as_copy: Запазване като копие
add_existing: Добавяне на съществуващ
creating_items: Създаване на записи
enable_create_button: Включване на бутона за създаване
enable_create_button: Показване на бутон за създаване
selecting_items: Избор на записи
enable_select_button: Включване на бутона за избор
enable_select_button: Показване на бутон за избор
comments: Коментари
no_comments: Все още няма коментари
click_to_expand: Натиснете за разширяване
@@ -375,7 +380,7 @@ file_moved: Файлът е преместен
collection_created: Колекцията е създадена
modified_on: Променен на
card_size: Големина на картите
sort_field: Поле за сортиране
sort_field: Сортиране по поле
add_sort_field: Добавяне на поле за сортиране
sort: Сортиране
status: Статус
@@ -415,12 +420,12 @@ bookmark_name: Име на отметка...
create_bookmark: Създаване на отметка
edit_bookmark: Редактиране на отметката
bookmarks: Отметки
presets: Готови изгледи
presets: Заготовки
unexpected_error: Неочаквана грешка
unexpected_error_copy: Възникна неочаквана грешка, опитайте отново по-късно.
copy_details: Копиране на детайли
no_app_access: Без достъп до приложението
no_app_access_copy: Не е позволено използването на администраторското приложение от този потребител.
no_app_access_copy: Не е позволено използването на администраторското приложение от потребителя.
password_reset_sent: Изпратена ви е сигурна връзка за възстановяване на вашата парола
password_reset_successful: Успешно нулирана парола
back: Назад
@@ -440,7 +445,7 @@ start_end_of_count_filtered_items: '{start}-{end} от {count} филтрира
one_item: '1 запис'
one_filtered_item: '1 филтриран запис'
delete_collection_are_you_sure: >-
Сигурни ли сте, че искате да изтриете тази колекция? Това действие ще изтрие колекцията и всички нейни записи. Действието е постоянно.
Сигурни ли сте, че искате да изтриете колекцията? Всички записи и самата колекция ще бъдат изтрити. Действието е постоянно.
collections_shown: Показани колекции
visible_collections: Видими колекции
hidden_collections: Скрити колекции
@@ -471,12 +476,16 @@ operators:
gt: По-голямо от
lte: По-малко или равно
gte: По-голямо или равно на
in: Е сред
nin: Не е сред
in: Е едно от
nin: Не е едно от
null: Е null
nnull: Не е null
contains: Съдържа
ncontains: Не съдържа
starts_with: Започва с
nstarts_with: Не започва с
ends_with: Завършва с
nends_with: Не завършва с
between: Е между
nbetween: Не е между
empty: Е празно
@@ -500,7 +509,7 @@ rows: Редове
columns: Колони
collection_setup: Настройки на колекция
optional_system_fields: Незадължителни системни полета
value_unique: Изисква се уникална стойност
value_unique: Само уникални стойности
all_activity: Цялата активност
create_item: Създаване на запис
display_template: Шаблон при показване
@@ -521,9 +530,9 @@ clear_filters: Изчистване на филтрите
saves_automatically: Автоматично запазване
role: Роля
user: Потребител
no_presets: Няма подготвени изгледи
no_presets_copy: Все още няма запазени подготвени изгледи или отметки.
no_presets_cta: Добавяне на подготвен изглед
no_presets: Няма заготовки
no_presets_copy: Все още няма запазени заготовки или отметки.
no_presets_cta: Добавяне на заготовка
create: Създаване
on_create: При създаване
on_update: При промяна
@@ -531,7 +540,7 @@ read: Четене
update: Обновяване
select_fields: Избор на полета
format_text: Форматиране на текст
bold: Удебелен
bold: Удебеляване
toggle: Превключване
icon_on: Активна икона
icon_off: Неактивна икона
@@ -559,7 +568,7 @@ wysiwyg_options:
alignright: Подравняване отдясно
forecolor: Основен цвят
backcolor: Цвят на фона
bold: Удебелен
bold: Удебеляване
italic: Курсив
underline: Подчертан
strikethrough: Зачеркнат
@@ -616,47 +625,47 @@ settings_data_model: Модел на данни
settings_permissions: Роли и позволения
settings_project: Настройки на проект
settings_webhooks: Уеб-куки
settings_presets: Изгледи и отметки
settings_presets: Заготовки и отметки
one_or_more_options_are_missing: Липсват една или повече опции
scope: Обхват
select: Избор...
layout: Оформление
tree_view: Дървовиден изглед
changes_are_permanent: Промените са постоянни
preset_name_placeholder: По подразбиране когато е празно...
preset_name_placeholder: Използва се за основна, ако е празно...
preset_search_placeholder: Заявка за търсене...
editing_preset: Редактиране на подготвени изгледи
editing_preset: Редактиране на заготовка
layout_preview: Предварителен преглед
layout_setup: Настройка на оформление
unsaved_changes: Незапазени промени
unsaved_changes_copy: Сигурни ли сте, че искате да напуснете тази страница?
unsaved_changes: Има незапазени промени
unsaved_changes_copy: Сигурни ли сте, че искате да отмените промените и напуснете тази страница?
discard_changes: Отмяна на промените
keep_editing: Продължаване на редакцията
page_help_collections_overview: '**Обзор на колекциите** — Показва всички налични колекции до които имате достъп.'
page_help_collections_overview: '**Обзор на колекциите** — Показване на всички налични колекции до които имате достъп.'
page_help_collections_collection: >-
**Преглед на записи** — Показва всички записи от {collection}, до които имате достъп. Дава възможност за персонализиране на оформлението, филтрите и подредбата, така, че да се впишат по-добре към вашите нужди, дори имате възможност да добавите отметки с тези конфигурации за по-бърз достъп.
**Преглед на записи** — Показване на всички записи от {collection}, до които имате достъп. Дава възможност за персонализиране на оформлението, филтрите и подредбата, така, че да се впишат по-добре към вашите нужди, дори имате възможност да добавите отметки с тези конфигурации за по-бърз достъп.
page_help_collections_item: >-
**Детайли за запис** — Формуляр за преглед и управление на този запис. Страничният панел също съдържа и пълна история на ревизиите и коментарите към него.
**Детайли за запис** — Формуляр за преглед и управление на записа. Страничният панел също съдържа и пълна история на ревизиите и коментарите към него.
page_help_activity_collection: >-
**Емисия на активността** — Подробен списък на активността за съдържанието, потребителите и системата.
page_help_docs_global: >-
**Обзор на документацията** — Показва документи, специфични за този проект, версия и схема.
**Обзор на документацията** — Показване на документи, специфични за проекта, версия и схема.
page_help_files_collection: >-
**Файлов архив** — Показва всички налични файлове, добавени към този проект. Дава възможност за персонализиране на оформлението, филтрите и подредбата, така, че да се впишат по-добре към вашите нужди, дори имате възможност да добавите отметки с тези конфигурации за по-бърз достъп.
**Файлов архив** — Показване на всички налични файлове, добавени към проекта. Дава възможност за персонализиране на оформлението, филтрите и подредбата, така, че да се впишат по-добре към вашите нужди, дори имате възможност да добавите отметки с тези конфигурации за по-бърз достъп.
page_help_files_item: >-
**Детайли за файл** — Формуляр за управление на мета-данните, редактиране на оригиналния файл и обновяване на настройките при достъп.
page_help_settings_project: "**Настройки за проекта** — Глобални настройки и конфигурация на вашият проект."
page_help_settings_datamodel_collections: >-
**Модел на данни: Колекции** — Показва всички налични колекции. Това включва видими, невидими и системни, както и все още неконтролираните таблици от базата данни, които могат да бъдат добавяни като колекции.
**Модел на данни: Колекции** — Показване на всички налични колекции. Включително видими, невидими и системни, както и все още неконфигурираните таблици в базата от данни, които могат да бъдат добавяни като колекции.
page_help_settings_datamodel_fields: >-
**Модел на данни: Колекция** — Формуляр за управление на тази колекция и нейните полета.
page_help_settings_roles_collection: '**Преглед на роли** — Показва администраторската, публичната и всички потребителски роли.'
page_help_settings_roles_collection: '**Преглед на роли** — Показване на администраторската, публичната и всички потребителски роли.'
page_help_settings_roles_item: "**Детайли за роля** — Управление на позволенията на ролята, както и някои други настройки."
page_help_settings_presets_collection: >-
**Преглед на изгледите** — Показва списък с всички предварително подготвени изгледи, включително за отделните: потребител, роля, и глобални отметки, както и изгледите по подразбиране.
**Преглед на заготовките** — Показване на списък от всички заготовки от изгледи, включително за отделните: потребител, роля, и глобални отметки, както и изгледите по подразбиране.
page_help_settings_presets_item: >-
**Детайли за подготвените изгледи** — Формуляр за управление на отметките и подготвените изгледи по подразбиране към колекциите.
page_help_settings_webhooks_collection: '**Преглед на уеб-куки** — Показва всички уеб-куки в проекта.'
**Детайли за заготовка** — Формуляр за управление на отметките и заготовките от изгледи по подразбиране отнасящи се за колекциите.
page_help_settings_webhooks_collection: '**Преглед на уеб-куки** — Показване на всички уеб-куки в проекта.'
page_help_settings_webhooks_item: '**Детайли за уеб-кука** — Формуляр за създаване и управление на уеб-куките в проекта.'
page_help_users_collection: '**Потребители** — Показване на всички потребители в системата на проекта.'
page_help_users_item: >-
@@ -664,11 +673,11 @@ page_help_users_item: >-
activity_feed: Емисия на активността
add_new: Добави нов запис
create_new: Създаване
all: Всички
none: Никое
all: Всичко
none: Нищо
no_layout_collection_selected_yet: Все още не е избран изглед/колекция
batch_delete_confirm: >-
Няма избрани записи | Наистина ли искате да изтриете този запис? Това действие не може да бъде отменено. | Наистина ли искате да изтриете тези {count} записа? Това действие не може да бъде отменено.
Няма избрани записи | Наистина ли искате да изтриете записа? Действието не може да бъде отменено. | Наистина ли искате да изтриете тези {count} записа? Това действие не може да бъде отменено.
cancel: Отмяна
collection: Колекция
collections: Колекции
@@ -691,13 +700,13 @@ fields:
icon: Икона
note: Бележка
display_template: Шаблон при показване
hidden: Скрито
hidden: Скриване
singleton: Сек
translations: Преводи на името на колекцията
archive_app_filter: Филтър за архиви
archive_value: Стойност при архивиране
unarchive_value: Стойност при разархивиране
sort_field: Поле за сортиране
sort_field: Сортиране по поле
accountability: Следене за активност и ревизии
directus_files:
$thumbnail: Умалена картинка
@@ -746,7 +755,7 @@ fields:
project_url: URL на проекта
project_color: Цвят на проекта
project_logo: Лого на проекта
public_foreground: Публично изображение
public_foreground: Основно изображение
public_background: Фоново изображение
public_note: Публично съобщение
auth_password_policy: Политика за пароли
@@ -758,9 +767,9 @@ fields:
collection: Име на колекцията
icon: Икона на колекцията
note: Бележка
hidden: Скрито
hidden: Скриване
singleton: Сек
translation: Превод на името на полето
translation: Превод за името на полето
display_template: Шаблон
directus_roles:
name: Име на роля
@@ -796,14 +805,16 @@ referential_action_set_null: Зануляване на {field}
referential_action_set_default: Задаване на {field} към стойността по подразбиране
choose_action: Изберете действие
continue: Продължение
continue_as: >-
{name} в момента е оторизиран. Ако разпознавате профила, изберете бутона за продължение.
editing_role: 'Роля - {role}'
creating_webhook: Създаване на уеб-кука
default: По подразбиране
delete: Изтриване
delete_are_you_sure: >-
Това действие е постоянно и не може да бъде отменено. Сигурни ли сте, че искате да продължите?
Действието е постоянно и не може да бъде отменено. Сигурни ли сте, че искате да продължите?
delete_field_are_you_sure: >-
Сигурни ли сте, че искате да изтриете "{field}" полето? Това действие не може да бъде отменено.
Сигурни ли сте, че искате да изтриете полето "{field}"? Действието не може да бъде отменено.
description: Описание
done: Готово
duplicate: Дублиране
@@ -814,7 +825,7 @@ field: Поле | Полета
file: Файл
file_library: Файлов архив
forgot_password: Забравена парола
hidden: Скрито
hidden: Скриване
icon: Икона
info: Информация
normal: Нормално
@@ -854,6 +865,7 @@ template: Шаблон
translation: Превод
value: Стойност
view_project: Преглед на проект
weeks: { }
report_error: Докладване на грешка
interfaces:
presentation-links:
@@ -871,6 +883,11 @@ interfaces:
allow_other: Други
show_more: 'Показване на още {count}'
items_shown: Показани записи
select-multiple-checkbox-tree:
name: Дърво от отметки
description: Избор измежду множество опции чрез вложени отметки
value_combining: Комбинация от стойности
value_combining_note: Контрол върху стойността за запазване при направен избор.
input-code:
code: Код
description: Писане или споделяне на кодови откъси
@@ -888,14 +905,14 @@ interfaces:
color: Цвят
description: Въвеждане или избор на стойност за цвят
placeholder: Избор на цвят...
preset_colors: Предварително зададени цветове
preset_colors: Заготовки от цветове
preset_colors_add_label: Добавяне на нов цвят...
name_placeholder: Въвеждане името на цвета...
datetime:
datetime: Дата и час
description: Въвеждане на дати и часове
include_seconds: Вклчване на секундите
set_to_now: Задаване като сега
set_to_now: Задаване на сега
use_24: Използване на 24 часов формат
system-display-template:
display-template: Шаблон при показване
@@ -916,8 +933,8 @@ interfaces:
choices_placeholder: Добавяне на избор
allow_other: Други
allow_other_label: Позволяване на други стойности
allow_none: Позволяване на нищо
allow_none_label: Текст за "Позволяване на нищо"
allow_none: Празен избор
allow_none_label: Позволяване на празен избор
choices_name_placeholder: Въвеждане на име...
choices_value_placeholder: Въвеждане на стойност...
select-multiple-dropdown:
@@ -965,9 +982,9 @@ interfaces:
imageToken: Токън за изображенията
imageToken_label: Какъв (статичен) токън да се добави, към адресите на изображенията
presentation-notice:
notice: Бележка
description: Показване на кратка бележка
text: Въвеждане на бележката...
notice: Пояснение
description: Показване на кратко пояснение
text: Въвеждане на пояснението...
list-o2m:
one-to-many: Един към много
description: Избор на множество свързани записи
@@ -1001,7 +1018,7 @@ interfaces:
alphabetize_label: Винаги в азбучен ред
add_tags: Добавяне на етикет...
input:
input: Въвеждане
input: Текстово поле
description: Ръчно въвеждане на стойност
trim: Почистване
trim_label: Почистване на краищата от интервали
@@ -1044,7 +1061,7 @@ interfaces:
options_override: Наслагване на опции
input-autocomplete-api:
input-autocomplete-api: Поле с автоматично дописване (API)
description: Автоматично дописване на стойности от външно API.
description: Автоматично дописване на стойности чрез външно API.
results_path: Път до резултатите
value_path: Път до стойността
trigger: Задействане
@@ -1086,7 +1103,7 @@ displays:
filesize: Големина на файла
description: Показване на големината на файл
formatted-value:
formatted-value: Формат на стойността
formatted-value: Форматиране на стойност
description: Показване на форматирана стойност на текст
format_title: Формат на заглавие
format_title_label: Автоматично форматиране на регистъра
@@ -1097,18 +1114,18 @@ displays:
icon:
icon: Икона
description: Показване като икона
filled: Изпълнен
filled_label: Използване на вариант с изпълване
filled: Запълване
filled_label: Използване на изпълнен вариант
image:
image: Изображение
description: Показване на малко изображение
circle: Кръг
circle_label: Показване като кръг
circle_label: Показване в кръгла форма
labels:
labels: Етикети
description: Въвеждане както на единичен, така и на множество етикети
default_foreground: Преден план по подразбиране
default_background: Фон по подразбиране
default_foreground: Основен цвят по подразбиране
default_background: Фонов цвят по подразбиране
format_label: Форматиране на всеки етикет
show_as_dot: Показване като точка
choices_value_placeholder: Въвеждане на стойност...
@@ -1120,14 +1137,14 @@ displays:
extension_only_label: Показване само на разширението
rating:
rating: Оценка
description: Показване на числото като звезди до максималната стойност
description: Показване на числото, като брой звезди до максималната стойност
simple: Опростен
simple_label: Показване на звездите в опростен вид
raw:
raw: Стойност в суров вид
related-values:
related-values: Релативни стойности
description: Показване на релативни стойности
description: Показване на релативните стойности
user:
user: Потребител
description: Показване на потребител от Directus

View File

@@ -3,6 +3,7 @@ item_revision: Item Revision
duplicate_field: Duplicate Field
half_width: Half Width
full_width: Full Width
group: Group
fill_width: Fill Width
field_name_translations: Field Name Translations
enter_password_to_enable_tfa: Enter your password to enable Two-Factor Authentication
@@ -106,6 +107,7 @@ field_m2a: M2A Relationship
field_o2m: O2M Relationship
field_m2m: M2M Relationship
field_translations: Translations
field_group: Field Group
item_permissions: Item Permissions
field_permissions: Field Permissions
field_validation: Field Validation
@@ -1077,6 +1079,9 @@ interfaces:
value_path: Value Path
trigger: Trigger
rate: Rate
group-raw:
name: Raw Fields
description: Show the fields as normal.
displays:
boolean:
boolean: Boolean

View File

@@ -1 +1,137 @@
---
edit_field: 필드 편집
item_revision: 아이템 리비전
duplicate_field: 중복 필드
field_name_translations: 항목 명 번역
enter_password_to_enable_tfa: 암호를 입력하여 이중 인증을 활성화하십시오
add_field: 필드 추가
role_name: 권한 이름
branch: 브랜치
indeterminate: 불확정 상태
children: 하위
required: 필수 사항
requires_value: 필수 항목
create_preset: 사전설정 생성
create_role: 역할 생성
create_user: 사용자 생성
create_webhook: 웹훅 생성
invite_users: 사용자 초대
invite: 초대
emails: 이메일
connection_poor: 연결 상태 불량
rename_folder: 폴더 이름 변경
delete_folder: 폴더 삭제
prefix: 접두사
suffix: 접미사
reset_bookmark: 북마크 초기화
rename_bookmark: 북마크 이름 변경
update_bookmark: 북마크 업데이트
delete_bookmark: 북마크 삭제
delete_bookmark_copy: >-
정말로 이 계정을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.
logoutReason:
SIGN_OUT: 로그아웃
SESSION_EXPIRED: 세션 만료
public: 공개
not_allowed: 허용되지 않음
validationError:
unique: 생산Key는 유일해야만 합니다.
all_access: 모든 액세스
no_access: 접근 불가
use_custom: 직접 설정
nullable: NULL허용
field_standard: 표준
field_file: 단일 파일
field_files: 여러 파일들
field_translations: 번역
delete_field: 필드 삭제
language: 언어
global: 글로벌
camera: 카메라
exposure: 노출
shutter: 셔터
iso: ISO
focal_length: 초점 거리
create_field: 필드 생성
reset_page_preferences: 앱 환경설정 초기화
hidden_field: 숨김 필드
hidden_on_detail: 실행 시 숨김
key:
alias: 별칭
auto_generate: 자동 생성
click_here: 여기를 클릭하세요
fields_group: 필드 그룹
no_collections_found: 컬렉션 없음
search_collection: 컬렉션 검색
new_field: '새 필드'
new_collection: '새 컬렉션'
choose_a_type: 유형을 선택...
default_value: 기본값
standard_field: 표준 항목들
single_file: 단일 파일
multiple_files: 여러 파일들
invalid_item: 비정상적인 아이템
next: 다음
field_name: 필드 이름
translations: 번역
note: 메모
enter_a_value: 값 입력
length: 길이
unique: 고유
post_comment_success: 댓글 작성됨
format: 형식
export_collection: '컬렉션 내보내기'
last_page: 마지막 페이지
last_access: 최근 접속
submit: 확인
move_to_folder: 폴더로 이동
move: 이동
system: 시스템
interface: 인터페이스
today: 오늘
yesterday: 어제
delete_comment: 댓글 삭제
month:
year:
select_all: 모두 선택
months:
january: 1월
february: 2월
march: 3월
april: 4월
may: 5월
june: 6월
july: 7월
august: 8월
september: 9월
october: 10월
november: 11월
december: 12월
drag_mode: 끌기 모드
type: 타입
creating_new_collection: 새 컬렉션 만들기
value_unique: 생산Key는 유일해야만 합니다.
wysiwyg_options:
selectall: 모두 선택
fields:
directus_collections:
note: 메모
directus_users:
language: 언어
last_page: 마지막 페이지
last_access: 최근 접속
directus_fields:
note: 메모
translation: 항목 명 번역
directus_roles:
name: 권한 이름
interfaces:
select-dropdown:
choices_value_placeholder: 값 입력
system-interface:
interface: 인터페이스
displays:
datetime:
format: 형식
labels:
choices_value_placeholder: 값 입력

View File

@@ -11,7 +11,7 @@
<v-checkbox v-model="fieldData.meta.hidden" :label="t('hidden_on_detail')" block />
</div>
<div class="field full">
<div class="field full" v-if="localType !== 'group'">
<div class="label type-label">{{ t('note') }}</div>
<v-input v-model="fieldData.meta.note" :placeholder="t('add_note')" />
</div>

View File

@@ -134,6 +134,7 @@ import useCollection from '@/composables/use-collection';
import { getLocalTypeForField } from '../get-local-type';
import { notify } from '@/utils/notify';
import formatTitle from '@directus/format-title';
import { localTypes } from '@/types';
import { initLocalStore, state, clearLocalStore } from './store';
import { unexpectedError } from '@/utils/unexpected-error';
@@ -159,9 +160,7 @@ export default defineComponent({
required: true,
},
type: {
type: String as PropType<
'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'm2a' | 'presentation' | 'translations'
>,
type: String as PropType<typeof localTypes[number]>,
default: null,
},
},
@@ -193,8 +192,7 @@ export default defineComponent({
const localType = computed(() => {
if (props.field === '+') return props.type;
let type: 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2a' | 'm2o' | 'presentation' | 'translations' =
'standard';
let type: typeof localTypes[number];
type = getLocalTypeForField(props.collection, props.field);
return type;
@@ -257,7 +255,7 @@ export default defineComponent({
},
];
if (props.type !== 'presentation') {
if (props.type !== 'presentation' && props.type !== 'group') {
tabs.push({
text: t('display'),
value: 'display',
@@ -392,7 +390,7 @@ export default defineComponent({
await Promise.all(
state.relations.map((relation) => {
const relationExists = !!relationsStore.getRelationForField(relation.collection, relation.field);
const relationExists = !!relationsStore.getRelationForField(relation.collection!, relation.field!);
if (relationExists) {
return api.patch(`/relations/${relation.collection}/${relation.field}`, relation);

View File

@@ -152,6 +152,7 @@ function initLocalStore(collection: string, field: string, type: typeof localTyp
else if (type === 'm2m' || type === 'files' || type === 'translations') useM2M();
else if (type === 'o2m') useO2M();
else if (type === 'presentation') usePresentation();
else if (type === 'group') useGroup();
else if (type === 'm2a') useM2A();
else useStandard();
@@ -1041,6 +1042,15 @@ function initLocalStore(collection: string, field: string, type: typeof localTyp
};
}
function useGroup() {
delete state.fieldData.schema;
state.fieldData.type = 'alias';
state.fieldData.meta = {
...(state.fieldData.meta || {}),
special: ['alias', 'no-data', 'group'],
};
}
function useStandard() {
watch(
() => state.fieldData.type,

View File

@@ -0,0 +1,109 @@
<template>
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon clickable @click.stop="toggle" name="more_vert" />
</template>
<v-list>
<v-list-item :to="`/settings/data-model/${field.collection}/${field.field}`">
<v-list-item-icon><v-icon name="edit" outline /></v-list-item-icon>
<v-list-item-content>
{{ t('edit_field') }}
</v-list-item-content>
</v-list-item>
<v-list-item :disabled="duplicable === false" clickable @click="$emit('duplicate')">
<v-list-item-icon>
<v-icon name="content_copy" />
</v-list-item-icon>
<v-list-item-content>{{ t('duplicate_field') }}</v-list-item-content>
</v-list-item>
<v-list-item clickable @click="$emit('toggleVisibility')">
<template v-if="field.meta?.hidden === false">
<v-list-item-icon><v-icon name="visibility_off" /></v-list-item-icon>
<v-list-item-content>{{ t('hide_field_on_detail') }}</v-list-item-content>
</template>
<template v-else>
<v-list-item-icon><v-icon name="visibility" /></v-list-item-icon>
<v-list-item-content>{{ t('show_field_on_detail') }}</v-list-item-content>
</template>
</v-list-item>
<v-divider />
<v-list-item
clickable
@click="$emit('setWidth', 'half')"
:disabled="field.meta?.width === 'half' || localType === 'group'"
>
<v-list-item-icon><v-icon name="border_vertical" /></v-list-item-icon>
<v-list-item-content>{{ t('half_width') }}</v-list-item-content>
</v-list-item>
<v-list-item
clickable
@click="$emit('setWidth', 'full')"
:disabled="field.meta?.width === 'full' || localType === 'group'"
>
<v-list-item-icon><v-icon name="border_right" /></v-list-item-icon>
<v-list-item-content>{{ t('full_width') }}</v-list-item-content>
</v-list-item>
<v-list-item
clickable
@click="$emit('setWidth', 'fill')"
:disabled="field.meta?.width === 'fill' || localType === 'group'"
>
<v-list-item-icon><v-icon name="aspect_ratio" /></v-list-item-icon>
<v-list-item-content>{{ t('fill_width') }}</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item
clickable
@click="$emit('delete')"
class="danger"
:disabled="field.schema?.is_primary_key === true || noDelete"
>
<v-list-item-icon><v-icon name="delete" outline /></v-list-item-icon>
<v-list-item-content>
{{ t('delete_field') }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import { Field } from '@/types';
import { useI18n } from 'vue-i18n';
import { getLocalTypeForField } from '../../get-local-type';
export default defineComponent({
name: 'field-select-menu',
emits: ['toggleVisibility', 'duplicate', 'delete', 'setWidth'],
props: {
field: {
type: Object as PropType<Field>,
required: true,
},
noDelete: {
type: Boolean,
default: false,
},
},
setup(props) {
const { t } = useI18n();
const localType = computed(() => getLocalTypeForField(props.field.collection, props.field.field));
const isPrimaryKey = computed(() => props.field.schema?.is_primary_key === true);
const duplicable = computed(() => localType.value === 'standard' && isPrimaryKey.value === false);
return { t, localType, isPrimaryKey, duplicable };
},
});
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div :class="(field.meta && field.meta.width) || 'full'">
<div class="field-select" :class="field.meta?.width || 'full'">
<v-input disabled v-if="disabled" class="field">
<template #prepend>
<v-icon name="lock" v-tooltip="t('system_fields_locked')" />
@@ -12,47 +12,40 @@
</template>
</v-input>
<div v-else-if="localType === 'translations'" class="group">
<div class="header">
<v-icon class="drag-handle" name="drag_indicator" />
<span class="name" v-tooltip="field.name">{{ field.field }}</span>
<div class="spacer" />
<v-icon small name="group_work" v-tooltip="t('fields_group')" />
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<span class="group-options" @click="toggle">
<v-icon name="more_vert" />
</span>
</template>
<draggable
v-if="localType === 'group'"
class="field-grid group full nested"
:model-value="nestedFields"
:force-fallback="true"
handle=".drag-handle"
:group="{ name: 'fields' }"
:set-data="hideDragImage"
:animation="150"
item-key="field"
@update:model-value="onGroupSortChange"
:fallbackOnBody="true"
:invertSwap="true"
>
<template #header>
<div class="header full">
<v-icon class="drag-handle" name="drag_indicator" @click.stop />
<span class="name">{{ field.field }}</span>
<v-icon v-if="hidden" name="visibility_off" class="hidden-icon" v-tooltip="t('hidden_field')" small />
<field-select-menu
:field="field"
@toggleVisibility="toggleVisibility"
@setWidth="setWidth($event)"
@duplicate="duplicateActive = true"
@delete="deleteActive = true"
:no-delete="nestedFields.length > 0"
/>
</div>
</template>
<v-list>
<v-list-item :to="`/settings/data-model/${field.collection}/${field.field}`">
<v-list-item-icon><v-icon name="edit" outline /></v-list-item-icon>
<v-list-item-content>
{{ t('edit_field') }}
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item clickable @click="deleteActive = true" class="danger">
<v-list-item-icon><v-icon name="delete" outline /></v-list-item-icon>
<v-list-item-content>
{{ t('delete_field') }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</div>
<router-link :to="`/settings/data-model/${translationsCollection}`">
<v-notice type="info" icon="translate">
<div>{{ t('click_to_manage_translated_fields', translationsFieldsCount) }}</div>
<div class="spacer" />
<v-icon name="launch" />
</v-notice>
</router-link>
</div>
<template #item="{ element }">
<field-select :field="element" :fields="fields" @setNestedSort="$emit('setNestedSort', $event)" />
</template>
</draggable>
<v-input v-else class="field" :class="{ hidden }" readonly clickable @click="openFieldDetail">
<template #prepend>
@@ -86,69 +79,13 @@
v-tooltip="t('db_only_click_to_configure')"
/>
<v-icon v-if="hidden" name="visibility_off" class="hidden-icon" v-tooltip="t('hidden_field')" small />
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon @click.stop="toggle" name="more_vert" />
</template>
<v-list>
<v-list-item :to="`/settings/data-model/${field.collection}/${field.field}`">
<v-list-item-icon><v-icon name="edit" outline /></v-list-item-icon>
<v-list-item-content>
{{ t('edit_field') }}
</v-list-item-content>
</v-list-item>
<v-list-item v-if="duplicable" clickable @click="duplicateActive = true">
<v-list-item-icon>
<v-icon name="content_copy" />
</v-list-item-icon>
<v-list-item-content>{{ t('duplicate_field') }}</v-list-item-content>
</v-list-item>
<v-list-item clickable @click="toggleVisibility">
<template v-if="hidden === false">
<v-list-item-icon><v-icon name="visibility_off" /></v-list-item-icon>
<v-list-item-content>{{ t('hide_field_on_detail') }}</v-list-item-content>
</template>
<template v-else>
<v-list-item-icon><v-icon name="visibility" /></v-list-item-icon>
<v-list-item-content>{{ t('show_field_on_detail') }}</v-list-item-content>
</template>
</v-list-item>
<v-divider />
<v-list-item clickable @click="setWidth('half')" :disabled="field.meta && field.meta.width === 'half'">
<v-list-item-icon><v-icon name="border_vertical" /></v-list-item-icon>
<v-list-item-content>{{ t('half_width') }}</v-list-item-content>
</v-list-item>
<v-list-item clickable @click="setWidth('full')" :disabled="field.meta && field.meta.width === 'full'">
<v-list-item-icon><v-icon name="border_right" /></v-list-item-icon>
<v-list-item-content>{{ t('full_width') }}</v-list-item-content>
</v-list-item>
<v-list-item clickable @click="setWidth('fill')" :disabled="field.meta && field.meta.width === 'fill'">
<v-list-item-icon><v-icon name="aspect_ratio" /></v-list-item-icon>
<v-list-item-content>{{ t('fill_width') }}</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item
clickable
@click="deleteActive = true"
class="danger"
:disabled="(field.schema && field.schema.is_primary_key === true) || false"
>
<v-list-item-icon><v-icon name="delete" outline /></v-list-item-icon>
<v-list-item-content>
{{ t('delete_field') }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
<field-select-menu
:field="field"
@toggleVisibility="toggleVisibility"
@setWidth="setWidth($event)"
@duplicate="duplicateActive = true"
@delete="deleteActive = true"
/>
</div>
</template>
</v-input>
@@ -196,7 +133,7 @@
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, ref, computed } from 'vue';
import { Field } from '@/types';
import { useCollectionsStore, useFieldsStore, useRelationsStore } from '@/stores/';
import { useCollectionsStore, useFieldsStore } from '@/stores/';
import { getInterfaces } from '@/interfaces';
import { useRouter } from 'vue-router';
import { cloneDeep } from 'lodash';
@@ -204,8 +141,13 @@ import { getLocalTypeForField } from '../../get-local-type';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
import { InterfaceConfig } from '@/interfaces/types';
import FieldSelectMenu from './field-select-menu.vue';
import hideDragImage from '@/utils/hide-drag-image';
import Draggable from 'vuedraggable';
export default defineComponent({
name: 'field-select',
components: { FieldSelectMenu, Draggable },
props: {
field: {
type: Object as PropType<Field>,
@@ -215,13 +157,16 @@ export default defineComponent({
type: Boolean,
default: false,
},
fields: {
type: Array as PropType<Field[]>,
default: () => [],
},
},
setup(props) {
setup(props, { emit }) {
const { t } = useI18n();
const router = useRouter();
const relationsStore = useRelationsStore();
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { interfaces } = getInterfaces();
@@ -229,8 +174,7 @@ export default defineComponent({
const editActive = ref(false);
const { deleteActive, deleting, deleteField } = useDeleteField();
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating, duplicable } =
useDuplicate();
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating } = useDuplicate();
const interfaceName = computed(() => {
return interfaces.value.find((inter: InterfaceConfig) => inter.id === props.field.meta?.interface)?.name;
@@ -240,7 +184,7 @@ export default defineComponent({
const localType = computed(() => getLocalTypeForField(props.field.collection, props.field.field));
const { translationsCollection, translationsFieldsCount } = useTranslations();
const nestedFields = computed(() => props.fields.filter((field) => field.meta?.group === props.field.meta?.id));
return {
t,
@@ -260,9 +204,9 @@ export default defineComponent({
hidden,
toggleVisibility,
localType,
translationsCollection,
translationsFieldsCount,
duplicable,
hideDragImage,
onGroupSortChange,
nestedFields,
};
function setWidth(width: string) {
@@ -303,14 +247,6 @@ export default defineComponent({
);
const duplicateTo = ref(props.field.collection);
const duplicable = computed(() => {
return (
['o2m', 'm2m', 'm2o', 'files', 'file', 'm2a'].includes(
getLocalTypeForField(props.field.collection, props.field.field)
) === false && props.field.schema?.is_primary_key === false
);
});
return {
duplicateActive,
duplicateName,
@@ -318,7 +254,6 @@ export default defineComponent({
duplicateTo,
saveDuplicate,
duplicating,
duplicable,
};
async function saveDuplicate() {
@@ -366,28 +301,16 @@ export default defineComponent({
router.push(`/settings/data-model/${props.field.collection}/${props.field.field}`);
}
function useTranslations() {
const translationsCollection = computed(() => {
if (localType.value !== 'translations') return null;
async function onGroupSortChange(fields: Field[]) {
const updates = fields.map((field, index) => ({
field: field.field,
meta: {
sort: index + 1,
group: props.field.meta!.id,
},
}));
const relation = relationsStore.relations.find((relation) => {
return (
relation.related_collection === props.field.collection && relation.meta?.one_field === props.field.field
);
});
if (!relation) return null;
return relation.collection;
});
const translationsFieldsCount = computed<number>(() => {
if (!translationsCollection.value) return 0;
const fields = fieldsStore.getFieldsForCollection(translationsCollection.value);
return fields.filter((field: Field) => field.meta?.hidden !== true).length;
});
return { translationsCollection, translationsFieldsCount };
emit('setNestedSort', updates);
}
},
});
@@ -396,6 +319,11 @@ export default defineComponent({
<style lang="scss" scoped>
@import '@/styles/mixins/form-grid';
.field-select {
--input-height: 48px;
--input-padding: 8px;
}
.full,
.fill {
grid-column: 1 / span 2;
@@ -443,43 +371,77 @@ export default defineComponent({
.group {
position: relative;
min-height: var(--input-height);
padding: var(--input-padding);
background-color: var(--card-face-color);
padding-top: 40px;
padding-bottom: 16px;
border-radius: var(--border-radius);
box-shadow: 0px 0px 6px 0px rgba(var(--card-shadow-color), 0.2);
> * {
position: relative;
z-index: 2;
}
&::before {
position: absolute;
top: 0;
left: -2px;
z-index: 1;
width: 4px;
height: 100%;
background-color: var(--primary);
border-radius: 2px;
content: '';
}
&::after {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
background-color: var(--primary);
opacity: 0.1;
content: '';
}
.header {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
margin-bottom: var(--input-padding);
}
.name {
width: 100%;
margin-bottom: 8px;
padding-top: 8px;
color: var(--primary);
font-family: var(--family-monospace);
}
.drag-handle {
margin-right: 8px;
transition: color var(--fast) var(--transition);
.drag-handle {
--v-icon-color: var(--primary);
&:hover {
color: var(--foreground);
margin-right: 8px;
}
.name {
flex-grow: 1;
}
}
}
.group-options {
cursor: pointer;
}
.field-grid {
position: relative;
display: grid;
grid-gap: 12px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
.v-notice {
cursor: pointer;
& + & {
margin-top: 12px;
}
}
.field {
--input-height: 48px;
--input-padding: 8px;
:deep(.input) {
background-color: var(--card-face-color) !important;
border: none !important;
@@ -554,4 +516,13 @@ export default defineComponent({
left: -8px;
color: var(--primary);
}
.sortable-ghost {
border-radius: var(--border-radius);
outline: 2px dashed var(--primary);
> * {
opacity: 0;
}
}
</style>

View File

@@ -1,21 +1,24 @@
<template>
<div class="fields-management">
<div class="field-grid">
<div class="field-grid" v-if="lockedFields.length > 0">
<field-select disabled v-for="field in lockedFields" :key="field.field" :field="field" />
</div>
<draggable
class="field-grid"
:model-value="usableFields"
:model-value="usableFields.filter((field) => isNil(field?.meta?.group))"
:force-fallback="true"
handle=".drag-handle"
group="fields"
:group="{ name: 'fields' }"
:set-data="hideDragImage"
@update:model-value="setSort"
item-key="field"
@update:model-value="setSort"
:animation="150"
:fallbackOnBody="true"
:invertSwap="true"
>
<template #item="{ element }">
<field-select :field="element" />
<field-select :field="element" :fields="usableFields" @setNestedSort="setNestedSort" />
</template>
</draggable>
@@ -62,9 +65,10 @@ import { Field } from '@/types';
import { useFieldsStore } from '@/stores/';
import FieldSelect from './field-select.vue';
import hideDragImage from '@/utils/hide-drag-image';
import { orderBy } from 'lodash';
import { orderBy, isNil } from 'lodash';
export default defineComponent({
name: 'fields-management',
components: { Draggable, FieldSelect },
props: {
collection: {
@@ -104,6 +108,11 @@ export default defineComponent({
icon: 'scatter_plot',
text: t('presentation_and_aliases'),
},
{
type: 'group',
icon: 'view_in_ar',
text: t('field_group'),
},
{
divider: true,
},
@@ -150,18 +159,27 @@ export default defineComponent({
},
]);
return { t, usableFields, lockedFields, setSort, hideDragImage, addOptions };
return { t, usableFields, lockedFields, setSort, hideDragImage, addOptions, setNestedSort, isNil };
async function setSort(fields: Field[]) {
const updates = fields.map((field, index) => ({
field: field.field,
meta: {
sort: index + 1,
group: null,
},
}));
await fieldsStore.updateFields(collection.value, updates);
}
async function setNestedSort(updates?: Field[]) {
updates = (updates || []).filter((val) => isNil(val) === false);
if (updates.length > 0) {
await fieldsStore.updateFields(collection.value, updates);
}
}
},
});
</script>
@@ -180,10 +198,7 @@ export default defineComponent({
display: grid;
grid-gap: 12px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
& + & {
margin-top: 12px;
}
padding-bottom: 24px;
}
.add-field {
@@ -191,7 +206,7 @@ export default defineComponent({
--v-button-background-color: var(--primary);
--v-button-background-color-hover: var(--primary-125);
margin-top: 12px;
margin-top: -8px;
.v-icon {
margin-right: 8px;

View File

@@ -1,18 +1,24 @@
import { useFieldsStore, useRelationsStore } from '@/stores';
import { Relation } from '@/types';
import { localTypes, Relation } from '@/types';
export function getLocalTypeForField(
collection: string,
field: string
): 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2a' | 'm2o' | 'presentation' | 'translations' {
export function getLocalTypeForField(collection: string, field: string): typeof localTypes[number] | null {
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const fieldInfo = fieldsStore.getField(collection, field);
const relations: Relation[] = relationsStore.getRelationsForField(collection, field);
if (!fieldInfo) return null;
if (relations.length === 0) {
if (fieldInfo.type === 'alias') return 'presentation';
if (fieldInfo.type === 'alias') {
if (fieldInfo.meta?.special?.includes('group')) {
return 'group';
}
return 'presentation';
}
return 'standard';
}

View File

@@ -164,7 +164,7 @@ export const useFieldsStore = defineStore({
unexpectedError(err);
}
},
async updateFields(collectionKey: string, updates: Partial<Field>[]) {
async updateFields(collectionKey: string, updates: DeepPartial<Field>[]) {
const updateID = nanoid();
const stateClone = [...this.fields];
@@ -246,11 +246,11 @@ export const useFieldsStore = defineStore({
/**
* Retrieve field info for a field or a related field
*/
getField(collection: string, fieldKey: string) {
getField(collection: string, fieldKey: string): Field | null {
if (fieldKey.includes('.')) {
return this.getRelationalField(collection, fieldKey);
return this.getRelationalField(collection, fieldKey) || null;
} else {
return this.fields.find((field) => field.collection === collection && field.field === fieldKey);
return this.fields.find((field) => field.collection === collection && field.field === fieldKey) || null;
}
},
/**

View File

@@ -1,4 +1,5 @@
import { Column } from 'knex-schema-inspector/dist/types/column';
import { FilterOperator } from '@directus/shared/types';
type Translations = {
language: string;
@@ -38,6 +39,7 @@ export const localTypes = [
'm2a',
'presentation',
'translations',
'group',
] as const;
export type FieldMeta = {
@@ -69,4 +71,14 @@ export interface FieldRaw {
export interface Field extends FieldRaw {
name: string;
children?: Field[] | null;
}
export type ValidationError = {
code: string;
field: string;
type: FilterOperator;
valid?: number | string | (number | string)[];
invalid?: number | string | (number | string)[];
substring?: string;
};

View File

@@ -11,7 +11,8 @@ export async function getFullcalendarLocale(lang: string): Promise<LocaleInput>
try {
const mod = await importCalendarLocale(l);
locale = mod.default.default;
// There's a problem in how @fullcalendar/core exports the language to "fake" ESM
locale = mod.default.default || mod.default;
break;
} catch {
continue;

View File

@@ -3,7 +3,7 @@ import vue from '@vitejs/plugin-vue';
import yaml from '@rollup/plugin-yaml';
import path from 'path';
import {
ensureExtensionsDirs,
ensureExtensionDirs,
getPackageExtensions,
getLocalExtensions,
generateExtensionsEntry,
@@ -43,12 +43,19 @@ function directusExtensions() {
const virtualIds = APP_EXTENSION_TYPES.map((type) => `${prefix}${type}`);
let extensionEntrys = {};
if (process.env.NODE_ENV !== 'production') loadExtensions();
return [
{
name: 'directus-extensions-serve',
apply: 'serve',
config: () => ({
optimizeDeps: {
include: SHARED_DEPS,
},
}),
async buildStart() {
await loadExtensions();
},
resolveId(id) {
if (virtualIds.includes(id)) {
return id;
@@ -61,11 +68,6 @@ function directusExtensions() {
return extensionEntrys[extensionType];
}
},
config: () => ({
optimizeDeps: {
include: SHARED_DEPS,
},
}),
},
{
name: 'directus-extensions-build',
@@ -92,7 +94,7 @@ function directusExtensions() {
const apiPath = path.join('..', 'api');
const extensionsPath = path.join(apiPath, 'extensions');
await ensureExtensionsDirs(extensionsPath);
await ensureExtensionDirs(extensionsPath);
const packageExtensions = await getPackageExtensions(apiPath);
const localExtensions = await getLocalExtensions(extensionsPath);

View File

@@ -2,6 +2,50 @@
_Changes marked with a :warning: contain potential breaking changes depending on your use of the package._
## v9.0.0-rc.81 (June 26, 2021)
### :rocket: Improvements
- **App**
- [#6466](https://github.com/directus/directus/pull/6466) Set calendar layout locale based on app locale
([@nickrum](https://github.com/nickrum))
### :bug: Bug Fixes
- **App**
- [#6481](https://github.com/directus/directus/pull/6481) Fix login page not showing user's name on app required
permissions role ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#6377](https://github.com/directus/directus/pull/6377) Fix app extensions loading and registration
([@nickrum](https://github.com/nickrum))
### :memo: Documentation
- [#6467](https://github.com/directus/directus/pull/6467) Import a File link in Assets tip broken
([@Mrmiffo](https://github.com/Mrmiffo))
### :package: Dependency Updates
- [#6509](https://github.com/directus/directus/pull/6509) update dependency prettier to v2.3.2
([@renovate[bot]](https://github.com/apps/renovate))
- [#6507](https://github.com/directus/directus/pull/6507) update dependency marked to v2.1.3
([@renovate[bot]](https://github.com/apps/renovate))
- [#6499](https://github.com/directus/directus/pull/6499) update dependency rollup to v2.52.3
([@renovate[bot]](https://github.com/apps/renovate))
- [#6497](https://github.com/directus/directus/pull/6497) update dependency eslint-plugin-vue to v7.12.1
([@renovate[bot]](https://github.com/apps/renovate))
- [#6482](https://github.com/directus/directus/pull/6482) Update vue to 3.1.2
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#6473](https://github.com/directus/directus/pull/6473) update dependency mitt to v3
([@renovate[bot]](https://github.com/apps/renovate))
- [#6470](https://github.com/directus/directus/pull/6470) update dependency fs-extra to v10
([@renovate[bot]](https://github.com/apps/renovate))
- [#6469](https://github.com/directus/directus/pull/6469) pin dependencies
([@renovate[bot]](https://github.com/apps/renovate))
- [#6468](https://github.com/directus/directus/pull/6468) update dependency @types/codemirror to v5.60.1
([@renovate[bot]](https://github.com/apps/renovate))
- [#6459](https://github.com/directus/directus/pull/6459) update dependency tinymce to v5.8.2
([@renovate[bot]](https://github.com/apps/renovate))
## v9.0.0-rc.80 (June 22, 2021)
### :sparkles: New Features

View File

@@ -1,4 +1,4 @@
const path = require('path');
const path = require('path/posix');
const fse = require('fs-extra');
const dirTree = require('directory-tree');

View File

@@ -19,8 +19,12 @@ If you're experiencing issues or think you have found a problem in Directus, be
8. Check for [existing Issues](https://github.com/directus/directus/issues?q=is%3Aissue) (and
[Discussions](https://github.com/directus/directus/discussions)) that match your problem
::: tip Source Contributors Those who have installed from source should also remove all dependencies
(`npx lerna clean -y`) and then reinstall them (`npm install`). :::
::: tip Source Contributors
Those who have installed from source should also remove all dependencies (`npx lerna clean -y`) and then reinstall them
(`npm install`).
:::
If you're still experiencing a problem after completing the above steps, you can chat through things on our
[community support](#community-support) or [report a bug](/contributing/introduction/#bug-reporting).

View File

@@ -86,7 +86,7 @@ export default {
};
```
## 2. Install Dependencies and Configure the Buildchain
## 2. Install Dependencies
Set up a package.json file by running:
@@ -95,38 +95,14 @@ npm init -y
```
To be read by the Admin App, your custom display's Vue component must first be bundled into a single `index.js` file. We
recommend bundling your code using Rollup. To install this and the other development dependencies, run this command:
recommend bundling your code using the directus-extension CLI from our `@directus/extension-sdk` package. The CLI
internally uses a Rollup configuration tailored specifically to bundling Directus extensions. To install the Extension
SDK, run this command:
```bash
npm i -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser rollup-plugin-vue @vue/compiler-sfc
npm i -D @directus/extension-sdk
```
You can then use the following Rollup configuration within `rollup.config.js`:
```js
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import vue from 'rollup-plugin-vue';
export default {
input: 'src/index.js',
output: {
format: 'es',
file: 'dist/index.js',
},
external: ['vue', '@directus/extension-sdk'],
plugins: [vue(), nodeResolve(), commonjs(), terser()],
};
```
::: tip Building multiple extensions
You can export an array of build configurations, so you can bundle (or even watch) multiple extensions at the same time.
See the [Rollup configuration file documentation](https://rollupjs.org/guide/en/#configuration-files) for more info.
:::
## 3. Develop Your Custom Display
The display itself is simply a function or a Vue component, providing a blank canvas for creating anything you need.
@@ -136,7 +112,7 @@ The display itself is simply a function or a Vue component, providing a blank ca
To build the display for use within Directus, run:
```bash
npx rollup -c
npx directus-extension build
```
Finally, move the output from your display's `dist` folder into your project's `/extensions/displays/my-custom-display`

View File

@@ -77,7 +77,7 @@ export default {
- `field` — The key of the field.
- `primaryKey` — The current item's primary key.
## 2. Install Dependencies and Configure the Buildchain
## 2. Install Dependencies
Set up a package.json file by running:
@@ -86,38 +86,14 @@ npm init -y
```
To be read by the Admin App, your custom interface's Vue component must first be bundled into a single `index.js` file.
We recommend bundling your code using Rollup. To install this and the other development dependencies, run this command:
We recommend bundling your code using the directus-extension CLI from our `@directus/extension-sdk` package. The CLI
internally uses a Rollup configuration tailored specifically to bundling Directus extensions. To install the Extension
SDK, run this command:
```bash
npm i -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser rollup-plugin-vue @vue/compiler-sfc
npm i -D @directus/extension-sdk
```
You can then use the following Rollup configuration within `rollup.config.js`:
```js
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import vue from 'rollup-plugin-vue';
export default {
input: 'src/index.js',
output: {
format: 'es',
file: 'dist/index.js',
},
external: ['vue', '@directus/extension-sdk'],
plugins: [vue(), nodeResolve(), commonjs(), terser()],
};
```
::: tip Building multiple extensions
You can export an array of build configurations, so you can bundle (or even watch) multiple extensions at the same time.
See the [Rollup configuration file documentation](https://rollupjs.org/guide/en/#configuration-files) for more info.
:::
## 3. Develop your Custom Interface
The interface itself is simply a Vue component, which provides an blank canvas for creating anything you need.
@@ -127,7 +103,7 @@ The interface itself is simply a Vue component, which provides an blank canvas f
To build the interface for use within Directus, run:
```bash
npx rollup -c
npx directus-extension build
```
Finally, move the output from your interface's `dist` folder into your project's

View File

@@ -82,7 +82,7 @@ The props you can use in an layout are:
- `filters` (sync) - The user's currently active filters.
- `search-query` (sync) - The user's current search query.
## 2. Install Dependencies and Configure the Buildchain
## 2. Install Dependencies
Set up a package.json file by running:
@@ -91,38 +91,14 @@ npm init -y
```
To be read by the Admin App, your custom layouts's Vue component must first be bundled into a single `index.js` file. We
recommend bundling your code using Rollup. To install this and the other development dependencies, run this command:
recommend bundling your code using the directus-extension CLI from our `@directus/extension-sdk` package. The CLI
internally uses a Rollup configuration tailored specifically to bundling Directus extensions. To install the Extension
SDK, run this command:
```bash
npm i -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser rollup-plugin-vue @vue/compiler-sfc
npm i -D @directus/extension-sdk
```
You can then use the following Rollup configuration within `rollup.config.js`:
```js
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import vue from 'rollup-plugin-vue';
export default {
input: 'src/index.js',
output: {
format: 'es',
file: 'dist/index.js',
},
external: ['vue', '@directus/extension-sdk'],
plugins: [vue(), nodeResolve(), commonjs(), terser()],
};
```
::: tip Building multiple extensions
You can export an array of build configurations, so you can bundle (or even watch) multiple extensions at the same time.
See the [Rollup configuration file documentation](https://rollupjs.org/guide/en/#configuration-files) for more info.
:::
## 3. Develop Your Custom Layout
The layout itself is simply a Vue component, which provides an blank canvas for creating anything you need.
@@ -132,7 +108,7 @@ The layout itself is simply a Vue component, which provides an blank canvas for
To build the layout for use within Directus, run:
```bash
npx rollup -c
npx directus-extension build
```
Finally, move the output from your layout's `dist` folder into your project's `/extensions/layouts/my-custom-layout`

View File

@@ -119,7 +119,7 @@ instance of the `collectionsStore` using `system.useCollectionsStore()`, but tha
If you setup a route with a parameter, you can pass it in as a prop.
## 2. Install Dependencies and Configure the Buildchain
## 2. Install Dependencies
Set up a package.json file by running:
@@ -128,38 +128,14 @@ npm init -y
```
To be read by the Admin App, your custom module's Vue component must first be bundled into a single `index.js` file. We
recommend bundling your code using Rollup. To install this and the other development dependencies, run this command:
recommend bundling your code using the directus-extension CLI from our `@directus/extension-sdk` package. The CLI
internally uses a Rollup configuration tailored specifically to bundling Directus extensions. To install the Extension
SDK, run this command:
```bash
npm i -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser rollup-plugin-vue @vue/compiler-sfc
npm i -D @directus/extension-sdk
```
You can then use the following Rollup configuration within `rollup.config.js`:
```js
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import vue from 'rollup-plugin-vue';
export default {
input: 'src/index.js',
output: {
format: 'es',
file: 'dist/index.js',
},
external: ['vue', '@directus/extension-sdk'],
plugins: [vue(), nodeResolve(), commonjs(), terser()],
};
```
::: tip Building multiple extensions
You can export an array of build configurations, so you can bundle (or even watch) multiple extensions at the same time.
See the [Rollup configuration file documentation](https://rollupjs.org/guide/en/#configuration-files) for more info.
:::
## 3. Develop Your Custom Module
The module itself is simply a Vue component, which provides an blank canvas for creating anything you need.
@@ -169,7 +145,7 @@ The module itself is simply a Vue component, which provides an blank canvas for
To build the module for use within Directus, run:
```bash
npx rollup -c
npx directus-extension build
```
Finally, move the output from your module's `dist` folder into your project's `/extensions/modules/my-custom-module`

View File

@@ -1,7 +1,7 @@
{
"name": "@directus/docs",
"private": false,
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "",
"main": "dist/index.js",
"scripts": {

View File

@@ -151,6 +151,30 @@ directus.transport.url = 'https://api2.example.com';
You can tap into the transport through `directus.transport`. If you are using the (default) `AxiosTransport`, you can
access axios through `directus.transport.axios`.
#### Intercepting requests and responses
Axios transport offers a wrapper around Axios interceptors to make it easy for you to inject/eject interceptors.
```ts
const requestInterceptor = directus.transport.requests.intercept((config) => {
config.headers['My-Custom-Header'] = 'Header value';
return config;
});
// If you don't want the interceptor anymore, remove it
requestInterceptor.eject();
```
```ts
const responseInterceptor = directus.transport.responses.intercept((response) => {
console.log('Response received', { response });
return response;
});
// If you don't want the interceptor anymore, remove it
responseInterceptor.eject();
```
## Items
You can get an instance of the item handler by providing the collection (and type, in the case of TypeScript) to the

View File

@@ -5,7 +5,7 @@
"docs",
"api"
],
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"command": {
"bootstrap": {
"npmClientArgs": [

47090
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,17 +31,17 @@
"@types/listr": "0.14.3",
"@types/node": "15.12.2",
"@types/supertest": "2.0.11",
"@typescript-eslint/eslint-plugin": "4.28.0",
"@typescript-eslint/parser": "4.28.0",
"@typescript-eslint/eslint-plugin": "4.28.1",
"@typescript-eslint/parser": "4.28.1",
"axios": "0.21.1",
"dockerode": "3.3.0",
"eslint": "7.29.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-prettier-vue": "3.1.0",
"eslint-plugin-vue": "7.11.1",
"eslint-plugin-vue": "7.12.1",
"globby": "11.0.4",
"jest": "27.0.5",
"jest": "27.0.6",
"knex": "0.95.6",
"lerna": "4.0.0",
"lint-staged": "11.0.0",
@@ -51,8 +51,8 @@
"npm-run-all": "4.1.5",
"oracledb": "5.2.0",
"pg": "8.6.0",
"prettier": "2.3.1",
"simple-git-hooks": "2.4.1",
"prettier": "2.3.2",
"simple-git-hooks": "2.5.1",
"sqlite3": "5.0.2",
"stylelint": "13.13.1",
"stylelint-config-prettier": "8.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/cli",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "The official Directus CLI",
"repository": {
"type": "git",
@@ -41,8 +41,8 @@
"author": "João Biondo <wolfulus@gmail.com>",
"license": "MIT",
"dependencies": {
"@directus/format-title": "9.0.0-rc.80",
"@directus/sdk": "9.0.0-rc.80",
"@directus/format-title": "9.0.0-rc.81",
"@directus/sdk": "9.0.0-rc.81",
"@types/yargs": "^17.0.0",
"app-module-path": "^2.2.0",
"chalk": "^4.1.0",
@@ -81,10 +81,10 @@
"@types/jest": "26.0.23",
"@types/js-yaml": "4.0.1",
"@types/marked-terminal": "3.1.1",
"jest": "27.0.5",
"jest": "27.0.6",
"nock": "13.1.0",
"npm-run-all": "4.1.5",
"prettier": "2.3.1",
"prettier": "2.3.2",
"rimraf": "3.0.2",
"ts-jest": "27.0.3",
"ts-node": "10.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "create-directus-project",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "A small installer util that will create a directory, add boilerplate folders, and install Directus through npm.",
"main": "lib/index.js",
"bin": "./lib/index.js",
@@ -12,7 +12,7 @@
"license": "GPL-3.0-only",
"dependencies": {
"chalk": "^4.1.1",
"commander": "^7.2.0",
"commander": "^8.0.0",
"execa": "^5.1.1",
"fs-extra": "^10.0.0",
"ora": "^5.4.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/drive-azure",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "Azure Blob driver for @directus/drive",
"license": "MIT",
"main": "dist/index.js",
@@ -35,7 +35,7 @@
],
"dependencies": {
"@azure/storage-blob": "^12.6.0",
"@directus/drive": "9.0.0-rc.80",
"@directus/drive": "9.0.0-rc.81",
"normalize-path": "^3.0.0"
},
"devDependencies": {
@@ -45,7 +45,7 @@
"@types/normalize-path": "3.0.0",
"dotenv": "10.0.0",
"fs-extra": "10.0.0",
"jest": "27.0.5",
"jest": "27.0.6",
"npm-run-all": "4.1.5",
"ts-jest": "27.0.3",
"typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/drive-gcs",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "Google Cloud Storage driver for @directus/drive",
"license": "MIT",
"main": "dist/index.js",
@@ -33,7 +33,7 @@
"dev": "npm run build -- -w --preserveWatchOutput --incremental"
},
"dependencies": {
"@directus/drive": "9.0.0-rc.80",
"@directus/drive": "9.0.0-rc.81",
"@google-cloud/storage": "^5.8.5",
"normalize-path": "^3.0.0"
},
@@ -44,7 +44,7 @@
"@types/node": "15.12.2",
"@types/normalize-path": "3.0.0",
"dotenv": "10.0.0",
"jest": "27.0.5",
"jest": "27.0.6",
"npm-run-all": "4.1.5",
"ts-jest": "27.0.3",
"typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/drive-s3",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "AWS S3 driver for @directus/drive",
"license": "MIT",
"main": "dist/index.js",
@@ -34,7 +34,7 @@
"dev": "npm run build -- -w --preserveWatchOutput --incremental"
},
"dependencies": {
"@directus/drive": "9.0.0-rc.80",
"@directus/drive": "9.0.0-rc.81",
"aws-sdk": "^2.928.0",
"normalize-path": "^3.0.0"
},
@@ -46,7 +46,7 @@
"@types/normalize-path": "3.0.0",
"dotenv": "10.0.0",
"fs-extra": "10.0.0",
"jest": "27.0.5",
"jest": "27.0.6",
"npm-run-all": "4.1.5",
"ts-jest": "27.0.3",
"typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/drive",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "Flexible and Fluent way to manage storage in Node.js.",
"license": "MIT",
"main": "dist/index.js",
@@ -54,7 +54,7 @@
"@types/jest": "26.0.23",
"@types/node": "15.12.2",
"dotenv": "10.0.0",
"jest": "27.0.5",
"jest": "27.0.6",
"npm-run-all": "4.1.5",
"ts-jest": "27.0.3",
"typescript": "4.3.4"

2
packages/extension-sdk/cli.js Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('./dist/cjs/cli/index.js');

View File

@@ -1,23 +1,35 @@
{
"name": "@directus/extension-sdk",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "A toolkit to develop extensions to extend Directus.",
"main": "dist/cjs/index.js",
"exports": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"bin": {
"directus-extension": "cli.js"
},
"scripts": {
"build": "run-p build:*",
"build": "npm run cleanup && run-p build:*",
"build:esm": "tsc --project ./tsconfig.json --module ES2015 --outDir ./dist/esm",
"build:cjs": "tsc --project ./tsconfig.json --module CommonJS --outDir ./dist/cjs",
"cleanup": "rimraf ./dist",
"dev": "npm run build -- -- -w --preserveWatchOutput --incremental"
"dev": "npm run build -- -- -w --preserveWatchOutput --incremental",
"prepublishOnly": "npm run build"
},
"author": "Nicola Krumschmidt",
"gitHead": "24621f3934dc77eb23441331040ed13c676ceffd",
"dependencies": {
"@directus/shared": "9.0.0-rc.80"
"@directus/shared": "9.0.0-rc.81",
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@vue/compiler-sfc": "^3.1.1",
"commander": "^8.0.0",
"ora": "^5.4.0",
"rollup": "^2.51.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-vue": "^6.0.0"
},
"devDependencies": {
"npm-run-all": "4.1.5",

View File

@@ -0,0 +1,28 @@
/* eslint-disable no-console */
import ora from 'ora';
import { rollup } from 'rollup';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import vue from 'rollup-plugin-vue';
import { SHARED_DEPS } from '@directus/shared/constants';
export default async function build(options: { input: string; output: string }): Promise<void> {
const spinner = ora('Building Directus extension...').start();
const bundle = await rollup({
input: options.input,
external: SHARED_DEPS,
plugins: [vue(), nodeResolve(), commonjs(), terser()],
});
await bundle.write({
format: 'es',
file: options.output,
});
await bundle.close();
spinner.succeed('Done');
}

View File

@@ -0,0 +1,18 @@
import { Command } from 'commander';
import build from './commands/build';
const pkg = require('../../../package.json');
const program = new Command();
program.name('directus-extension').usage('[command] [options]');
program.version(pkg.version, '-v, --version');
program
.command('build')
.description('Bundle a Directus extension to a single entrypoint')
.option('-i, --input <file>', 'change the default entrypoint', 'src/index.js')
.option('-o, --output <file>', 'change the default output file', 'dist/index.js')
.action(build);
program.parse(process.argv);

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/format-title",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "Custom string formatter that converts any string into [Title Case](http://www.grammar-monster.com/lessons/capital_letters_title_case.htm)",
"keywords": [
"title-case",
@@ -37,7 +37,7 @@
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "13.0.0",
"rimraf": "3.0.2",
"rollup": "2.52.2",
"rollup": "2.52.3",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-typescript2": "0.30.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/gatsby-source-directus",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "Source plugin for pulling data into Gatsby from a Directus API.",
"author": "João Biondo <wolfulus@gmail.com>",
"license": "MIT",

View File

@@ -1,13 +1,14 @@
{
"name": "@directus/schema",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "Utility for extracting information about existing DB schema",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc --build && echo \"Built successfully\"",
"prepare": "npm run build",
"dev": "npm-watch build"
"dev": "npm-watch build",
"prepublishOnly": "npm run build"
},
"watch": {
"build": {

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/sdk",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "The official Directus SDK for use in JavaScript!",
"repository": {
"type": "git",
@@ -53,13 +53,13 @@
"@types/jest": "26.0.23",
"argon2": "0.28.2",
"dotenv": "10.0.0",
"jest": "27.0.5",
"jest": "27.0.6",
"jest-environment-jsdom-global": "2.0.4",
"mockdate": "3.0.5",
"nock": "13.1.0",
"npm-run-all": "4.1.5",
"rimraf": "3.0.2",
"rollup": "2.52.2",
"rollup": "2.52.3",
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-terser": "7.0.2",

View File

@@ -1,9 +1,22 @@
import { IStorage } from '../../storage';
import axios, { AxiosInstance } from 'axios';
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ITransport, TransportMethods, TransportResponse, TransportError, TransportOptions } from '../../transport';
export type AxiosTransportRefreshHandler = () => Promise<void>;
export type AxiosEjector = {
eject(): void;
};
export type AxiosInterceptorFunction<T> = (
onFulfilled?: (value: T) => T | Promise<T>,
onRejected?: (error: any) => any
) => AxiosEjector;
export type AxiosInterceptor<T> = {
intercept: AxiosInterceptorFunction<T>;
};
/**
* Axios transport implementation
*/
@@ -37,10 +50,36 @@ export class AxiosTransport implements ITransport {
return this._axios;
}
private async request<T = any, R = any>(
get requests(): AxiosInterceptor<AxiosRequestConfig> {
return {
intercept: (onFulfilled, onRejected) => {
const id = this._axios.interceptors.request.use(onFulfilled, onRejected);
return {
eject: () => {
this._axios.interceptors.request.eject(id);
},
};
},
};
}
get responses(): AxiosInterceptor<AxiosResponse> {
return {
intercept: (onFulfilled, onRejected) => {
const id = this._axios.interceptors.response.use(onFulfilled, onRejected);
return {
eject: () => {
this._axios.interceptors.response.eject(id);
},
};
},
};
}
protected async request<T = any, R = any>(
method: TransportMethods,
path: string,
data?: any,
data?: Record<string, any>,
options?: TransportOptions
): Promise<TransportResponse<T, R>> {
try {

View File

@@ -185,4 +185,61 @@ describe('axios transport', function () {
const response3 = await transport.get('/auth');
expect(response3.data?.auth).toBe(false);
});
it('can inject and eject request interceptors', async function () {
nock(URL)
.defaultReplyHeaders({
'x-new-header-value': (req) => {
return (req.getHeader('x-new-header') || '').toString();
},
})
.get('/test')
.times(3)
.reply(203);
const storage = new MemoryStorage();
const transport = new AxiosTransport(URL, storage);
const response1 = await transport.get('/test');
expect(response1.headers['x-new-header-value']).toBe('');
const interceptor1 = transport.requests.intercept((config) => {
config.headers['x-new-header'] = 'Testing';
return config;
});
const response2 = await transport.get('/test');
expect(response2.headers['x-new-header-value']).toBe('Testing');
interceptor1.eject();
const response3 = await transport.get('/test');
expect(response3.headers['x-new-header-value']).toBe('');
});
it('can inject and eject response interceptors', async function () {
nock(URL)
.get('/test')
.times(3)
.reply(203, () => ({ data: 'original data' }));
const storage = new MemoryStorage();
const transport = new AxiosTransport(URL, storage);
const response1 = await transport.get('/test');
expect(response1.data).toBe('original data');
const interceptor1 = transport.responses.intercept((response) => {
response.data = { data: 'injected data' };
return response;
});
const response2 = await transport.get('/test');
expect(response2.data).toBe('injected data');
interceptor1.eject();
const response3 = await transport.get('/test');
expect(response3.data).toBe('original data');
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/shared",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "Code shared between all directus packages.",
"exports": {
"./composables": {
@@ -21,17 +21,18 @@
}
},
"scripts": {
"build": "run-p build:*",
"build": "npm run cleanup && run-p build:*",
"build:esm": "tsc --project ./tsconfig.json --module ES2015 --outDir ./dist/esm",
"build:cjs": "tsc --project ./tsconfig.json --module CommonJS --outDir ./dist/cjs",
"cleanup": "rimraf ./dist",
"dev": "npm run build -- -- -w --preserveWatchOutput --incremental"
"dev": "npm run build -- -- -w --preserveWatchOutput --incremental",
"prepublishOnly": "npm run build"
},
"author": "Nicola Krumschmidt",
"gitHead": "24621f3934dc77eb23441331040ed13c676ceffd",
"dependencies": {
"fs-extra": "7.0.1",
"vue": "3.1.1"
"fs-extra": "10.0.0",
"vue": "3.1.2"
},
"devDependencies": {
"npm-run-all": "4.1.5",

View File

@@ -3,7 +3,7 @@ import fse from 'fs-extra';
import { pluralize } from './pluralize';
import { EXTENSION_TYPES } from '../constants';
export async function ensureExtensionsDirs(extensionsPath: string): Promise<void> {
export async function ensureExtensionDirs(extensionsPath: string): Promise<void> {
for (const extensionType of EXTENSION_TYPES) {
const dirPath = path.resolve(extensionsPath, pluralize(extensionType));
await fse.ensureDir(dirPath);

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/specs",
"version": "9.0.0-rc.80",
"version": "9.0.0-rc.81",
"description": "OpenAPI Specification of the Directus API",
"main": "index.js",
"scripts": {