mirror of
https://github.com/directus/directus.git
synced 2026-01-30 02:07:57 -05:00
Merge branch 'main' into aggregation
This commit is contained in:
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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, ...).
|
||||
|
||||
-->
|
||||
26
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
26
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
return require('./dist/cli/index.js');
|
||||
require('./dist/cli/index.js');
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -81,6 +81,7 @@
|
||||
- id
|
||||
- first_name
|
||||
- last_name
|
||||
- last_page
|
||||
- email
|
||||
- password
|
||||
- location
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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}"`);
|
||||
}
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
124
app/src/interfaces/group-divider/group-divider.vue
Normal file
124
app/src/interfaces/group-divider/group-divider.vue
Normal 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>
|
||||
76
app/src/interfaces/group-divider/index.ts
Normal file
76
app/src/interfaces/group-divider/index.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
67
app/src/interfaces/group-raw/group-raw.vue
Normal file
67
app/src/interfaces/group-raw/group-raw.vue
Normal 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>
|
||||
13
app/src/interfaces/group-raw/index.ts
Normal file
13
app/src/interfaces/group-raw/index.ts
Normal 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: [],
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: عرض القالب
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: 값 입력
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
44
changelog.md
44
changelog.md
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const path = require('path');
|
||||
const path = require('path/posix');
|
||||
const fse = require('fs-extra');
|
||||
const dirTree = require('directory-tree');
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
47090
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
2
packages/extension-sdk/cli.js
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require('./dist/cjs/cli/index.js');
|
||||
@@ -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",
|
||||
|
||||
28
packages/extension-sdk/src/cli/commands/build.ts
Normal file
28
packages/extension-sdk/src/cli/commands/build.ts
Normal 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');
|
||||
}
|
||||
18
packages/extension-sdk/src/cli/index.ts
Normal file
18
packages/extension-sdk/src/cli/index.ts
Normal 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);
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user