Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-06-23 18:09:00 -04:00
196 changed files with 5651 additions and 1811 deletions

5
.github/CODEOWNERS vendored
View File

@@ -1,6 +1,11 @@
* @rijkvanzanten
/docs/*.md @benhaynes
/packages/cli @WoLfulus
/packages/sdk @WoLfulus
/packages/gatsby-source-directus @WoLfulus
/packages/shared @nickrum
/packages/extension-sdk @nickrum
/app/vite.config.js @nickrum

61
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Bug Report
description: Create a report to help us improve
labels: 'Bug (Potential)'
body:
- type: markdown
attributes:
value: Hi, thank you for taking the time to create an issue!
- type: markdown
attributes:
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.
- type: input
attributes:
label: What version of Directus are you using?
description: 'For example: v9.1.4'
validations:
required: true
- type: input
attributes:
label: What version of Node.js are you using?
description: 'For example: 12.0.0'
validations:
required: true
- type: input
attributes:
label: What database are you using?
description: 'For example: Postgres 13, SQLite 3.31.0'
validations:
required: true
- type: input
attributes:
label: What browser are you using?
description: 'For example: Chrome, Safari'
validations:
required: true
- type: input
attributes:
label: What operating system are you using?
description: 'For example: macOS, Windows'
validations:
required: true
- type: input
attributes:
label: How are you deploying Directus?
description: 'For example: running locally, Docker, PaaS'
validations:
required: true
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior. Contributors should be able to follow the steps provided in order to reproduce the bug.
validations:
required: true

View File

@@ -10,7 +10,7 @@ jobs:
fail-fast: false
matrix:
db: ['mssql', 'mysql', 'postgres', 'maria', 'sqlite3']
node-version: ['12-alpine', '14-alpine', '15-alpine']
node-version: ['12-alpine', '14-alpine', '16-alpine']
env:
CACHED_IMAGE: ghcr.io/directus/directus-e2e-test-cache:${{ matrix.node-version }}
steps:
@@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '15'
node-version: '16'
- name: restore node_modules cache
uses: actions/cache@v2
with:

View File

@@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '15'
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -1,6 +1,6 @@
# NOTE: Testing Only. DO NOT use this in production
ARG NODE_VERSION=15-alpine
ARG NODE_VERSION=16-alpine
FROM node:${NODE_VERSION}

View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-rc.76",
"version": "9.0.0-rc.80",
"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.",
@@ -59,6 +59,9 @@
"cli": "cross-env DIRECTUS_DEV=true NODE_ENV=development ts-node --script-mode --transpile-only src/cli/index.ts",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=12.0.0"
},
"files": [
"dist",
"LICENSE",
@@ -66,15 +69,18 @@
"example.env"
],
"dependencies": {
"@directus/app": "9.0.0-rc.76",
"@directus/drive": "9.0.0-rc.76",
"@directus/drive-azure": "9.0.0-rc.76",
"@directus/drive-gcs": "9.0.0-rc.76",
"@directus/drive-s3": "9.0.0-rc.76",
"@directus/format-title": "9.0.0-rc.76",
"@directus/schema": "9.0.0-rc.76",
"@directus/specs": "9.0.0-rc.76",
"@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",
"@godaddy/terminus": "^4.9.0",
"@rollup/plugin-alias": "^3.1.2",
"@rollup/plugin-virtual": "^2.0.3",
"argon2": "^0.28.1",
"async": "^3.2.0",
"async-mutex": "^0.3.1",
@@ -129,6 +135,7 @@
"qs": "^6.9.4",
"rate-limiter-flexible": "^2.2.2",
"resolve-cwd": "^3.0.0",
"rollup": "^2.52.1",
"sharp": "^0.28.3",
"stream-json": "^1.7.1",
"uuid": "^8.3.2",
@@ -162,7 +169,7 @@
"@types/express-pino-logger": "4.0.2",
"@types/express-session": "1.17.3",
"@types/fs-extra": "9.0.11",
"@types/inquirer": "7.3.1",
"@types/inquirer": "7.3.2",
"@types/js-yaml": "4.0.1",
"@types/json2csv": "5.0.2",
"@types/jsonwebtoken": "8.5.2",

View File

@@ -56,7 +56,7 @@ export default async function createApp(): Promise<express.Application> {
await initializeExtensions();
await registerExtensionHooks();
registerExtensionHooks();
const app = express();
@@ -170,7 +170,7 @@ export default async function createApp(): Promise<express.Application> {
// Register custom hooks / endpoints
await emitAsyncSafe('routes.custom.init.before', { app });
await registerExtensionEndpoints(customRouter);
registerExtensionEndpoints(customRouter);
await emitAsyncSafe('routes.custom.init.after', { app });
app.use(notFoundHandler);

View File

@@ -6,31 +6,39 @@ import { getConfigFromEnv } from './utils/get-config-from-env';
import { validateEnv } from './utils/validate-env';
let cache: Keyv | null = null;
let schemaCache: Keyv | null = null;
if (env.CACHE_ENABLED === true) {
validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']);
cache = getKeyvInstance();
cache.on('error', (err) => logger.error(err));
export function getCache(): { cache: Keyv | null; schemaCache: Keyv | null } {
if (env.CACHE_ENABLED === true && cache === null) {
validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']);
cache = getKeyvInstance(ms(env.CACHE_TTL as string));
cache.on('error', (err) => logger.error(err));
}
if (env.CACHE_SCHEMA !== false && schemaCache === null) {
schemaCache = getKeyvInstance(typeof env.CACHE_SCHEMA === 'string' ? ms(env.CACHE_SCHEMA) : undefined);
schemaCache.on('error', (err) => logger.error(err));
}
return { cache, schemaCache };
}
export default cache;
function getKeyvInstance() {
function getKeyvInstance(ttl: number | undefined): Keyv {
switch (env.CACHE_STORE) {
case 'redis':
return new Keyv(getConfig('redis'));
return new Keyv(getConfig('redis', ttl));
case 'memcache':
return new Keyv(getConfig('memcache'));
return new Keyv(getConfig('memcache', ttl));
case 'memory':
default:
return new Keyv(getConfig());
return new Keyv(getConfig('memory', ttl));
}
}
function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory'): Options<any> {
function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory', ttl: number | undefined): Options<any> {
const config: Options<any> = {
namespace: env.CACHE_NAMESPACE,
ttl: ms(env.CACHE_TTL as string),
ttl,
};
if (store === 'redis') {

View File

@@ -1,4 +1,4 @@
import { Transformation } from './types/assets';
import { Transformation } from './types';
export const SYSTEM_ASSET_ALLOW_LIST: Transformation[] = [
{

View File

@@ -1,28 +1,24 @@
import express, { Router } from 'express';
import env from '../env';
import { RouteNotFoundException } from '../exceptions';
import { listExtensions } from '../extensions';
import { respond } from '../middleware/respond';
import { Router } from 'express';
import asyncHandler from '../utils/async-handler';
import { RouteNotFoundException } from '../exceptions';
import { listExtensions, getAppExtensionSource } from '../extensions';
import { respond } from '../middleware/respond';
import { depluralize } from '@directus/shared/utils';
import { AppExtensionType, Plural } from '@directus/shared/types';
import { APP_EXTENSION_TYPES } from '@directus/shared/constants';
const router = Router();
const extensionsPath = env.EXTENSIONS_PATH as string;
const appExtensions = ['interfaces', 'layouts', 'displays', 'modules'];
router.get(
['/:type', '/:type/*'],
'/:type',
asyncHandler(async (req, res, next) => {
if (appExtensions.includes(req.params.type) === false) {
const type = depluralize(req.params.type as Plural<AppExtensionType>);
if (APP_EXTENSION_TYPES.includes(type) === false) {
throw new RouteNotFoundException(req.path);
}
return next();
}),
express.static(extensionsPath),
asyncHandler(async (req, res, next) => {
const extensions = await listExtensions(req.params.type);
const extensions = listExtensions(type);
res.locals.payload = {
data: extensions,
@@ -33,4 +29,23 @@ router.get(
respond
);
router.get(
'/:type/index.js',
asyncHandler(async (req, res) => {
const type = depluralize(req.params.type as Plural<AppExtensionType>);
if (APP_EXTENSION_TYPES.includes(type) === false) {
throw new RouteNotFoundException(req.path);
}
const extensionSource = getAppExtensionSource(type);
if (extensionSource === undefined) {
throw new RouteNotFoundException(req.path);
}
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
res.end(extensionSource);
})
);
export default router;

View File

@@ -51,6 +51,7 @@ const defaults: Record<string, any> = {
CACHE_NAMESPACE: 'system-cache',
CACHE_AUTO_PURGE: false,
CACHE_CONTROL_S_MAXAGE: '0',
CACHE_SCHEMA: true,
OAUTH_PROVIDERS: '',

View File

@@ -1,91 +1,139 @@
import express, { Router } from 'express';
import { ensureDir } from 'fs-extra';
import path from 'path';
import { AppExtensionType, Extension, ExtensionType } from '@directus/shared/types';
import {
generateExtensionsEntry,
getLocalExtensions,
getPackageExtensions,
pluralize,
resolvePackage,
} from '@directus/shared/utils';
import { APP_EXTENSION_TYPES, EXTENSION_TYPES, SHARED_DEPS } from '@directus/shared/constants';
import getDatabase from './database';
import emitter from './emitter';
import env from './env';
import * as exceptions from './exceptions';
import { ServiceUnavailableException } from './exceptions';
import logger from './logger';
import * as services from './services';
import { EndpointRegisterFunction, HookRegisterFunction } from './types';
import { HookRegisterFunction, EndpointRegisterFunction } from './types';
import fse from 'fs-extra';
import { getSchema } from './utils/get-schema';
import listFolders from './utils/list-folders';
import * as services from './services';
import { schedule, validate } from 'node-cron';
import { REGEX_BETWEEN_PARENS } from './constants';
import { rollup } from 'rollup';
// @TODO Remove this once a new version of @rollup/plugin-virtual has been released
// @ts-expect-error
import virtual from '@rollup/plugin-virtual';
import alias from '@rollup/plugin-alias';
export async function ensureFoldersExist(): Promise<void> {
const folders = ['endpoints', 'hooks', 'interfaces', 'modules', 'layouts', 'displays'];
let extensions: Extension[] = [];
let extensionBundles: Partial<Record<AppExtensionType, string>> = {};
for (const folder of folders) {
const folderPath = path.resolve(env.EXTENSIONS_PATH, folder);
export async function initializeExtensions(): Promise<void> {
await ensureDirsExist();
extensions = await getExtensions();
extensionBundles = await generateExtensionBundles();
logger.info(`Loaded extensions: ${listExtensions().join(', ')}`);
}
export function listExtensions(type?: ExtensionType): string[] {
if (type === undefined) {
return extensions.map((extension) => extension.name);
} else {
return extensions.filter((extension) => extension.type === type).map((extension) => extension.name);
}
}
export function getAppExtensionSource(type: AppExtensionType): string | undefined {
return extensionBundles[type];
}
export function registerExtensionEndpoints(router: Router): void {
const endpoints = extensions.filter((extension) => extension.type === 'endpoint');
registerEndpoints(endpoints, router);
}
export function registerExtensionHooks(): void {
const hooks = extensions.filter((extension) => extension.type === 'hook');
registerHooks(hooks);
}
async function getExtensions(): Promise<Extension[]> {
const packageExtensions = await getPackageExtensions('.');
const localExtensions = await getLocalExtensions(env.EXTENSIONS_PATH);
return [...packageExtensions, ...localExtensions];
}
async function generateExtensionBundles() {
const sharedDepsMapping = await getSharedDepsMapping(SHARED_DEPS);
const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
find: name,
replacement: path,
}));
const bundles: Partial<Record<AppExtensionType, string>> = {};
for (const extensionType of APP_EXTENSION_TYPES) {
const entry = generateExtensionsEntry(extensionType, extensions);
const bundle = await rollup({
input: 'entry',
external: SHARED_DEPS,
plugins: [virtual({ entry }), alias({ entries: internalImports })],
});
const { output } = await bundle.generate({ format: 'es' });
bundles[extensionType] = output[0].code;
await bundle.close();
}
return bundles;
}
async function ensureDirsExist() {
for (const extensionType of EXTENSION_TYPES) {
const dirPath = path.resolve(env.EXTENSIONS_PATH, pluralize(extensionType));
try {
await ensureDir(folderPath);
await fse.ensureDir(dirPath);
} catch (err) {
logger.warn(err);
}
}
}
export async function initializeExtensions(): Promise<void> {
await ensureFoldersExist();
}
async function getSharedDepsMapping(deps: string[]) {
const appDir = await fse.readdir(path.join(resolvePackage('@directus/app'), 'dist'));
export async function listExtensions(type: string): Promise<string[]> {
const extensionsPath = env.EXTENSIONS_PATH as string;
const location = path.join(extensionsPath, type);
const depsMapping: Record<string, string> = {};
for (const dep of deps) {
const depName = appDir.find((file) => dep.replace(/\//g, '_') === file.substring(0, file.indexOf('.')));
try {
return await listFolders(location);
} catch (err) {
if (err.code === 'ENOENT') {
throw new ServiceUnavailableException(`Extension folder "extensions/${type}" couldn't be opened`, {
service: 'extensions',
});
if (depName) {
depsMapping[dep] = `${env.PUBLIC_URL}/admin/${depName}`;
} else {
logger.warn(`Couldn't find extension internal dependency "${dep}"`);
}
throw err;
}
return depsMapping;
}
export async function registerExtensions(router: Router): Promise<void> {
await registerExtensionHooks();
await registerExtensionEndpoints(router);
}
export async function registerExtensionEndpoints(router: Router): Promise<void> {
let endpoints: string[] = [];
try {
endpoints = await listExtensions('endpoints');
registerEndpoints(endpoints, router);
} catch (err) {
logger.warn(err);
}
}
export async function registerExtensionHooks(): Promise<void> {
let hooks: string[] = [];
try {
hooks = await listExtensions('hooks');
registerHooks(hooks);
} catch (err) {
logger.warn(err);
}
}
function registerHooks(hooks: string[]) {
const extensionsPath = env.EXTENSIONS_PATH as string;
function registerHooks(hooks: Extension[]) {
for (const hook of hooks) {
try {
registerHook(hook);
} catch (error) {
logger.warn(`Couldn't register hook "${hook}"`);
logger.warn(`Couldn't register hook "${hook.name}"`);
logger.warn(error);
}
}
function registerHook(hook: string) {
const hookPath = path.resolve(extensionsPath, 'hooks', hook, 'index.js');
function registerHook(hook: Extension) {
const hookPath = path.resolve(hook.path, hook.entrypoint || '');
const hookInstance: HookRegisterFunction | { default?: HookRegisterFunction } = require(hookPath);
let register: HookRegisterFunction = hookInstance as HookRegisterFunction;
@@ -113,20 +161,18 @@ function registerHooks(hooks: string[]) {
}
}
function registerEndpoints(endpoints: string[], router: Router) {
const extensionsPath = env.EXTENSIONS_PATH as string;
function registerEndpoints(endpoints: Extension[], router: Router) {
for (const endpoint of endpoints) {
try {
registerEndpoint(endpoint);
} catch (error) {
logger.warn(`Couldn't register endpoint "${endpoint}"`);
logger.warn(`Couldn't register endpoint "${endpoint.name}"`);
logger.warn(error);
}
}
function registerEndpoint(endpoint: string) {
const endpointPath = path.resolve(extensionsPath, 'endpoints', endpoint, 'index.js');
function registerEndpoint(endpoint: Extension) {
const endpointPath = path.resolve(endpoint.path, endpoint.entrypoint || '');
const endpointInstance: EndpointRegisterFunction | { default?: EndpointRegisterFunction } = require(endpointPath);
let register: EndpointRegisterFunction = endpointInstance as EndpointRegisterFunction;
@@ -137,7 +183,7 @@ function registerEndpoints(endpoints: string[], router: Router) {
}
const scopedRouter = express.Router();
router.use(`/${endpoint}/`, scopedRouter);
router.use(`/${endpoint.name}/`, scopedRouter);
register(scopedRouter, { services, exceptions, env, database: getDatabase(), getSchema });
}

View File

@@ -78,10 +78,6 @@ const authenticate: RequestHandler = asyncHandler(async (req, res, next) => {
req.accountability.app = user.app_access === true || user.app_access == 1;
}
if (req.accountability?.user) {
await database('directus_users').update({ last_access: new Date() }).where({ id: req.accountability.user });
}
return next();
});

View File

@@ -1,11 +1,13 @@
import { RequestHandler } from 'express';
import cache from '../cache';
import { getCache } from '../cache';
import env from '../env';
import asyncHandler from '../utils/async-handler';
import { getCacheControlHeader } from '../utils/get-cache-headers';
import { getCacheKey } from '../utils/get-cache-key';
const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next) => {
const { cache } = getCache();
if (req.method.toLowerCase() !== 'get') return next();
if (env.CACHE_ENABLED !== true) return next();
if (!cache) return next();

View File

@@ -2,7 +2,7 @@ import { RequestHandler } from 'express';
import { Transform, transforms } from 'json2csv';
import ms from 'ms';
import { PassThrough } from 'stream';
import cache from '../cache';
import { getCache } from '../cache';
import env from '../env';
import asyncHandler from '../utils/async-handler';
import { getCacheKey } from '../utils/get-cache-key';
@@ -10,6 +10,8 @@ import { parse as toXML } from 'js2xmlparser';
import { getCacheControlHeader } from '../utils/get-cache-headers';
export const respond: RequestHandler = asyncHandler(async (req, res) => {
const { cache } = getCache();
if (
req.method.toLowerCase() === 'get' &&
env.CACHE_ENABLED === true &&

View File

@@ -185,6 +185,8 @@ export class AuthenticationService {
});
}
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: user.id });
emitStatus('success');
if (allowedAttempts !== null) {
@@ -230,6 +232,8 @@ export class AuthenticationService {
.update({ token: newRefreshToken, expires: refreshTokenExpiration })
.where({ token: refreshToken });
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: record.id });
return {
accessToken,
refreshToken: newRefreshToken,

View File

@@ -1,6 +1,6 @@
import SchemaInspector from '@directus/schema';
import { Knex } from 'knex';
import cache from '../cache';
import { getCache } from '../cache';
import { ALIAS_TYPES } from '../constants';
import getDatabase, { getSchemaInspector } from '../database';
import { systemCollectionRows } from '../database/system-data/collections';
@@ -9,6 +9,7 @@ import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import logger from '../logger';
import { FieldsService, RawField } from '../services/fields';
import { ItemsService, MutationOptions } from '../services/items';
import Keyv from 'keyv';
import {
AbstractServiceOptions,
Accountability,
@@ -29,12 +30,18 @@ export class CollectionsService {
accountability: Accountability | null;
schemaInspector: ReturnType<typeof SchemaInspector>;
schema: SchemaOverview;
cache: Keyv<any> | null;
schemaCache: Keyv<any> | null;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
this.schema = options.schema;
const { cache, schemaCache } = getCache();
this.cache = cache;
this.schemaCache = schemaCache;
}
/**
@@ -128,8 +135,12 @@ export class CollectionsService {
return payload.collection;
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
if (this.schemaCache) {
await this.schemaCache.clear();
}
return payload.collection;
@@ -156,8 +167,12 @@ export class CollectionsService {
return collectionNames;
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
if (this.schemaCache) {
await this.schemaCache.clear();
}
return collections;
@@ -416,8 +431,12 @@ export class CollectionsService {
await trx.schema.dropTable(collectionKey);
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
if (this.schemaCache) {
await this.schemaCache.clear();
}
return collectionKey;
@@ -443,8 +462,12 @@ export class CollectionsService {
}
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
if (this.schemaCache) {
await this.schemaCache.clear();
}
return collectionKeys;

View File

@@ -1,7 +1,7 @@
import SchemaInspector from '@directus/schema';
import { Knex } from 'knex';
import { Column } from 'knex-schema-inspector/dist/types/column';
import cache from '../cache';
import { getCache } from '../cache';
import { ALIAS_TYPES } from '../constants';
import getDatabase, { getSchemaInspector } from '../database';
import { systemFieldRows } from '../database/system-data/fields/';
@@ -18,6 +18,7 @@ import getLocalType from '../utils/get-local-type';
import { toArray } from '../utils/to-array';
import { isEqual } from 'lodash';
import { RelationsService } from './relations';
import Keyv from 'keyv';
export type RawField = DeepPartial<Field> & { field: string; type: typeof types[number] };
@@ -28,6 +29,8 @@ export class FieldsService {
payloadService: PayloadService;
schemaInspector: ReturnType<typeof SchemaInspector>;
schema: SchemaOverview;
cache: Keyv<any> | null;
schemaCache: Keyv<any> | null;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || getDatabase();
@@ -36,6 +39,10 @@ export class FieldsService {
this.itemsService = new ItemsService('directus_fields', options);
this.payloadService = new PayloadService('directus_fields', options);
this.schema = options.schema;
const { cache, schemaCache } = getCache();
this.cache = cache;
this.schemaCache = schemaCache;
}
private get hasReadAccess() {
@@ -244,8 +251,12 @@ export class FieldsService {
}
});
if (cache && env.CACHE_AUTO_PURGE) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE) {
await this.cache.clear();
}
if (this.schemaCache) {
await this.schemaCache.clear();
}
}
@@ -291,8 +302,12 @@ export class FieldsService {
}
}
if (cache && env.CACHE_AUTO_PURGE) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE) {
await this.cache.clear();
}
if (this.schemaCache) {
await this.schemaCache.clear();
}
return field.field;
@@ -396,8 +411,12 @@ export class FieldsService {
}
});
if (cache && env.CACHE_AUTO_PURGE) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE) {
await this.cache.clear();
}
if (this.schemaCache) {
await this.schemaCache.clear();
}
emitAsyncSafe(`fields.delete`, {

View File

@@ -7,7 +7,6 @@ import { extension } from 'mime-types';
import path from 'path';
import sharp from 'sharp';
import url from 'url';
import cache from '../cache';
import { emitAsyncSafe } from '../emitter';
import env from '../env';
import { ForbiddenException, ServiceUnavailableException } from '../exceptions';
@@ -121,8 +120,8 @@ export class FilesService extends ItemsService {
await sudoService.updateOne(primaryKey, payload, { emitEvents: false });
if (cache && env.CACHE_AUTO_PURGE) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE) {
await this.cache.clear();
}
emitAsyncSafe(`files.upload`, {
@@ -208,8 +207,8 @@ export class FilesService extends ItemsService {
}
}
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
return keys;

View File

@@ -416,6 +416,18 @@ export class GraphQLService {
_ncontains: {
type: GraphQLString,
},
_starts_with: {
type: GraphQLString,
},
_nstarts_with: {
type: GraphQLString,
},
_ends_with: {
type: GraphQLString,
},
_nends_with: {
type: GraphQLString,
},
_in: {
type: new GraphQLList(GraphQLString),
},
@@ -1265,10 +1277,10 @@ export class GraphQLService {
},
}),
resolve: async () => ({
interfaces: await listExtensions('interfaces'),
displays: await listExtensions('displays'),
layouts: await listExtensions('layouts'),
modules: await listExtensions('modules'),
interfaces: listExtensions('interface'),
displays: listExtensions('display'),
layouts: listExtensions('layout'),
modules: listExtensions('module'),
}),
},
server_specs_oas: {

View File

@@ -1,6 +1,7 @@
import { Knex } from 'knex';
import { clone, cloneDeep, merge, pick, without } from 'lodash';
import cache from '../cache';
import { getCache } from '../cache';
import Keyv from 'keyv';
import getDatabase from '../database';
import runAST from '../database/run-ast';
import emitter, { emitAsyncSafe } from '../emitter';
@@ -52,6 +53,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
accountability: Accountability | null;
eventScope: string;
schema: SchemaOverview;
cache: Keyv<any> | null;
constructor(collection: string, options: AbstractServiceOptions) {
this.collection = collection;
@@ -59,6 +61,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
this.accountability = options.accountability || null;
this.eventScope = this.collection.startsWith('directus_') ? this.collection.substring(9) : 'items';
this.schema = options.schema;
this.cache = getCache().cache;
return this;
}
@@ -208,8 +211,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
});
}
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
return primaryKey;
@@ -236,8 +239,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
return primaryKeys;
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
return primaryKeys;
@@ -524,8 +527,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
}
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
if (opts?.emitEvents !== false) {
@@ -589,8 +592,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
return primaryKeys;
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
return primaryKeys;
@@ -673,8 +676,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
}
});
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
await this.cache.clear();
}
if (opts?.emitEvents !== false) {
@@ -717,6 +720,11 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
}
for (const [name, field] of fields) {
if (this.schema.collections[this.collection].primary === name) {
defaults[name] = null;
continue;
}
defaults[name] = field.defaultValue;
}

View File

@@ -9,6 +9,8 @@ import SchemaInspector from '@directus/schema';
import { ForeignKey } from 'knex-schema-inspector/dist/types/foreign-key';
import getDatabase, { getSchemaInspector } from '../database';
import { getDefaultIndexName } from '../utils/get-default-index-name';
import { getCache } from '../cache';
import Keyv from 'keyv';
export class RelationsService {
knex: Knex;
@@ -17,6 +19,7 @@ export class RelationsService {
accountability: Accountability | null;
schema: SchemaOverview;
relationsItemService: ItemsService<RelationMeta>;
schemaCache: Keyv<any> | null;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || getDatabase();
@@ -31,6 +34,8 @@ export class RelationsService {
// allowed to extract the relations regardless of permissions to directus_relations. This
// happens in `filterForbidden` down below
});
this.schemaCache = getCache().schemaCache;
}
async readAll(collection?: string, opts?: QueryOptions): Promise<Relation[]> {
@@ -183,6 +188,10 @@ export class RelationsService {
await relationsItemService.createOne(metaRow);
});
if (this.schemaCache) {
await this.schemaCache.clear();
}
}
/**
@@ -259,6 +268,10 @@ export class RelationsService {
}
}
});
if (this.schemaCache) {
await this.schemaCache.clear();
}
}
/**
@@ -296,6 +309,10 @@ export class RelationsService {
await trx('directus_relations').delete().where({ many_collection: collection, many_field: field });
}
});
if (this.schemaCache) {
await this.schemaCache.clear();
}
}
/**

View File

@@ -6,7 +6,7 @@ import os from 'os';
import { performance } from 'perf_hooks';
// @ts-ignore
import { version } from '../../package.json';
import cache from '../cache';
import { getCache } from '../cache';
import getDatabase, { hasDatabaseConnection } from '../database';
import env from '../env';
import logger from '../logger';
@@ -189,6 +189,8 @@ export class ServerService {
return {};
}
const { cache } = getCache();
const checks: Record<string, HealthCheck[]> = {
'cache:responseTime': [
{

View File

@@ -2,7 +2,6 @@ import argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import { Knex } from 'knex';
import { clone } from 'lodash';
import cache from '../cache';
import getDatabase from '../database';
import env from '../env';
import {
@@ -287,8 +286,8 @@ export class UsersService extends ItemsService {
await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id });
if (cache && env.CACHE_AUTO_PURGE) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE) {
await this.cache.clear();
}
}
@@ -343,8 +342,8 @@ export class UsersService extends ItemsService {
await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id });
if (cache && env.CACHE_AUTO_PURGE) {
await cache.clear();
if (this.cache && env.CACHE_AUTO_PURGE) {
await this.cache.clear();
}
}

View File

@@ -361,6 +361,22 @@ export function applyFilter(
dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
}
if (operator === '_starts_with') {
dbQuery[logical].where(key, 'like', `${compareValue}%`);
}
if (operator === '_nstarts_with') {
dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
}
if (operator === '_ends_with') {
dbQuery[logical].where(key, 'like', `%${compareValue}`);
}
if (operator === '_nends_with') {
dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
}
if (operator === '_gt') {
dbQuery[logical].where(selectionRaw, '>', compareValue);
}

View File

@@ -1,5 +1,6 @@
import BaseJoi, { AnySchema } from 'joi';
import { Filter } from '../types';
import { escapeRegExp } from 'lodash';
const Joi: typeof BaseJoi = BaseJoi.extend({
type: 'string',
@@ -92,6 +93,22 @@ function getJoi(operator: string, value: any) {
return Joi.string().ncontains(value);
}
if (operator === '_starts_with') {
return Joi.string().pattern(new RegExp(`^${escapeRegExp(value)}.*`), { name: 'starts_with' });
}
if (operator === '_nstarts_with') {
return Joi.string().pattern(new RegExp(`^${escapeRegExp(value)}.*`), { name: 'starts_with', invert: true });
}
if (operator === '_ends_with') {
return Joi.string().pattern(new RegExp(`.*${escapeRegExp(value)}$`), { name: 'ends_with' });
}
if (operator === '_nends_with') {
return Joi.string().pattern(new RegExp(`.*${escapeRegExp(value)}$`), { name: 'ends_with', invert: true });
}
if (operator === '_in') {
return Joi.any().equal(...(value as (string | number)[]));
}

View File

@@ -12,20 +12,32 @@ import getDefaultValue from './get-default-value';
import getLocalType from './get-local-type';
import { mergePermissions } from './merge-permissions';
import getDatabase from '../database';
import { getCache } from '../cache';
import env from '../env';
import ms from 'ms';
export async function getSchema(options?: {
accountability?: Accountability;
database?: Knex;
}): Promise<SchemaOverview> {
// Allows for use in the CLI
const database = options?.database || getDatabase();
const schemaInspector = SchemaInspector(database);
const { schemaCache } = getCache();
const result: SchemaOverview = {
collections: {},
relations: [],
permissions: [],
};
let result: SchemaOverview;
if (env.CACHE_SCHEMA !== false && schemaCache) {
const cachedSchema = (await schemaCache.get('schema')) as SchemaOverview;
if (cachedSchema) {
result = cachedSchema;
} else {
result = await getDatabaseSchema(database, schemaInspector);
await schemaCache.set('schema', result, typeof env.CACHE_SCHEMA === 'string' ? ms(env.CACHE_SCHEMA) : undefined);
}
} else {
result = await getDatabaseSchema(database, schemaInspector);
}
let permissions: Permission[] = [];
@@ -65,6 +77,19 @@ export async function getSchema(options?: {
result.permissions = permissions;
return result;
}
async function getDatabaseSchema(
database: Knex,
schemaInspector: ReturnType<typeof SchemaInspector>
): Promise<SchemaOverview> {
const result: SchemaOverview = {
collections: {},
relations: [],
permissions: [],
};
const schemaOverview = await schemaInspector.overview();
const collections = [

View File

@@ -1,24 +0,0 @@
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
export default async function listFolders(location: string): Promise<string[]> {
const fullPath = path.resolve(location);
const files = await readdir(fullPath);
const directories: string[] = [];
for (const file of files) {
const filePath = path.join(fullPath, file);
const stats = await stat(filePath);
if (stats.isDirectory()) {
directories.push(file);
}
}
return directories;
}

View File

@@ -53,6 +53,10 @@ function validateFilter(filter: Query['filter']) {
case '_neq':
case '_contains':
case '_ncontains':
case '_starts_with':
case '_nstarts_with':
case '_ends_with':
case '_nends_with':
case '_gt':
case '_gte':
case '_lt':

View File

@@ -1,9 +1,9 @@
{
"name": "@directus/app",
"version": "9.0.0-rc.76",
"version": "9.0.0-rc.80",
"private": false,
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
"author": "Rijk van Zanten <rijk@rngr.org>",
"author": "Rijk van Zanten <rijkvanzanten@me.com>",
"main": "dist/index.html",
"files": [
"dist",
@@ -18,7 +18,7 @@
"access": "public"
},
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"copy-docs-images": "rimraf public/img/docs && copyfiles -u 3 \"../docs/assets/**/*\" \"public/img/docs\" --verbose",
@@ -28,8 +28,10 @@
},
"gitHead": "24621f3934dc77eb23441331040ed13c676ceffd",
"devDependencies": {
"@directus/docs": "9.0.0-rc.76",
"@directus/format-title": "9.0.0-rc.76",
"@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",
"@fullcalendar/core": "5.8.0",
"@fullcalendar/daygrid": "5.8.0",
"@fullcalendar/interaction": "5.8.0",
@@ -41,7 +43,7 @@
"@tinymce/tinymce-vue": "4.0.3",
"@types/base-64": "1.0.0",
"@types/bytes": "3.1.0",
"@types/codemirror": "5.60.0",
"@types/codemirror": "5.60.1",
"@types/color": "3.0.1",
"@types/diff": "5.0.0",
"@types/dompurify": "2.2.2",
@@ -60,7 +62,7 @@
"@vue/compiler-sfc": "3.1.1",
"axios": "0.21.1",
"base-64": "1.0.0",
"codemirror": "5.61.1",
"codemirror": "5.62.0",
"copyfiles": "2.4.1",
"cropperjs": "1.5.12",
"date-fns": "2.22.1",
@@ -69,7 +71,7 @@
"front-matter": "4.0.2",
"html-entities": "2.3.2",
"jsonlint-mod": "1.7.6",
"marked": "2.1.1",
"marked": "2.1.2",
"micromustache": "8.0.3",
"mime": "2.5.2",
"mitt": "2.1.0",
@@ -80,12 +82,12 @@
"qrcode": "1.4.4",
"rimraf": "3.0.2",
"sass": "1.35.1",
"tinymce": "5.8.1",
"tinymce": "5.8.2",
"typescript": "4.3.4",
"vite": "2.3.7",
"vite": "2.3.8",
"vue": "3.1.1",
"vue-i18n": "9.1.6",
"vue-router": "4.0.9",
"vue-router": "4.0.10",
"vuedraggable": "4.0.3"
}
}

View File

@@ -8,7 +8,7 @@ const api = axios.create({
baseURL: getRootPath(),
withCredentials: true,
headers: {
'Cache-Control': 'no-cache',
'Cache-Control': 'no-store',
},
});

View File

@@ -86,7 +86,7 @@ export default defineComponent({
},
to: {
type: [String, Object] as PropType<string | RouteLocation>,
default: null,
default: '',
},
href: {
type: String,
@@ -140,7 +140,7 @@ export default defineComponent({
const component = computed<'a' | 'router-link' | 'button'>(() => {
if (props.disabled) return 'button';
if (notEmpty(props.href)) return 'a';
if (notEmpty(props.to)) return 'router-link';
if (props.to) return 'router-link';
return 'button';
});

View File

@@ -6,6 +6,7 @@
:checked="groupCheckedStateOverride"
:label="text"
:value="value"
:disabled="disabled"
v-model="treeValue"
/>
</template>
@@ -23,12 +24,13 @@
:text="choice[itemText]"
:value="choice[itemValue]"
:children="choice[itemChildren]"
:disabled="disabled"
v-model="treeValue"
/>
</v-list-group>
<v-list-item v-else-if="!children && !hidden">
<v-checkbox :checked="checked" :label="text" :value="value" v-model="treeValue" />
<v-checkbox :disabled="disabled" :checked="checked" :label="text" :value="value" v-model="treeValue" />
</v-list-item>
</template>
@@ -88,13 +90,31 @@ export default defineComponent({
type: String,
default: 'children',
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const visibleChildrenValues = computed(() => {
if (!props.search) return props.children?.map((child) => child[props.itemValue]);
return props.children
?.filter((child) => child[props.itemText].toLowerCase().includes(props.search.toLowerCase()))
?.filter(
(child) =>
child[props.itemText].toLowerCase().includes(props.search.toLowerCase()) ||
childrenHaveMatch(child.children)
)
?.map((child) => child[props.itemValue]);
function childrenHaveMatch(children: Record<string, any>[] | undefined): boolean {
if (!children) return false;
return children.some(
(child) =>
child[props.itemText].toLowerCase().includes(props.search.toLowerCase()) ||
childrenHaveMatch(child[props.itemChildren])
);
}
});
const childrenValues = computed(() => props.children?.map((child) => child[props.itemValue]) || []);
@@ -294,7 +314,7 @@ export default defineComponent({
return emitValue(rawValue);
}
function emitLeaf(rawValue: (string | number)[], { added, removed }: Delta) {
function emitLeaf(rawValue: (string | number)[], { added }: Delta) {
const allChildrenRecursive = getRecursiveChildrenValues('all');
const leafChildrenRecursive = getRecursiveChildrenValues('leaf');
@@ -355,7 +375,7 @@ export default defineComponent({
return emitValue(rawValue);
}
function emitExclusive(rawValue: (string | number)[], { added, removed }: Delta) {
function emitExclusive(rawValue: (string | number)[], { added }: Delta) {
const childrenValuesRecursive = getRecursiveChildrenValues('all');
// When enabling the group level

View File

@@ -11,6 +11,7 @@
:text="choice[itemText]"
:value="choice[itemValue]"
:children="choice[itemChildren]"
:disabled="disabled"
v-model="value"
/>
</v-list>
@@ -52,6 +53,10 @@ export default defineComponent({
type: String,
default: 'children',
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const value = computed({

View File

@@ -3,7 +3,14 @@
<template #activator="{ toggle }">
<v-input :disabled="disabled">
<template #input>
<span ref="contentEl" class="content" contenteditable @keydown="onKeyDown" @input="onInput" @click="onClick">
<span
ref="contentEl"
class="content"
:contenteditable="!disabled"
@keydown="onKeyDown"
@input="onInput"
@click="onClick"
>
<span class="text" />
</span>
<span class="placeholder" v-if="placeholder && !modelValue">{{ placeholder }}</span>

View File

@@ -1,4 +1,5 @@
import { Field, FilterOperator } from '@/types';
import { Field } from '@/types';
import { FilterOperator } from '@directus/shared/types';
export type FormField = DeepPartial<Field> & {
field: string;

View File

@@ -214,7 +214,6 @@ body {
&.left {
margin-right: 8px;
margin-left: -4px;
&.small {
margin-right: 4px;
@@ -223,7 +222,6 @@ body {
}
&.right {
margin-right: -6px;
margin-left: 6px;
&.small {

View File

@@ -58,7 +58,7 @@ import slugify from '@sindresorhus/slugify';
import { omit } from 'lodash';
export default defineComponent({
emits: ['click', 'keydown', 'update:modelValue'],
emits: ['click', 'keydown', 'update:modelValue', 'focus'],
inheritAttrs: false,
props: {
autofocus: {
@@ -149,6 +149,7 @@ export default defineComponent({
trimIfEnabled();
attrs?.onBlur?.(e);
},
focus: (e: PointerEvent) => emit('focus', e),
}));
const attributes = computed(() => omit(attrs, ['class']));
const classes = computed(() => [

View File

@@ -36,7 +36,7 @@ export default defineComponent({
},
to: {
type: String,
default: null,
default: '',
},
active: {
type: Boolean,

View File

@@ -39,7 +39,7 @@ export default defineComponent({
},
to: {
type: [String, Object] as PropType<string | RouteLocation>,
default: null,
default: '',
},
href: {
type: String,

View File

@@ -1,6 +1,6 @@
import api from '@/api';
import useCollection from '@/composables/use-collection';
import { Filter, Item } from '@/types/';
import { Filter, Item } from '@directus/shared/types';
import filtersToQuery from '@/utils/filters-to-query';
import moveInArray from '@/utils/move-in-array';
import { isEqual, orderBy, throttle } from 'lodash';

View File

@@ -1,12 +1,7 @@
import { getLayouts } from '@/layouts';
import { LayoutProps } from '@/layouts/types';
import { computed, reactive, provide, inject, Ref, UnwrapRef } from 'vue';
type LayoutState<T, Options, Query> = {
props: LayoutProps<Options, Query>;
} & T;
const layoutSymbol = Symbol();
import { computed, reactive, provide, Ref, UnwrapRef } from 'vue';
import { LayoutProps, LayoutState } from '@directus/shared/types';
import { LAYOUT_SYMBOL } from '@directus/shared/constants';
export function useLayout<Options = any, Query = any>(
layoutName: Ref<string>,
@@ -25,17 +20,7 @@ export function useLayout<Options = any, Query = any>(
return reactive<LayoutState<Record<string, any>, Options, Query>>({ ...setupResult, props });
});
provide(layoutSymbol, layoutState);
return layoutState;
}
export function useLayoutState<T extends Record<string, any> = Record<string, any>, Options = any, Query = any>(): Ref<
UnwrapRef<LayoutState<Record<string, any>, Options, Query>>
> {
const layoutState = inject<Ref<UnwrapRef<LayoutState<T, Options, Query>>>>(layoutSymbol);
if (!layoutState) throw new Error('[useLayoutState]: This function has to be used inside a layout component.');
provide(LAYOUT_SYMBOL, layoutState);
return layoutState;
}

View File

@@ -1,6 +1,6 @@
import { useCollection } from '@/composables/use-collection';
import { usePresetsStore, useUserStore } from '@/stores';
import { Filter, Preset } from '@/types/';
import { Filter, Preset } from '@directus/shared/types';
import { debounce, isEqual } from 'lodash';
import { computed, ComputedRef, ref, Ref, watch } from 'vue';

View File

@@ -1,6 +1,4 @@
import api from '@/api';
import { getRootPath } from '@/utils/get-root-path';
import { asyncPool } from '@/utils/async-pool';
import { App } from 'vue';
import { getDisplays } from './index';
import { DisplayConfig } from './types';
@@ -12,18 +10,11 @@ export async function registerDisplays(app: App): Promise<void> {
const displays: DisplayConfig[] = Object.values(displayModules).map((module) => module.default);
try {
const customResponse = await api.get('/extensions/displays/');
const customDisplays: string[] = customResponse.data.data || [];
const customDisplays: { default: DisplayConfig[] } = import.meta.env.DEV
? await import('@directus-extensions-display')
: await import(/* @vite-ignore */ `${getRootPath()}extensions/displays/index.js`);
await asyncPool(5, customDisplays, async (displayName) => {
try {
const result = await import(/* @vite-ignore */ `${getRootPath()}extensions/displays/${displayName}/index.js`);
displays.push(result.default);
} catch (err) {
// eslint-disable-next-line no-console
console.warn(`Couldn't load custom displays "${displayName}":`, err);
}
});
displays.push(...customDisplays.default);
} catch {
// eslint-disable-next-line no-console
console.warn(`Couldn't load custom displays`);

View File

@@ -12,9 +12,8 @@
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { Field } from '@/types';
import { Field, Relation } from '@/types';
import { defineComponent, PropType, computed } from 'vue';
import { Relation } from '@/types/relations';
export default defineComponent({
emits: ['input'],

View File

@@ -1,17 +1,19 @@
<template>
<v-notice v-if="!collectionField" type="warning">
{{ t('interfaces.system-display-template.collection_field_not_setup') }}
</v-notice>
<v-notice v-else-if="collection === null" type="warning">
{{ t('interfaces.system-display-template.select_a_collection') }}
</v-notice>
<v-field-template
v-else
:collection="collection"
@update:model-value="$emit('input', $event)"
:model-value="value"
:disabled="disabled"
/>
<div class="system-display-template">
<v-notice v-if="!collectionField" type="warning">
{{ t('interfaces.system-display-template.collection_field_not_setup') }}
</v-notice>
<v-notice v-else-if="collection === null" type="warning">
{{ t('interfaces.system-display-template.select_a_collection') }}
</v-notice>
<v-field-template
v-else
:collection="collection"
:model-value="value"
:disabled="disabled"
@update:model-value="$emit('input', $event)"
/>
</div>
</template>
<script lang="ts">
@@ -45,6 +47,7 @@ export default defineComponent({
const collection = computed(() => {
if (!props.collectionField) return null;
const collectionName = values.value[props.collectionField];
const collectionExists = !!collectionsStore.collections.find(
(collection) => collection.collection === collectionName
);

View File

@@ -115,7 +115,11 @@ export default defineComponent({
if (!image.value) return null;
const { filesize, width, height, type } = image.value;
return `${n(width)}x${n(height)}${formatFilesize(filesize)}${type}`;
if (width && height) {
return `${n(width)}x${n(height)}${formatFilesize(filesize)}${type}`;
}
return `${formatFilesize(filesize)}${type}`;
});
watch(

View File

@@ -4,7 +4,7 @@
ref="editorElement"
:init="editorOptions"
:disabled="disabled"
model-events="change keydown blur focus paste ExecCommand"
model-events="change keydown blur focus paste ExecCommand SetContent"
v-model="internalValue"
@onFocusIn="setFocus(true)"
@onFocusOut="setFocus(false)"
@@ -265,7 +265,9 @@ export default defineComponent({
return props.value;
},
set(newValue: string) {
emit('input', newValue);
if (newValue !== props.value && (props.value === null && newValue === '') === false) {
emit('input', newValue);
}
},
});

View File

@@ -1,7 +1,7 @@
import { Position } from 'codemirror';
import { cloneDeep } from 'lodash';
type Alteration =
export type Alteration =
| 'bold'
| 'italic'
| 'strikethrough'
@@ -258,7 +258,7 @@ const alterations: AlterationFunctions = {
},
};
export function edit(codemirror: CodeMirror.Editor | null, type: Alteration, options?: Record<string, any>): void {
export function applyEdit(codemirror: CodeMirror.Editor | null, type: Alteration, options?: Record<string, any>): void {
if (codemirror) {
const cursor = codemirror.getCursor('head');
const cursorFrom = codemirror.getCursor('from');

View File

@@ -153,7 +153,7 @@ import CodeMirror from 'codemirror';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder.js';
import { edit, CustomSyntax } from './edits';
import { applyEdit, CustomSyntax, Alteration } from './edits';
import { getPublicURL } from '@/utils/get-root-path';
import { md } from '@/utils/md';
import { addTokenToURL } from '@/api';
@@ -208,7 +208,10 @@ export default defineComponent({
codemirror.on('change', (cm) => {
const content = cm.getValue();
emit('input', content);
if (content !== props.value && (props.value === null && content === '') === false) {
emit('input', content);
}
});
}
});
@@ -223,7 +226,7 @@ export default defineComponent({
if (existingValue !== newValue) {
codemirror.setValue('');
codemirror.clearHistory();
codemirror.setValue(newValue);
codemirror.setValue(newValue ?? '');
codemirror.refresh();
}
}
@@ -253,18 +256,18 @@ export default defineComponent({
columns: 4,
});
useShortcut('meta+b', () => edit(codemirror, 'bold'), markdownInterface);
useShortcut('meta+i', () => edit(codemirror, 'italic'), markdownInterface);
useShortcut('meta+k', () => edit(codemirror, 'link'), markdownInterface);
useShortcut('meta+alt+d', () => edit(codemirror, 'strikethrough'), markdownInterface);
useShortcut('meta+alt+q', () => edit(codemirror, 'blockquote'), markdownInterface);
useShortcut('meta+alt+c', () => edit(codemirror, 'code'), markdownInterface);
useShortcut('meta+alt+1', () => edit(codemirror, 'heading', { level: 1 }), markdownInterface);
useShortcut('meta+alt+2', () => edit(codemirror, 'heading', { level: 2 }), markdownInterface);
useShortcut('meta+alt+3', () => edit(codemirror, 'heading', { level: 3 }), markdownInterface);
useShortcut('meta+alt+4', () => edit(codemirror, 'heading', { level: 4 }), markdownInterface);
useShortcut('meta+alt+5', () => edit(codemirror, 'heading', { level: 5 }), markdownInterface);
useShortcut('meta+alt+6', () => edit(codemirror, 'heading', { level: 6 }), markdownInterface);
useShortcut('meta+b', () => edit('bold'), markdownInterface);
useShortcut('meta+i', () => edit('italic'), markdownInterface);
useShortcut('meta+k', () => edit('link'), markdownInterface);
useShortcut('meta+alt+d', () => edit('strikethrough'), markdownInterface);
useShortcut('meta+alt+q', () => edit('blockquote'), markdownInterface);
useShortcut('meta+alt+c', () => edit('code'), markdownInterface);
useShortcut('meta+alt+1', () => edit('heading', { level: 1 }), markdownInterface);
useShortcut('meta+alt+2', () => edit('heading', { level: 2 }), markdownInterface);
useShortcut('meta+alt+3', () => edit('heading', { level: 3 }), markdownInterface);
useShortcut('meta+alt+4', () => edit('heading', { level: 4 }), markdownInterface);
useShortcut('meta+alt+5', () => edit('heading', { level: 5 }), markdownInterface);
useShortcut('meta+alt+6', () => edit('heading', { level: 6 }), markdownInterface);
return {
t,
@@ -293,6 +296,12 @@ export default defineComponent({
imageDialogOpen.value = false;
}
function edit(type: Alteration, options?: Record<string, any>) {
if (codemirror) {
applyEdit(codemirror, type, options);
}
}
},
});
</script>

View File

@@ -30,9 +30,8 @@
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { Field } from '@/types';
import { Relation, Collection, Field } from '@/types';
import { defineComponent, PropType, computed } from 'vue';
import { Relation, Collection } from '@/types';
import { useCollectionsStore } from '@/stores';
export default defineComponent({
emits: ['input'],

View File

@@ -3,7 +3,7 @@ import { Header } from '@/components/v-table/types';
import { useFieldsStore } from '@/stores/';
import { Field } from '@/types';
import { addRelatedPrimaryKeyToFields } from '@/utils/add-related-primary-key-to-fields';
import { cloneDeep, get } from 'lodash';
import { cloneDeep, get, merge } from 'lodash';
import { Ref, ref, watch } from 'vue';
import { RelationInfo } from './use-relation';
@@ -83,7 +83,7 @@ export default function usePreview(
responseData = responseData
.map((item) => {
const updatedItem = updatedItems.find((updated) => updated[junctionPkField] === item[junctionPkField]);
if (updatedItem !== undefined) return updatedItem;
if (updatedItem !== undefined) return merge(item, updatedItem);
return item;
})
.concat(...newItems);

View File

@@ -1,4 +1,4 @@
import { Filter } from '@/types';
import { Filter } from '@directus/shared/types';
import { get } from 'lodash';
import { computed, ComputedRef, Ref, ref } from 'vue';
import { RelationInfo } from './use-relation';

View File

@@ -56,7 +56,7 @@ import api from '@/api';
import { getFieldsFromTemplate } from '@/utils/get-fields-from-template';
import hideDragImage from '@/utils/hide-drag-image';
import NestedDraggable from './nested-draggable.vue';
import { Filter } from '@/types';
import { Filter } from '@directus/shared/types';
import { Relation } from '@/types';
import DrawerCollection from '@/views/private/components/drawer-collection';
import DrawerItem from '@/views/private/components/drawer-item';

View File

@@ -22,9 +22,8 @@
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { Field } from '@/types';
import { Field, Relation } from '@/types';
import { defineComponent, PropType, computed } from 'vue';
import { Relation } from '@/types/relations';
export default defineComponent({
emits: ['input'],
props: {

View File

@@ -24,7 +24,7 @@ type Link = {
icon: string;
label: string;
type: string;
url: string;
url?: string;
};
export default defineComponent({
@@ -44,7 +44,7 @@ export default defineComponent({
const linksParsed = computed(() => {
return props.links.map((link) => ({
...link,
url: render(link.url, values.value),
url: render(link.url ?? '', values.value),
}));
});
@@ -59,13 +59,10 @@ export default defineComponent({
.presentation-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.action {
& + & {
margin-left: 8px;
}
&.info {
--v-button-icon-color: var(--white);
--v-button-background-color: var(--primary);

View File

@@ -1,7 +1,9 @@
<template>
<v-notice :icon="icon" :type="color">
<div v-html="md(text)" />
</v-notice>
<div class="presentation-notice">
<v-notice :icon="icon" :type="color">
<div v-html="md(text)" />
</v-notice>
</div>
</template>
<script lang="ts">

View File

@@ -1,6 +1,4 @@
import api from '@/api';
import { getRootPath } from '@/utils/get-root-path';
import { asyncPool } from '@/utils/async-pool';
import { App } from 'vue';
import { getInterfaces } from './index';
import { InterfaceConfig } from './types';
@@ -13,20 +11,11 @@ export async function registerInterfaces(app: App): Promise<void> {
const interfaces: InterfaceConfig[] = Object.values(interfaceModules).map((module) => module.default);
try {
const customResponse = await api.get('/extensions/interfaces/');
const customInterfaces: string[] = customResponse.data.data || [];
const customInterfaces: { default: InterfaceConfig[] } = import.meta.env.DEV
? await import('@directus-extensions-interface')
: await import(/* @vite-ignore */ `${getRootPath()}extensions/interfaces/index.js`);
await asyncPool(5, customInterfaces, async (interfaceName) => {
try {
const result = await import(
/* @vite-ignore */ `${getRootPath()}extensions/interfaces/${interfaceName}/index.js`
);
interfaces.push(result.default);
} catch (err) {
// eslint-disable-next-line no-console
console.warn(`Couldn't load custom interface "${interfaceName}":`, err);
}
});
interfaces.push(...customInterfaces.default);
} catch {
// eslint-disable-next-line no-console
console.warn(`Couldn't load custom interfaces`);

View File

@@ -12,9 +12,8 @@
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { Field } from '@/types';
import { Field, Relation } from '@/types';
import { defineComponent, PropType, computed } from 'vue';
import { Relation } from '@/types/relations';
export default defineComponent({
emits: ['input'],

View File

@@ -0,0 +1,124 @@
import { defineInterface } from '@/interfaces/define';
import { Field } from '@/types';
import InterfaceSelectMultipleCheckboxesTree from './select-multiple-checkbox-tree.vue';
const repeaterFields: DeepPartial<Field>[] = [
{
field: 'text',
type: 'string',
name: '$t:text',
meta: {
width: 'half',
interface: 'input',
options: {
placeholder: '$t:interfaces.select-dropdown.choices_name_placeholder',
},
},
},
{
field: 'value',
type: 'string',
name: '$t:value',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
placeholder: '$t:interfaces.select-dropdown.choices_name_placeholder',
},
},
},
];
const repeaterFieldsChildren: DeepPartial<Field> = {
field: 'children',
type: 'json',
name: '$t:children',
meta: {
width: 'full',
interface: 'list',
options: {
fields: repeaterFields,
},
},
};
function getNestedRepeaterFields(level = 0, maxLevel = 3): DeepPartial<Field>[] {
if (level < maxLevel) {
return [
...repeaterFields,
{
...repeaterFieldsChildren,
meta: {
...repeaterFieldsChildren.meta,
options: {
fields: getNestedRepeaterFields(level + 1),
},
},
},
];
}
return repeaterFields;
}
export default defineInterface({
id: 'select-multiple-checkbox-tree',
name: '$t:interfaces.select-multiple-checkbox-tree.name',
icon: 'account_tree',
component: InterfaceSelectMultipleCheckboxesTree,
description: '$t:interfaces.select-multiple-checkbox-tree.description',
types: ['json', 'csv'],
options: [
{
field: 'choices',
type: 'json',
name: '$t:choices',
meta: {
width: 'full',
interface: 'list',
options: {
template: '{{ text }}',
fields: getNestedRepeaterFields(),
},
},
},
{
field: 'valueCombining',
type: 'string',
name: '$t:interfaces.select-multiple-checkbox-tree.value_combining',
schema: {
default_value: 'all',
},
meta: {
note: '$t:interfaces.select-multiple-checkbox-tree.value_combining_note',
interface: 'select-dropdown',
options: {
choices: [
{
text: '$t:all',
value: 'all',
},
{
text: '$t:branch',
value: 'branch',
},
{
text: '$t:leaf',
value: 'leaf',
},
{
text: '$t:indeterminate',
value: 'indeterminate',
},
{
text: '$t:exclusive',
value: 'exclusive',
},
],
},
},
},
],
recommendedDisplays: ['labels'],
});

View File

@@ -0,0 +1,75 @@
<template>
<div class="select-multiple-checkbox-tree">
<div class="search">
<v-input class="input" v-model="search" type="text" :placeholder="t('search')">
<template #prepend>
<v-icon name="search" />
</template>
<template #append v-if="search">
<v-icon name="clear" clickable @click="search = ''" />
</template>
</v-input>
</div>
<v-checkbox-tree
@update:model-value="$emit('input', $event)"
:model-value="value"
:search="search"
:disabled="disabled"
:choices="choices"
:value-combining="valueCombining"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import { useI18n } from 'vue-i18n';
type Choice = {
text: string;
value: string | number;
children?: Choice[];
};
export default defineComponent({
emits: ['input'],
props: {
value: {
type: Array as PropType<string[]>,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
choices: {
type: Array as PropType<Choice[]>,
default: () => [],
},
valueCombining: {
type: String as PropType<'all' | 'branch' | 'leaf' | 'indeterminate' | 'exclusive'>,
default: 'all',
},
},
setup() {
const { t } = useI18n();
const search = ref('');
return { search, t };
},
});
</script>
<style scoped>
.select-multiple-checkbox-tree {
border: var(--border-width) solid var(--border-normal);
border-radius: var(--border-radius);
}
.search {
padding: 10px;
padding-bottom: 0;
}
</style>

View File

@@ -33,9 +33,8 @@
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { Field } from '@/types';
import { Field, Relation } from '@/types';
import { defineComponent, PropType, computed } from 'vue';
import { Relation } from '@/types/relations';
import { useCollectionsStore } from '@/stores/';
export default defineComponent({

View File

@@ -380,6 +380,8 @@ export default defineComponent({
async function fetchPreviews() {
if (!translationsRelation.value || !languagesRelation.value || !languages.value) return;
if (props.primaryKey === '+') return;
loading.value = true;
try {

View File

@@ -30,7 +30,8 @@ pt-BR: Portuguese (Brazil)
pt-PT: Portuguese (Portugal)
ro-RO: Romanian (Romania)
ru-RU: Russian (Russian Federation)
sr-SP: Serbian (Serbia and Montenegro)
sr-SP: Serbian (Cyrillic) (Serbia and Montenegro)
sr-CS: Serbian (Latin) (Serbia and Montenegro)
si-LK: Sinhala (Sri Lanka)
sk-SK: Slovak (Slovakia)
es-ES: Spanish (Spain)

View File

@@ -110,7 +110,7 @@ revision_post_update: إليك ما بدا عليه هذا العنصر بعد
changes_made: هذه هي التغييرات المحددة التي تم إجراؤها...
no_relational_data: ولا يغيب عن البال أن هذا لا يشمل بيانات علاقية.
hide_field_on_detail: مخفي في التفاصيل
show_field_on_detail: مخفي في التفاصيل
show_field_on_detail: أظهر الحقل في التفاصيل
delete_field: حذف الحقل
fields_and_layout: الحقول٪s ق وتخطيط
field_create_success: 'حقل تم إنشاؤه: "{field}"'
@@ -206,7 +206,7 @@ clear_value: إزالة القيمة
reset_to_default: إعادة تعيين إلى الافتراضي
undo_changes: التراجع عن التغييرات
notifications: إشعارات
show_all_activity: كل النشاطات
show_all_activity: أظهر كل النشاط
page_not_found: الصفحة غير موجودة
page_not_found_body: الصفحة التي تبحث عنها غير موجودة.
confirm_revert: تأكيد الرجوع
@@ -220,23 +220,34 @@ revision_delta_updated: 'تم تحديث حقل واحد | تحديث حقول {
revision_delta_deleted: حذف
revision_delta_reverted: تمت الإستعادة
revision_delta_other: تنقيح
revision_delta_by: '{user} {date}'
revision_delta_by: '{date} بواسطة {user}'
private_user: خصخصة المستخدم
revision_preview: معاينة التعديلات
updates_made: ترقيات مصنوعة
leave_comment: اترك تعليقا...
post_comment_success: تم ارسال التعليق
item_create_success: عنصر تم إنشاؤه | عناصر تم إنشاؤها
item_update_success: عنصر تم تحديثه | عناصر تم تحديثها
item_delete_success: عنصر تم حذفه | عناصر تم حذفها
this_collection: هذه المجموعة
related_collection: مجموعة ذات صلة
related_collections: مجموعات ذات صلة
translations_collection: مجموعة الترجمة
languages_collection: مجموعة اللغات
export_data: تصدير البيانات
format: التنسيق
use_current_filters_settings: استخدام التصفية و الإعدادات الحالية
export_collection: 'تصدير {collection}'
submit: إرسال
move_to_folder: أنقل إلى مجلد
move: نقل
system: نظام
add_field_related: إضافة حقل إلى مجموعة ذات صلة
today: اليوم
yesterday: أمس
delete_comment: احذف التعليق
month: شهر
year: سنة
select_all: تحديد الكل
months:
january: يناير
@@ -251,14 +262,19 @@ months:
october: أكتوير
november: نوفمبر
december: ديسمبر
drag_mode: وضع السحب
url: الرابط
import: إستيراد
file_details: تفاصيل الملف
dimensions: الأبعاد
size: حجم
created: تم الإنشاء
modified: معدّل
checksum: بصمة الملف
owner: المالك
edited_by: تم التحرير بواسطة
folder: مجلد
zoom: تكبير/تصغير
download: تنزيل
open: فتح
open_in_new_window: فتح في نافذة جديدة
@@ -308,7 +324,7 @@ list-m2a: الباني (M2A)
item_count: 'لا توجد عناصر | عنصر واحد | {count} العناصر'
no_items_copy: لا توجد عناصر في هذه المجموعة بعد.
file_count: 'لايوجد دور أو صلاحية | دور أو صلاحية وحيدة | {count} أدوار أو صلاحيات'
no_files_copy: لا توجد مسلسلات هنا.
no_files_copy: لا توجد ملفات هنا.
user_count: 'لا توجد عناصر | عنصر واحد | {count} العناصر'
no_users_copy: لا يوجد مستخدمون في هذه التجزئة.
webhooks_count: 'لاتوجد روابط ويب | رابط ويب | {count} روابط ويب'
@@ -342,9 +358,10 @@ collection_created: تم تحديث المجموعة
modified_on: التعديل في
card_size: حجم البطاقة
sort_field: فرز الحقل
add_sort_field: فرز الحقل
add_sort_field: إضافة حقل الترتيب
sort: رتب
status: حالة
remove: احذف
toggle_manual_sorting: تفعيل الترتيب اليدوي
bookmark_doesnt_exist: الإشارة المرجعية غير موجودة
bookmark_doesnt_exist_copy: لم يتم العثور على العلامة المرجعية التي تحاول فتحها.
@@ -366,22 +383,48 @@ errors:
INVALID_CREDENTIALS: اسم المستخدم و / أو كلمة المرور خاطئة
INVALID_OTP: كلمة مرور لمرة واحدة خاطئة
INVALID_PAYLOAD: Invalid payload
ITEM_NOT_FOUND: لم يتم العثور على العنصر
ROUTE_NOT_FOUND: غير موجود
USER_SUSPENDED: المستخدم موقوف
UNKNOWN: خطأ غير متوقع
INTERNAL_SERVER_ERROR: خطأ غير متوقع
bookmark_name: اسم الإشارة المرجعية...
create_bookmark: إنشاء إشارة مرجعية
edit_bookmark: تحرير الإشارة المرجعية
bookmarks: محفوظات
presets: الإعدادات المسبقة
unexpected_error: خطأ غير متوقع
unexpected_error_copy: حدث خطأ غير متوقع. الرجاء المحاولة مرة أخرى لاحقاً.
copy_details: نسخ التفاصيل
password_reset_sent: لقد أرسلنا لك رابط آمن لإعادة تعيين كلمة المرور الخاصة بك
password_reset_successful: تم إعادة تعيين كلمة المرور بنجاح
back: رجوع
editing_image: تحرير صورة
square: مربع
free: حر
aspect_ratio: نسبة العرض إلى الارتفاع
rotate: تدوير
all_users: كل المستخدمين
delete_collection: احذف المجموعة
update_collection_success: تم تحديث المجموعة
delete_collection_success: تم حذف المجموعة
start_end_of_count_items: '{start}-{end} من {count} عناصر'
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: مجموعات مخفية
show_hidden_collections: أظهر المجموعات المخفية
hide_hidden_collections: اخفي المجموعات المخفية
system_collections: مجموعات النظام
placeholder: مكان محجوز
icon_left: أيقونة اليسار
icon_right: أيقونة اليمين
count_other_revisions: '{count} مراجعات أخرى'
font: الخط
serif: خط سيريف Serif
monospace: Monospace
divider: الفاصل
@@ -392,10 +435,14 @@ log_in_with: 'تسجيل الدخول باستخدام {provider}'
advanced_filter: تصفية متقدمة
delete_advanced_filter: حذف التصفية
operators:
eq: يساوي
neq: لا يساوي
lt: أقل من
gt: أكبر من
lte: اقل او يساوي
gte: أكبر من أو يساوي
in: واحد من
nin: ليس من
nnull: باطل
contains: يحتوي على
ncontains: لا يحتوي
@@ -421,39 +468,68 @@ layout_options: خيارات التصميم
rows: الصفوف
columns: أعمدة
collection_setup: لا توجد مجموعات الإعداد
optional_system_fields: حقول النظام الاختيارية
value_unique: القيمة يجب أن تكون
all_activity: كل النشاط
create_item: أنشئ عنصر
display_template: عرض القالب
language_display_template: قالب عرض اللغة
translations_display_template: قالب عرض الترجمات
n_items_selected: 'لم يتم تحديد عناصر | 1 عنصر محدد | {n} عناصر محددة'
per_page: لكل صفحة
all_files: '‮كل الملفات'
my_files: ملفاتي
recent_files: احدث الملفات
create_folder: أنشئ مجلد
folder_name: اسم المجلد...
add_file: أضف ملف
replace_file: استبدال ملف
no_results: لا توجد نتائج
no_results_copy: ضبط أو مسح تصفية البحث لرؤية النتائج.
clear_filters: مسح التصفية
saves_automatically: يحفظ تلقائياً
role: الدور
user: مستخدم
no_presets: لا إعدادات مسبقة
no_presets_copy: لم يتم حفظ أي إعدادات مسبقة أو إشارات مرجعية حتى الآن.
no_presets_cta: إضافة إعداد مسبق
create: انشاء
on_create: عند الإنشاء
on_update: عند التحديث
read: قراءة
update: تحديث
select_fields: حدد حقول
format_text: تنسيق النص
width: العرض
height: الارتفاع
source: مصدر
unlimited: غير محدود
open_link_in: افتح الرابط في
wysiwyg_options:
codeblock: نص برمجي
link: إضافة/تحرير الرابط
unlink: إزالة الرابط
image: إضافة/تحرير الصورة
copy: نسخ
cut: قص
paste: لصق
fontselect: تحديد الخط
fontsizeselect: حدد حجم الخط
remove: احذف
removeformat: إزالة التنسيق
selectall: تحديد الكل
table: الجدول
visualaid: عرض العناصر غير المرئية
directionality: الاتجاهية
dropdown: قائمة منسدلة
choices: الخيارات
choices_option_configured_incorrectly: تم تكوين الخيارات بشكل غير صحيح
deselect: إلغاء تحديد
deselect_all: إلغاء تحديد الكل
other: أخرى...
adding_user: إضافة مستخدم
unknown_user: مستخدم غير معروف
creating_in: 'إنشاء عنصر في {collection}'
editing_in: 'تعديل عنصر في {collection}'
creating_unit: 'إنشاء {unit}'
editing_unit: 'تعديل {unit}'
@@ -464,21 +540,59 @@ settings_permissions: الأدوار والأذونات
settings_project: إعدادات المشروع
settings_webhooks: روابط الويب (Webhooks)
settings_presets: الإعدادات المسبقة والعلامات
one_or_more_options_are_missing: واحد أو أكثر من الخيارات مفقودة
scope: مجال
select: حدد...
layout: نموذج العرض
changes_are_permanent: التغييرات دائمة
preset_name_placeholder: يعمل كإعداد افتراضي عندما يكون فارغًا...
editing_preset: تحرير الإعداد المسبق
layout_preview: معاينة نموذج العرض
layout_setup: إعداد نموذج العرض
unsaved_changes: لم يتم حفظ التعديلات
unsaved_changes_copy: هل ترغب حقّا بمغادرة هذه الصفحة؟
discard_changes: تجاهل التغييرات
keep_editing: استمر في التحرير
page_help_collections_overview: '**نظرة عامة للمجموعات** - قوائم جميع المجموعات التي لديك حق الوصول إليها.'
page_help_collections_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_item: "**تفاصيل الدور** - إدارة أذونات الدور والإعدادات الأخرى."
page_help_settings_presets_collection: >-
**استعراض الإعدادات المسبقة** - عرض جميع الإعدادات المسبقة في المشروع، بما في ذلك: المستخدم، الدور، الإشارات المرجعية العالمية، فضلا عن نماذج عروض الافتراضية.
page_help_settings_presets_item: >-
**تفاصيل الإعداد المسبق** - نموذج لإدارة الإشارات المرجعية والضبط المسبق الافتراضي للمجموعات.
page_help_settings_webhooks_collection: '**تصفح Webhooks** - عرض جميع webhooks داخل المشروع.'
page_help_settings_webhooks_item: '**تفاصيل Webhook ** - نموذج لإنشاء وإدارة webhook للمشروع.'
page_help_users_collection: '**دليل المستخدم** - يعرض جميع مستخدمي النظام داخل هذا المشروع.'
page_help_users_item: >-
**تفاصيل المستخدم** - إدارة معلومات حسابك، أو عرض تفاصيل المستخدمين الآخرين.
add_new: إضافة جديدة
create_new: أنشئ جديد
all: الجميع
no_layout_collection_selected_yet: لم يتم تحديد نموذج عرض/مجموعة بعد
batch_delete_confirm: >-
لم يتم اختيار عناصر | هل أنت متأكد أنك تريد حذف هذا البند؟ لا يمكن التراجع عن هذا الإجراء. | هل تريد بالتأكيد حذف هذه {count} عنصر ؟ لا يمكن التراجع عن هذا الإجراء.
cancel: إلغاء
collection: المجموعة
collections: مجموعات
singleton_label: معاملته ككائن واحد
system_fields_locked: حقول النظام مقفلة ولا يمكن تعديلها
fields:
directus_activity:
item: المفتاح الأساسي للعنصر
@@ -496,15 +610,23 @@ fields:
note: ملحوظة
display_template: عرض القالب
hidden: مخفي
translations: ترجمات تسمية المجموعات
archive_value: أرشفة القيمة
unarchive_value: إلغاء أرشفة القيمة
sort_field: فرز الحقل
accountability: تتبع النشاط والتنقيح
directus_files:
$thumbnail: صورة مصغرة
title: العنوان
description: التفاصيل
tags: الوسوم
location: الموقع
storage: تخزين
filename_disk: اسم الملف (القرص)
filename_download: اسم الملف (تحميل)
metadata: البيانات التعريفية
filesize: حجم الملف
modified_by: تم تعديله بواسطة
modified_by: تم التعديل بواسطة
modified_on: التعديل في
created_on: تم الإنشاء بتاريخ
created_by: تم الإنشاء بواسطة
@@ -537,6 +659,7 @@ fields:
project_color: لون المشروع
project_logo: شعار المشروع
directus_fields:
collection: اسم المجموعة
icon: أيقونة المجموعة
note: ملحوظة
hidden: مخفي
@@ -546,13 +669,26 @@ fields:
name: إسم الدور
icon: أيقونة الدور
description: التفاصيل
users: المستخدمون في الدور
field_options:
directus_collections:
track_activity_revisions: تتبع النشاط والتنقيحات
only_track_activity: تتبع النشاط فقط
do_not_track_anything: عدم تتبع أي شيء
no_fields_in_collection: 'لا توجد حقول في "{collection}" حتى الآن'
do_nothing: لا تفعل شيئا
save_current_user_role: احفظ دور المستخدم الحالي
save_current_datetime: أحفظ التاريخ/الوقت الحالي
comment: تعليق
referential_action_field_label_m2o: عند حذف {collection}...
referential_action_field_label_o2m: عند إلغاء تحديد {collection}...
referential_action_no_action: منع الحذف
referential_action_cascade: حذف العنصر {collection} (تسلسلي)
referential_action_set_default: تعيين {field} إلى قيمته الافتراضية
choose_action: اختر الإجراء
continue: واصل
editing_role: 'دور {role}'
creating_webhook: إنشاء Webhook
default: الإفتراضي
delete: حذف
delete_are_you_sure: >-
@@ -571,6 +707,8 @@ forgot_password: نسيت كلمة المرور
hidden: مخفي
icon: أيقونة
info: معلومات
warning: تحذير
danger: خطر
junction_collection: مجموعة تلاقي
latency: وقت الإستجابة
login: تسجيل الدخول
@@ -603,92 +741,192 @@ template: قالب
translation: الترجمة
value: قيمة
view_project: عرض المشروع
report_error: الإبلاغ عن خطأ
interfaces:
presentation-links:
links: روابط
primary: الأساسي
link: روابط
button: أزرار
error: لا يمكن تنفيذ الإجراء
select-multiple-checkbox:
checkboxes: مربعات الاختيار
description: اختر بين خيارات متعددة عبر مربعات الاختيار
show_more: 'أظهر {count} أكثر'
items_shown: العناصر المعروضة
input-code:
code: نص برمجي
description: كتابة أو مشاركة الكود البرمجي
line_number: رقم السطر
placeholder: أدخل الكود هنا...
system-collection:
collection: المجموعة
system-collections:
collections: مجموعات
select-color:
color: لون
description: أدخل أو حدد قيمة اللون
placeholder: اختر لون...
preset_colors: ألوان محددة مسبقًا
preset_colors_add_label: إضافة لون جديد...
name_placeholder: أدخل اسم اللون...
datetime:
datetime: التاريخ والوقت
description: أدخل التواريخ والأوقات
set_to_now: اضبط على الآن
use_24: استخدم تنسيق 24 ساعة
system-display-template:
display-template: عرض القالب
description: خلط النص الثابت وقيم الحقل الديناميكي
collection_field: حقل المجموعة
collection_field_not_setup: خيار حقل المجموعة خاطئ التكوين
select_a_collection: حدد مجموعة
presentation-divider:
divider: الفاصل
title_placeholder: أدخل عنوانًا...
inline_title_label: أظهر العنوان داخل السطر
margin_top: الهامش العلوي
margin_top_label: زيادة الهامش العلوي
select-dropdown:
description: حدد قيمة من القائمة المنسدلة
choices_placeholder: أضف خيار جديد
allow_other_label: السماح بقيم أخرى
allow_none_label: لا يسمح بتحديد
choices_name_placeholder: أدخل اسم...
choices_value_placeholder: أدخل قيمة...
select-multiple-dropdown:
select-multiple-dropdown: منسدلة (متعدد)
description: حدد قيم متعددة من القائمة المنسدلة
file:
file: ملف
description: حدد أو ارفع ملف
files:
files: الملفات
description: حدد أو ارفع ملفات متعددة
input-hash:
hash: الهاش
masked: مقنع
masked_label: إخفاء القيم الحقيقية
select-icon:
icon: أيقونة
description: حدد أيقونة من القائمة المنسدلة
search_for_icon: البحث عن أيقونة...
file-image:
image: صورة
description: حدد أو ارفع صورة
select-dropdown-m2o:
description: حدد عنصر واحد ذو صلة
display_template: عرض القالب
input-rich-text-md:
description: أدخل و معاينة markdown
list-o2m:
description: حدد عدة عناصر ذات صلة
select-radio:
description: حدد اختيار واحد من خيارات متعددة
list:
edit_fields: تعديل الحقول
field_name_placeholder: أدخل اسم الحقل...
slider:
slider: شريط
always_show_value: إظهار القيمة دائمًا
tags:
tags: الوسوم
description: حدد أو أضف وسوم
auto_formatter: استخدام التنسيق التلقائي للعنوان
alphabetize: رتب أبجديا
alphabetize_label: فرض الترتيب الأبجدي
add_tags: إضافة وسوم...
input:
description: أدخل قيمة يدوياً
mask: مقنع
mask_label: إخفاء القيمة الحقيقية
minimum_value: أدنى قيمة
maximum_value: أقصى قيمة
slug_label: اجعل رابط القيمة المدخلة آمن
input-multiline:
description: أدخل النص المتعدد الأسطر
boolean:
description: التبديل بين تشغيل وإيقاف التشغيل
label_default: تمكين
translations:
display_template: عرض القالب
no_collection: لا توجد مجموعة
user:
user: مستخدم
description: حدد مستخدم directus موجود
select_mode: تحديد الوضع
modes:
auto: تلقائي
dropdown: قائمة منسدلة
input-rich-text-html:
description: محرر نص يكتب محتوى HTML
toolbar: شريط الأدوات
custom_formats: تنسيقات مخصصة
options_override: تجاوز الخيارات
input-autocomplete-api:
rate: معدل
displays:
boolean:
boolean: نعم/لا
description: عرض حالة التشغيل وإيقاف التشغيل
color_on: تشغيل اللون
color_off: إيقاف اللون
collection:
collection: المجموعة
description: عرض مجموعة
icon_label: أظهر أيقونة المجموعة
color:
color: لون
description: عرض نقطة ملونة
default_color: اللون الافتراضي
datetime:
datetime: التاريخ والوقت
description: عرض القيم المتصلة بالوقت
format: التنسيق
long: طويل
short: قصير
relative: نسبي
relative_label: 'أظهر الوقت النسبي، مثال: قبل 5 دقائق'
file:
file: ملف
description: اعرض الملفات
filesize:
filesize: حجم الملف
description: عرض حجم الملف
formatted-value:
formatted-value: قيمة منسقة
description: عرض نسخة منسقة من النص
format_title: تنسيق العنوان
icon:
icon: أيقونة
description: عرض أيقونة
filled: مملوء
image:
image: صورة
description: عرض معاينة صغيرة للصورة
circle: دائرة
circle_label: عرض كدائرة
labels:
show_as_dot: أظهر كنقطة
choices_value_placeholder: أدخل قيمة...
choices_text_placeholder: أدخل نص...
mime-type:
description: أظهر MIME-Type الملف
extension_only_label: أظهر ملحق الملف فقط
rating:
rating: التقييم
simple: بسيط
simple_label: أظهر النجوم بصيغة بسيطة
related-values:
related-values: القيم ذات الصلة
description: اعرض القيم ذات الصلة
user:
user: مستخدم
description: عرض مستخدم directus
avatar: الصورةالرمزية
name: الاسم
both: كل منهما
circle_label: أظهر المستخدم في دائرة
layouts:
cards:
cards: البطاقات
@@ -702,3 +940,7 @@ layouts:
comfortable: مريحة
compact: مضغوط
cozy: مريح
calendar:
calendar: تقويم
start_date_field: حقل تاريخ البداية
end_date_field: حقل تاريخ الإنتهاء

View File

@@ -20,6 +20,7 @@ create_role: Създаване на роля
create_user: Създаване на потребител
create_webhook: Създаване на уеб-кука
invite_users: Покана към потребители
email_examples: "admin{'@'}example.com, potrebitel{'@'}organizacia.com..."
invite: Покана
email_already_invited: Вече е изпратена покана в пощата на "{email}"
emails: E-пощи
@@ -85,6 +86,9 @@ validationError:
all_access: Пълен достъп
no_access: Без достъп
use_custom: Персонализиран
nullable: Може да е null
allow_null_value: Може да е NULL
enter_value_to_replace_nulls: Въвеждане на нова стойност, която да замени NULL в това поле.
field_standard: Стандартно
field_presentation: Презентационни и псевдоними
field_file: Единствен файл
@@ -334,6 +338,7 @@ interface_not_found: 'Интерфейсът "{interface}" не е намере
reset_interface: Нулиране на интерфейс
display_not_found: 'Изгледът "{display}" не е намерен.'
reset_display: Нулиране на изглед
list-m2a: Строител (M2A)
item_count: 'Няма записи | Един запис | {count} записа'
no_items_copy: Все още няма записи в тази колекция.
file_count: 'Няма файлове | Един файл | {count} файла'
@@ -374,6 +379,7 @@ sort_field: Поле за сортиране
add_sort_field: Добавяне на поле за сортиране
sort: Сортиране
status: Статус
remove: Премахване
toggle_manual_sorting: Ръчна подредба
bookmark_doesnt_exist: Отметката не съществива
bookmark_doesnt_exist_copy: Отметката която се опитвате да отворите не може да бъде намерена.
@@ -401,6 +407,7 @@ errors:
ROUTE_NOT_FOUND: Не е намерен
RECORD_NOT_UNIQUE: Вече съществува такава стойност
USER_SUSPENDED: Потребителя е деактивиран
CONTAINS_NULL_VALUES: Полето съдържа null стойност
UNKNOWN: Неочаквана грешка
INTERNAL_SERVER_ERROR: Неочаквана грешка
value_hashed: Хеширана стойност
@@ -497,6 +504,8 @@ value_unique: Изисква се уникална стойност
all_activity: Цялата активност
create_item: Създаване на запис
display_template: Шаблон при показване
language_display_template: Шаблон при показване на езиците
translations_display_template: Шаблон при показване на преводите
n_items_selected: 'Не са избрани записи | 1 избран запис | {n} избрани записа'
per_page: На страница
all_files: Всички файлове
@@ -778,9 +787,15 @@ save_current_datetime: Запазване на текущия дата/час
block: Блок
inline: Вложен
comment: Кометнар
relational_triggers: Релационни действия
referential_action_field_label_m2o: При изтриване на {collection}...
referential_action_field_label_o2m: При деселектиране на {collection}...
referential_action_no_action: Избягване на изтривания
referential_action_cascade: Каскадно изтриване на {collection}
referential_action_set_null: Зануляване на {field}
referential_action_set_default: Задаване на {field} към стойността по подразбиране
choose_action: Изберете действие
continue: Продължение
continue_as: >-
<b>{name}</b> в момента е оторизиран. Ако разпознавате този профил, изберете бутона за продължение.
editing_role: 'Роля - {role}'
creating_webhook: Създаване на уеб-кука
default: По подразбиране
@@ -851,11 +866,16 @@ interfaces:
button: Бутони
error: Действието не може да бъде изпълнено
select-multiple-checkbox:
checkboxes: Отметки
description: Избор измежду множество опции чрез отметки
allow_other: Други
show_more: 'Показване на още {count}'
items_shown: Показани записи
input-code:
code: Код
description: Писане или споделяне на кодови откъси
line_number: Номериране на редове
placeholder: Въвеждане на кода...
system-collection:
collection: Колекция
description: Избор от съществуващи колекции
@@ -866,6 +886,11 @@ interfaces:
include_system_collections: Включване на системните колекции
select-color:
color: Цвят
description: Въвеждане или избор на стойност за цвят
placeholder: Избор на цвят...
preset_colors: Предварително зададени цветове
preset_colors_add_label: Добавяне на нов цвят...
name_placeholder: Въвеждане името на цвета...
datetime:
datetime: Дата и час
description: Въвеждане на дати и часове
@@ -874,16 +899,30 @@ interfaces:
use_24: Използване на 24 часов формат
system-display-template:
display-template: Шаблон при показване
description: Комбиниране на статичен текст и динамични стойности от полета
collection_field: Поле на колекцията
collection_field_not_setup: Опциите на полето от колекцията са неправилно конфигурирани
select_a_collection: Избор на колекция
presentation-divider:
divider: Разделител
description: Именуване и разделяне на полетата по секции
title_placeholder: Въвеждане на заглавие...
inline_title: Заглавие в линия
inline_title_label: Показване на заглавията в линия
margin_top: Горно отстояние
margin_top_label: Увеличено горно отстояние
select-dropdown:
description: Избор на стойност от падащо меню
choices_placeholder: Добавяне на избор
allow_other: Други
allow_other_label: Позволяване на други стойности
allow_none: Позволяване на нищо
allow_none_label: Текст за "Позволяване на нищо"
choices_name_placeholder: Въвеждане на име...
choices_value_placeholder: Въвеждане на стойност...
select-multiple-dropdown:
select-multiple-dropdown: Падащо меню (множествен избор)
description: Избор на множество стойности от падащо меню
file:
file: Файл
description: Избор или качване на файл
@@ -892,24 +931,56 @@ interfaces:
description: Избор или качване на множество файлове
input-hash:
hash: Хеш
description: Въвеждане на стойност за хеширане
masked: Маскиране
masked_label: Скриване на истинските стойности
select-icon:
icon: Икона
description: Избор на икона от падащото меню
search_for_icon: Търсене на икона...
file-image:
image: Изображение
description: Избор или качване на изображение
system-interface:
interface: Интерфейс
description: Избор на съществуващ интерфейс
placeholder: Избор на интерфейс...
system-interface-options:
interface-options: Опции за интерфейса
description: Диалог за избор на опции за интерфейс
list-m2m:
many-to-many: Много към много
description: Избор на множество свързващи записи
select-dropdown-m2o:
many-to-one: Много към един
description: Избор на един свързан запис
display_template: Шаблон при показване
input-rich-text-md:
markdown: Markdown
description: Въвеждане и преглед за markdown
customSyntax: Персонализирани блокове
customSyntax_label: Добавяне на персонализиран синтаксис
customSyntax_add: Добавяне на персонален синтаксис
box: Блоков / Вложен
imageToken: Токън за изображенията
imageToken_label: Какъв (статичен) токън да се добави, към адресите на изображенията
presentation-notice:
notice: Бележка
description: Показване на кратка бележка
text: Въвеждане на бележката...
list-o2m:
one-to-many: Един към много
description: Избор на множество свързани записи
no_collection: Колекцията не може да бъде намерена
select-radio:
radio-buttons: Радио бутони
description: Един или няколко избора
list:
repeater: Повторение
description: Създаване на множество записи със същата структура
edit_fields: Редактиране на полета
add_label: 'Текст за "Създаване"'
field_name_placeholder: Въвеждане име на полето...
field_note_placeholder: Въвеждане бележка към полето...
slider:
slider: Плъзгач
@@ -931,21 +1002,32 @@ interfaces:
add_tags: Добавяне на етикет...
input:
input: Въвеждане
description: Ръчно въвеждане на стойност
trim: Почистване
trim_label: Почистване на краищата от интервали
mask: Маскиране
mask_label: Скриване на истинската стойност
clear: Изчистване на стойност
clear_label: Запазване като празен стринг
minimum_value: Минимална стойност
maximum_value: Максимална стойност
step_interval: Интервал на стъпка
slug: Превръщане в Slug
slug_label: Превръщане на стойностите безопасни за URL
input-multiline:
textarea: Поле за текст
description: Въвеждане на чист текст на няколко реда
boolean:
toggle: Превключване
description: Превключване между вкл. и изкл.
label_placeholder: Въвеждане на етикет...
label_default: Включване
translations:
display_template: Шаблон при показване
no_collection: Няма колекция
list-o2m-tree-view:
description: Дървовиден изглед за вложени рекурсивни записи от тип - един-към-много
recursive_only: Дървовидния интерфейс, работи само за рекурсивни релации.
user:
user: Потребител
description: Избор на съществуващ потребител
@@ -953,11 +1035,16 @@ interfaces:
modes:
auto: Автоматичен
dropdown: Падащо меню
modal: Модал
modal: Диалог
input-rich-text-html:
wysiwyg: WYSIWYG
description: Редактор на текст с допълнения в HTML формат
toolbar: Лента с инструменти
custom_formats: Персонализирани формати
options_override: Наслагване на опции
input-autocomplete-api:
input-autocomplete-api: Поле с автоматично дописване (API)
description: Автоматично дописване на стойности от външно API.
results_path: Път до резултатите
value_path: Път до стойността
trigger: Задействане

View File

@@ -168,6 +168,7 @@ documentation: Dokumentace
duration: Trvání
modified_on: Upraveno
status: Stav
remove: Odebrat
bookmarks: Záložky
back: Zpět
all_users: Všichni uživatelé

View File

@@ -197,6 +197,7 @@ user_directory: Bruger Mappe
duration: Varighed
modified_on: Ændringsdato
status: Status
remove: Fjern
bookmarks: Foretrukne
back: Tilbage
placeholder: Placeholder

View File

@@ -379,6 +379,7 @@ sort_field: Sortierfeld
add_sort_field: Sortierfeld hinzufügen
sort: Sortieren
status: Status
remove: Löschen
toggle_manual_sorting: Manuelle Sortierung umschalten
bookmark_doesnt_exist: Lesezeichen existiert nicht
bookmark_doesnt_exist_copy: Das zu öffnende Lesezeichen konnte nicht gefunden werden.
@@ -795,8 +796,6 @@ referential_action_set_null: '{field} auf Null setzen'
referential_action_set_default: '{field} au Standardwert setzen'
choose_action: Aktion auswählen
continue: Weiter
continue_as: >-
<b>{name}</b> ist bereits authentifiziert. Wenn Sie dieses Konto kennen, drücken Sie bitte auf Fortfahren.
editing_role: '{role} Rolle'
creating_webhook: Webhook erstellen
default: Standard
@@ -855,7 +854,6 @@ template: Vorlage
translation: Übersetzung
value: Wert
view_project: Projekt anzeigen
weeks: { }
report_error: Fehler melden
interfaces:
presentation-links:

View File

@@ -107,6 +107,7 @@ activity: Δραστηριότητα
field_width: Πλάτος πεδίου
duration: Διάρκεια
status: Κατάσταση
remove: Διαγραφή
bookmarks: Σελιδοδείκτες
back: Πίσω
operators:

View File

@@ -8,6 +8,11 @@ field_name_translations: Field Name Translations
enter_password_to_enable_tfa: Enter your password to enable Two-Factor Authentication
add_field: Add Field
role_name: Role Name
branch: Branch
leaf: Leaf
indeterminate: Indeterminate
exclusive: Exclusive
children: Children
db_only_click_to_configure: 'Database Only: Click to Configure '
show_archived_items: Show Archived Items
edited: Value Edited
@@ -383,6 +388,7 @@ sort_field: Sort Field
add_sort_field: Add Sort Field
sort: Sort
status: Status
remove: Remove
toggle_manual_sorting: Toggle Manual Sorting
bookmark_doesnt_exist: Bookmark Doesn't Exist
bookmark_doesnt_exist_copy: The bookmark you're trying to open couldn't be found.
@@ -481,6 +487,10 @@ operators:
nnull: Isn't null
contains: Contains
ncontains: Doesn't contain
starts_with: Starts with
nstarts_with: Doesn't start with
ends_with: Ends with
nends_with: Doesn't end with
between: Is between
nbetween: Isn't between
empty: Is empty
@@ -807,7 +817,7 @@ referential_action_set_default: Set {field} to its default value
choose_action: Choose Action
continue: Continue
continue_as: >-
<b>{name}</b> is currently authenticated. If you recognize this account, press continue.
{name} is currently authenticated. If you recognize this account, press continue.
editing_role: '{role} Role'
creating_webhook: Creating Webhook
default: Default
@@ -884,6 +894,11 @@ interfaces:
allow_other: Allow Other
show_more: 'Show {count} more'
items_shown: Items Shown
select-multiple-checkbox-tree:
name: Checkboxes (Tree)
description: Choose between multiple options via nested checkboxes
value_combining: Value Combining
value_combining_note: Controls what value is stored when nested selections are made.
input-code:
code: Code
description: Write or share code snippets

View File

@@ -378,6 +378,7 @@ sort_field: Campo Ordenamiento
add_sort_field: Añadir campo de ordenación
sort: Ordenar
status: Estatus
remove: Eliminar
toggle_manual_sorting: Alternar Ordenamiento Manual
bookmark_doesnt_exist: El Marcador no existe
bookmark_doesnt_exist_copy: El Marcador que está intentando abrir no pudo ser encontrado.
@@ -793,8 +794,6 @@ referential_action_set_null: Anular el campo {field}
referential_action_set_default: Establecer {field} a su valor predeterminado
choose_action: Elegir acción
continue: Continuar
continue_as: >-
Actualmente <b>{name}</b> ha iniciado sesión. Si reconoce esta cuenta, presione continuar.
editing_role: 'Rol {role}'
creating_webhook: Creando Webhook
default: Predeterminado

View File

@@ -378,6 +378,7 @@ sort_field: Campo Ordenamiento
add_sort_field: Añadir campo de ordenación
sort: Ordenar
status: Estado
remove: Eliminar
toggle_manual_sorting: Alternar Ordenamiento Manual
bookmark_doesnt_exist: El Marcador no existe
bookmark_doesnt_exist_copy: El Marcador que está intentando abrir no pudo ser encontrado.
@@ -793,8 +794,6 @@ referential_action_set_null: Anular el campo {field}
referential_action_set_default: Establecer {field} a su valor predeterminado
choose_action: Elegir acción
continue: Hacer continuación
continue_as: >-
Actualmente <b>{name}</b> ha iniciado sesión. Si reconoce esta cuenta, presione continuar.
editing_role: 'Rol {role}'
creating_webhook: Creando Webhook
default: Por defecto

View File

@@ -378,6 +378,7 @@ sort_field: Campo Ordenamiento
add_sort_field: Añadir campo de ordenación
sort: Ordenar
status: Estado
remove: Eliminar
toggle_manual_sorting: Alternar Ordenamiento Manual
bookmark_doesnt_exist: El Marcador no existe
bookmark_doesnt_exist_copy: El Marcador que está intentando abrir no pudo ser encontrado.
@@ -793,8 +794,6 @@ referential_action_set_null: Anular el campo {field}
referential_action_set_default: Establecer {field} a su valor predeterminado
choose_action: Elegir acción
continue: Continuar
continue_as: >-
Actualmente <b>{name}</b> ha iniciado sesión. Si reconoce esta cuenta, presione continuar.
editing_role: 'Rol {role}'
creating_webhook: Creando Webhook
default: Por defecto

View File

@@ -378,6 +378,7 @@ sort_field: Sorteerimisväli
add_sort_field: "Lisa sorteerimisväli\n"
sort: Sorteeri
status: Staatus
remove: Eemalda
toggle_manual_sorting: Luba käsitsi sorteerimine
bookmark_doesnt_exist: Järjehoidjat ei leitud
bookmark_doesnt_exist_copy: Järjehoidja, mida sa soovid kasutada, ei leitud.
@@ -794,8 +795,6 @@ referential_action_set_null: Nulli {field} väli
referential_action_set_default: Seadista {field} väljale vaikeväärtus
choose_action: Vali tegevus
continue: Continue
continue_as: >-
<b>{name}</b> on selle projekti jaoks juba autenditud. Kui tunned selle konto ära, siis vajuta Jätka.
editing_role: '{role} roll'
creating_webhook: Loo veebikonks
default: Vaikimisi

View File

@@ -378,6 +378,7 @@ sort_field: Lajittelukenttä
add_sort_field: Lisää lajittelukenttä
sort: Järjestä
status: Tila
remove: Poista
toggle_manual_sorting: Vaihda manuaalinen lajittelu
bookmark_doesnt_exist: Kirjamerkkiä ei ole olemassa
bookmark_doesnt_exist_copy: Kirjanmerkkiä, jota yrität avata, ei löytynyt.
@@ -787,8 +788,6 @@ relational_triggers: Relatiiviset käynnistimet
referential_action_no_action: Estä poisto
choose_action: Valitse toiminto
continue: Jatka
continue_as: >-
<b>{name}</b> on tällä hetkellä autentikoitu. Jos tunnistat tämän tilin, paina jatka.
editing_role: '{role} rooli'
creating_webhook: Luodaan webhook
default: Oletus

View File

@@ -379,6 +379,7 @@ sort_field: Champ de tri
add_sort_field: Ajouter un champ de tri
sort: Trier
status: État
remove: Retirer
toggle_manual_sorting: Activer/désactiver le tri manuel
bookmark_doesnt_exist: Le favori n'existe pas
bookmark_doesnt_exist_copy: Le favori que vous essayez d'ouvrir est introuvable.
@@ -795,8 +796,6 @@ referential_action_set_null: Vider le champ {field}
referential_action_set_default: Remettre {field} à sa valeur par défaut
choose_action: Choisir une action
continue: Continuer
continue_as: >-
<b>{name}</b> est actuellement authentifié. Si vous reconnaissez ce compte, cliquez sur continuer.
editing_role: 'Rôle {role}'
creating_webhook: Création du Webhook
default: Défaut
@@ -855,7 +854,6 @@ template: Modèle
translation: Traduction
value: Valeur
view_project: Voir le projet
weeks: { }
report_error: Signaler l'erreur
interfaces:
presentation-links:

View File

@@ -277,6 +277,7 @@ card_size: Kártya mérete
sort_field: Rendezési mező
sort: Rendezés
status: Állapot
remove: Eltávolítás
toggle_manual_sorting: Manuális rendezés ki/bekapcsolása
bookmark_doesnt_exist: Könyvjelző nem létezik
bookmark_doesnt_exist_copy: Ez a könyvjelző nem található.

View File

@@ -372,6 +372,7 @@ sort_field: Sortir Baris
add_sort_field: Tambah Baris Sortir
sort: Sortir
status: Status
remove: Hapus
toggle_manual_sorting: Aktifkan Sortir Manual
bookmark_doesnt_exist: Bookmark Tidak Tersedia
bookmark_doesnt_exist_copy: Bookmark yang Anda ingin bukan tidak dapat ditemukan.

View File

@@ -346,6 +346,7 @@ sort_field: Ordina per
add_sort_field: Aggiungi Campo Ordinamento
sort: Ordina
status: Stato
remove: Rimuovere
bookmark_doesnt_exist: Segnalibro Non Esiste
bookmark_doesnt_exist_copy: Il segnalibro che stai cercando di aprire non è stato trovato.
select_an_item: Seleziona un elemento...
@@ -698,8 +699,6 @@ referential_action_field_label_m2o: Alla cancellazione di {collection}...
referential_action_set_default: Imposta {field} al suo valore predefinito
choose_action: Scegli azione
continue: Continua
continue_as: >-
<b>{name}</b> è già autenticato. Se riconosci questo account, premi continua.
editing_role: '{role} Ruolo'
default: Predefinito
delete: Elimina

View File

@@ -188,6 +188,7 @@ modified_on: 修正日時
sort_field: フィールドの並べ替え
sort: 並べ替え
status: ステータス
remove: 削除
toggle_manual_sorting: 手動ソートを有効にする
bookmark_doesnt_exist: ブックマークは存在しません
bookmark_doesnt_exist_copy: 開こうとしているブックマークが見つかりませんでした。

View File

@@ -331,6 +331,7 @@ card_size: Kortelės dydis
sort_field: Rūšiavimo laukas
sort: Rūšiuoti
status: Būsena
remove: Pašalinti
toggle_manual_sorting: Perjungti rankinį rūšiavimą
bookmark_doesnt_exist: Žymė neegzistuoja
bookmark_doesnt_exist_copy: Žymė, kurią bandote atidaryti, neegzistuoja.
@@ -683,8 +684,6 @@ block: Blokas
inline: Toje pačioje eilutėje
comment: Komentaras
continue: Tęsti
continue_as: >-
<b>{name}</b> jau yra prisijungęs. Jeigu tai jūs, spauskite tęsti.
editing_role: '{role} grupė'
creating_webhook: Kuriamas Webhook'ą
default: Numatytasis

View File

@@ -50,6 +50,7 @@ activity: Aktiviti
user_directory: Direktori pengguna
duration: Durasi
status: Status
remove: Buang
bookmarks: Penandabuku
back: Kembali
operators:

View File

@@ -378,6 +378,7 @@ sort_field: Sorteerveld
add_sort_field: Voeg Sorteerveld Toe
sort: Sorteren
status: Status
remove: Verwijder
toggle_manual_sorting: Activeer handmatige volgorde
bookmark_doesnt_exist: Bladwijzer bestaat niet
bookmark_doesnt_exist_copy: De bladwijzer die je probeert te openen kon niet worden gevonden.
@@ -775,8 +776,6 @@ block: Blok
inline: Doorlopend
comment: Reactie
continue: Doorgaan
continue_as: >-
<b>{name}</b> is momenteel ingelogd. Als je dit account herkent, druk dan op 'Doorgaan'.
editing_role: '{role} Rol'
creating_webhook: Webhook Aanmaken
default: Standaard

View File

@@ -315,6 +315,7 @@ sort_field: Sorter felt
add_sort_field: Legg til sorteringsfelt
sort: Sorter
status: Status
remove: Fjern
toggle_manual_sorting: Veksle manuell sortering
bookmark_doesnt_exist: Bokmerket finnes ikke
bookmark_doesnt_exist_copy: Kunne ikke finne bokmerket du prøver å åpne.

View File

@@ -161,7 +161,7 @@ this_will_auto_setup_fields_relations: Spowoduje to automatyczne ustawienie wszy
click_here: Kliknij tutaj
to_manually_setup_translations: aby ręcznie ustawić tłumaczenia.
click_to_manage_translated_fields: >-
Nie ma jeszcze tłumaczonych pól. Kliknij tutaj, aby je stworzyć. | Istnieje jedno tłumaczone pole. Kliknij tutaj, aby nim zarządzać. | Istnieją {count} tłumaczone pól. Kliknij tutaj, aby nimi zarządzać.
Nie ma jeszcze przetłumaczonych pól. Kliknij tutaj, aby je stworzyć. | Istnieje jedno przetłumaczone pole. Kliknij tutaj, aby nim zarządzać. | Ilość przetłumaczonych pół: {count}. Kliknij tutaj, aby nimi zarządzać.
fields_group: Grupa pól
no_collections_found: Nie znaleziono żadnych kolekcji.
new_data_alert: 'W modelu danych zostaną utworzone następujące elementy:'
@@ -364,7 +364,7 @@ field_width: Szerokość pola
add_filter: Dodaj Filtr
upper_limit: Górny limit...
lower_limit: Dolny limit...
user_directory: Katalog użytkownika
user_directory: Katalog użytkowników
documentation: Dokumentacja
sidebar: Pasek boczny
duration: Czas trwania
@@ -378,6 +378,7 @@ sort_field: Pole sortowania
add_sort_field: Dodaj pole sortowania
sort: Sortuj
status: Status
remove: Usuń
toggle_manual_sorting: Przełącz ręczne sortowanie
bookmark_doesnt_exist: Zakładka nie istnieje
bookmark_doesnt_exist_copy: Nie znaleziono zakładki, którą próbujesz otworzyć.
@@ -656,7 +657,7 @@ page_help_settings_presets_item: >-
**Ustawienia szczegółów** — Formularz do zarządzania zakładkami i domyślnymi ustawieniami kolekcji.
page_help_settings_webhooks_collection: '**Przeglądaj Webhooks** — zawiera listę wszystkich webhooków w ramach projektu.'
page_help_settings_webhooks_item: '**Szczegóły Webhook** — Formularz do tworzenia i zarządzania webhookami projektu.'
page_help_users_collection: '**Katalog Użytkownika** — Lista wszystkich użytkowników systemu w tym projekcie.'
page_help_users_collection: '**Katalog Użytkowników** — Lista wszystkich użytkowników systemu w tym projekcie.'
page_help_users_item: >-
**Szczegóły użytkownika** — Zarządzaj informacjami o koncie lub zobacz szczegóły innych użytkowników.
activity_feed: Kanał aktywności
@@ -794,8 +795,6 @@ referential_action_set_null: Pola oznaczone jako bez wartości {field}
referential_action_set_default: Ustaw {field} na wartość domyślną
choose_action: Wybierz akcję
continue: Kontynuuj
continue_as: >-
<b>{name}</b> jest obecnie uwierzytelniony. Jeśli rozpoznajesz to konto, naciśnij przycisk Kontynuuj.
editing_role: 'Rola {role}'
creating_webhook: Tworzenie Webhooka
default: Domyślnie
@@ -843,7 +842,7 @@ select_existing: Wybierz istniejące
select_field_type: Wybierz typ pola
select_interface: Wybierz interfejs
settings: Ustawienia
sign_in: Zarejestruj się
sign_in: Zaloguj się
sign_out: Wyloguj się
sign_out_confirm: Na pewno chcesz się wylogować?
something_went_wrong: Coś poszło nie tak.

View File

@@ -379,6 +379,7 @@ sort_field: Campo de ordenação
add_sort_field: Adicionar campo de ordenação
sort: Ordenar
status: Status
remove: Remover
toggle_manual_sorting: Alternar ordenação manual
bookmark_doesnt_exist: Marcador não existe
bookmark_doesnt_exist_copy: O marcador que você está tentando abrir não pôde ser encontrado.
@@ -794,8 +795,6 @@ referential_action_cascade: Excluir o item {collection} (cascade)
referential_action_set_default: Definir {field} para o seu valor padrão
choose_action: Escolha uma ação
continue: Continuar
continue_as: >-
<b>{name}</b> está atualmente autenticado. Se você reconhecer esta conta, clique em continuar.
editing_role: 'Cargo {role}'
creating_webhook: Criando Webhook
default: Padrão

View File

@@ -368,6 +368,7 @@ sort_field: Поле сортировки
add_sort_field: Добавить поле сортировки
sort: Сортировать
status: Статус
remove: Удалить
toggle_manual_sorting: Переключить Ручную Сортировку
bookmark_doesnt_exist: Закладка Не Существует
bookmark_doesnt_exist_copy: Закладка, которую вы пытаетесь открыть не найдена.
@@ -767,8 +768,6 @@ save_current_datetime: Сохранить Текущую Дату/Время
block: Блокировать
comment: Комментарий
continue: Продолжить
continue_as: >-
<b>{name}</b> в настоящее время аутентифицирован. Если вы узнаете этот аккаунт, нажмите продолжить.
editing_role: '{role} Роль'
creating_webhook: Создание Веб-хука
default: По умолчанию

View File

@@ -379,6 +379,7 @@ sort_field: Polje za Sortiranje
add_sort_field: Dodaj Polje za Sortiranje
sort: Sortiranje
status: Status
remove: Ukloni
toggle_manual_sorting: Uključi Ručno Sortiranje
bookmark_doesnt_exist: Oznaka ne postoji
bookmark_doesnt_exist_copy: Oznaku koju pokušavate otvoriti nije moguće pronaći.
@@ -795,8 +796,6 @@ referential_action_set_null: Poništi vrijednost {field} polja
referential_action_set_default: Postavi {field} na njegovu podrazumijevanu vrijednost
choose_action: Izaberi Radnju
continue: Nastavi
continue_as: >-
<b>{name}</b> je trenutno autorizovan. Ukoliko prepoznajete ovaj korisnički račun, pritisnite nastavi.
editing_role: '{role} Uloga'
creating_webhook: Kreiranje Webhook-a
default: Podrazumijevano
@@ -855,7 +854,6 @@ template: Šablon
translation: Prevodi
value: Vrijednost
view_project: Pregledaj Projekat
weeks: { }
report_error: Prijavi Grešku
interfaces:
presentation-links:

View File

@@ -367,6 +367,7 @@ sort_field: Sortera fält
add_sort_field: Lägg till sorteringsfält
sort: Sortera
status: Status
remove: Ta bort
toggle_manual_sorting: Växla manuell sortering
bookmark_doesnt_exist: Bokmärket finns inte
bookmark_doesnt_exist_copy: Bokmärket du försöker öppna kunde inte hittas.
@@ -759,8 +760,6 @@ block: Blockera
inline: Infogad
comment: Kommentera
continue: Fortsätt
continue_as: >-
<b>{name}</b> är för närvarande autentiserad. Om du känner igen detta konto, tryck fortsätt.
editing_role: '{role} roll'
creating_webhook: Skapar Webhook
default: Standard

View File

@@ -11,6 +11,7 @@ add_field: เพิ่มฟิลด์
role_name: ชื่อบทบาท
db_only_click_to_configure: 'ฐานข้อมูลเท่านั้น: คลิกเพื่อกำหนดค่า '
show_archived_items: แสดงรายการเก็บถาวร
edited: ค่าที่ถูกแก้ไข
required: จำเป็นต้องระบุ
required_for_app_access: จำเป็นต้องระบุเพื่อเข้าถึงแอป
requires_value: ต้องการข้อมูล
@@ -19,7 +20,9 @@ create_role: สร้างบทบาท
create_user: สร้างผู้ใช้งาน
create_webhook: สร้างเว็บฮุก
invite_users: เชิญชวนผู้ใช้งาน
email_examples: "admin{'@'}example.com, user{'@'}example.com..."
invite: เชิญชวน
email_already_invited: อีเมล "{email}" ได้รับเชิญแล้ว
emails: อีเมล
connection_excellent: สัญญาณดีมาก
connection_good: สัญญาณดี
@@ -79,9 +82,13 @@ validationError:
nnull: ต้องไม่เป็นค่า null
required: ต้องระบุค่า
unique: ค่าต้องไม่ซ้ำ
regex: รูปแบบข้อมูลไม่ถูกต้อง
all_access: เข้าถึงได้ทั้งหมด
no_access: ไม่ให้เข้าถึงได้ทั้งหมด
use_custom: ใช้ค่ากำหนดเอง
nullable: สามารถเป็นค่าว่างได้
allow_null_value: อนุญาตเป็นค่าว่างได้
enter_value_to_replace_nulls: โปรดป้อนค่าใหม่ที่ไม่ใช่ค่าว่างในฟิลด์นี้
field_standard: มาตรฐาน
field_presentation: การแสดงผลและนาวแผง
field_file: ไฟล์เดียว
@@ -136,6 +143,7 @@ decimal: เลขฐานสิบ
float: เลขทศนิยม
integer: จำนวนเต็ม
json: JSON
xml: XML
string: ข้อความแบบสั้น
text: ข้อความแบบยาว
time: เวลา
@@ -144,6 +152,11 @@ uuid: UUID
hash: ค่าแฮช
not_available_for_type: ไม่มีสำหรับชนิดนี้
create_translations: สร้างการแปลใหม่
auto_refresh: รีเฟรซอัตโนมัติ
refresh_interval: ช่วงรีเฟรซ
no_refresh: อย่ารีเฟรซ
refresh_interval_seconds: รีเฟรชทันที หรือทุกวินาที หรือทุกๆ {seconds} วินาที
refresh_interval_minutes: ทุกนาที หรือทุก {minutes} นาที
auto_generate: สร้างอัตโนมัติ
this_will_auto_setup_fields_relations: นี่จะตั้งค่าทุกฟิลด์ และความสัมพันธ์ที่จำเป็นโดยอัตโนมัติ
click_here: คลิกที่นี่
@@ -151,7 +164,9 @@ to_manually_setup_translations: ตั้งค่าการแปลภาษ
click_to_manage_translated_fields: >-
ยังไม่มีฟิลด์ที่ถูกแปล คลิ๊กตรงนี้เพื่อเริ่มแปล | มี 1 ฟิลด์ที่ถูกแปลแล้ว คลิ๊กตรงนี้เพื่อจัดการ | มี {count} ฟิลด์ที่ถูกแปลแล้ว คลิ๊กตรงนี้เพื่อจัดการ
fields_group: กลุ่มของฟิลด์
no_collections_found: ไม่พบตาราง
new_data_alert: 'สิ่งเหล่านี้จะถูกสร้างใน Data Model ของคุณ'
search_collection: ค้นหาตาราง...
new_field: 'สร้างฟิลด์ใหม่'
new_collection: 'คอลเลกชันใหม่'
add_m2o_to_collection: 'เพิ่ม Many-to-One ให้ "{collection}"'
@@ -218,6 +233,7 @@ item_delete_success: ไอเท็มถูกลบแล้ว|ไอเท
this_collection: คอลเลกชันนี้
related_collection: คอลเลกชันที่เกี่ยวข้อง
related_collections: คอลเลกชันที่เกี่ยวข้อง
translations_collection: การแปลตาราง
languages_collection: คอลเลกชันภาษา
export_data: ส่งข้อมูลออก
format: รูปแบบ
@@ -304,6 +320,10 @@ save_and_create_new: บันทึกและสร้างใหม่อ
save_and_stay: บันทึกและอยู่หน้าเดิม
save_as_copy: บันทึกเป็นสำเนา
add_existing: เพิ่มจากที่มีอยู่
creating_items: สร้างรายการ
enable_create_button: เปิดใช้งานปุ่มสร้าง
selecting_items: เลือกรายการ
enable_select_button: เปิดใช้งานปุ่มเลือก
comments: ความคิดเห็น
no_comments: ยังไม่มีความเห็น
click_to_expand: คลิกเพื่อขยาย
@@ -318,6 +338,7 @@ interface_not_found: 'ไม่พบอินเทอร์เฟส "{interfa
reset_interface: ล้างการตั้งค่าอินเทอร์เฟส
display_not_found: 'ไม่พบการแสดงผล "{display}"'
reset_display: ล้างค่าการแสดงผล
list-m2a: ตัวสร้าง (M2A)
item_count: 'ไม่พบไอเท็ม|1 ไอเท็ม|{count} ไอเท็ม'
no_items_copy: ไม่มีรายการในคอลเล็กชันนี้
file_count: 'ไม่มีไฟล์ | 1 ไฟล์ | {count} ไฟล์'
@@ -358,6 +379,7 @@ sort_field: เรียงฟิลด์
add_sort_field: เพิ่มฟิลด์สำหรับเรียง
sort: เรียง
status: สถานะ
remove: เอาออก
toggle_manual_sorting: สลับค่าการเรียงด้วยตนเอง
bookmark_doesnt_exist: ไม่มีบุ๊กมาร์ก
bookmark_doesnt_exist_copy: ไม่พบบุ๊กมาร์กที่พยายามเปิด
@@ -384,6 +406,8 @@ errors:
ITEM_NOT_FOUND: ไม่พบไอเท็ม
ROUTE_NOT_FOUND: ไม่พบข้อมูลที่ต้องการ
RECORD_NOT_UNIQUE: พบค่าที่ซ้ำกัน
USER_SUSPENDED: ผู้ใช้ถูกระงับ
CONTAINS_NULL_VALUES: ฟิลด์มีค่าว่าง
UNKNOWN: พบข้อผิดพลาดบางอย่างที่ไม่คาดคิด
INTERNAL_SERVER_ERROR: พบข้อผิดพลาดบางอย่างที่ไม่คาดคิด
value_hashed: ข้อมูลถูกเข้ารหัสไว้อย่างปลอดภัย
@@ -461,6 +485,8 @@ operators:
has: ประกอบด้วยบางคีย์เหล่านี้
loading: กำลังโหลด...
drop_to_upload: วางเพื่ออัพโหลด
item: รายการ
items: รายการ
upload_file: อัพโหลดไฟล์
upload_file_indeterminate: กำลังอัพโหลดไฟล์...
upload_file_success: อัพโหลดไฟล์แล้ว
@@ -478,6 +504,8 @@ value_unique: ค่าต้องไม่ซ้ำ
all_activity: กิจกรรมทั้งหมด
create_item: สร้างไอเท็ม
display_template: แม่แบบการแสดงผล
language_display_template: แม่แบบการแสดงภาษา
translations_display_template: แม่แบบการแปลภาษา
n_items_selected: 'ไม่มีรายการที่เลือก | เลือกแล้ว 1 รายการ | เลือกแล้ว {n} รายการ'
per_page: ต่อหน้า
all_files: ทุกไฟล์
@@ -508,8 +536,21 @@ toggle: สลับ
icon_on: แสดงไอคอน
icon_off: ไม่แสดงไอคอน
label: ป้าย
image_url: URL ของรูปภาพ
alt_text: ข้อความแสดงแทน
media: สื่อ
width: กว้าง
height: สูง
source: แหล่งที่มา
url_placeholder: ป้อน URL
display_text: ข้อความแสดง
display_text_placeholder: ป้อนข้อความแสดง...
tooltip: คำแนะนำ
tooltip_placeholder: ใส่คำแนะนำ
unlimited: ไม่จำกัด
open_link_in: เปิดลิงก์
new_tab: แท็บใหม่
current_tab: แท็บปัจจุบัน
wysiwyg_options:
aligncenter: จัดตำแหน่งกึ่งกลาง
alignjustify: จัดตำแหน่งชิดขอบซ้ายและขวา
@@ -529,7 +570,10 @@ wysiwyg_options:
bullist: รายการแบบไม่มีลําดับ
numlist: รายการแบบมีลำดับ
hr: เส้นคั่นแนวนอน
link: เพิ่ม/แก้ไข ลิงค์
unlink: ลบลิงค์
media: เพิ่ม/แก้ไข สื่อ
image: เพิ่ม/แก้ไขภาพ
copy: คัดลอก
cut: ตัด
paste: วาง
@@ -551,6 +595,7 @@ wysiwyg_options:
selectall: เลือกทั้งหมด
table: ตาราง
visualaid: แสดงเครื่องหมายที่มองไม่เห็น
source_code: แก้ไข Source Code
fullscreen: เต็มจอ
directionality: อย่างมีทิศทาง
dropdown: ตัวเลือกแบบดรอปดาวน์
@@ -563,6 +608,8 @@ adding_user: เพิ่มผู้ใช้
unknown_user: ผู้ใช้ที่ไม่รู้จัก
creating_in: 'กำลังสร้างไอเท็มใน {collection}'
editing_in: 'กำลังแก้ไขไอเท็มใน {collection}'
creating_unit: 'สร้าง {unit}'
editing_unit: 'แก้ไข {unit}'
editing_in_batch: 'กำลังแก้ไข {count} ไอเท็ม'
no_options_available: ไม่มีตัวเลือกให้ใช้
settings_data_model: รูปแบบข้อมูล
@@ -570,10 +617,13 @@ settings_permissions: บทบาทและสิทธิ
settings_project: การตั้งค่าโครงการ
settings_webhooks: Webhooks
settings_presets: ค่าที่ตั้งไว้ล่วงหน้าและบุ๊คมาร์ก
one_or_more_options_are_missing: ไม่พบตัวเลือกรายการ
scope: ขอบเขต
select: เลือก...
layout: เค้าโครง
tree_view: มุมมองแผนภูมิ
changes_are_permanent: การเปลี่ยนแปลงนี้ถาวร
preset_name_placeholder: ให้ค่าเริ่มต้นเมื่อค่าว่าง
preset_search_placeholder: ค้นหาด้วย...
editing_preset: แก้ไขค่าที่ตั้งไว้ล่วงหน้า
layout_preview: ตัวอย่างเค้าโครง
@@ -643,11 +693,14 @@ fields:
display_template: แม่แบบการแสดงผล
hidden: ซ่อน
singleton: Singleton
translations: ชุดการตั้งชื่อการแปล
archive_app_filter: เก็บตัวกรองแอป
archive_value: เก็บค่า
unarchive_value: ยกเลิกการเก็บค่า
sort_field: เรียงฟิลด์
accountability: การติดตามกิจกรรมและการแก้ไข
directus_files:
$thumbnail: รูปขนาดย่อ
title: ตำแหน่ง
description: รายละเอียด
tags: แท็ก
@@ -720,6 +773,11 @@ fields:
users: ผู้ใช้ในบทบาทนี้
module_list: รายการโมดูล
collection_list: รายการคอลเลกชัน
field_options:
directus_collections:
track_activity_revisions: ติดตามกิจกรรมและการแก้ไข
only_track_activity: ติดตามกิจกรรมเท่านั้น
do_not_track_anything: อย่าติดตามอะไรเลย
no_fields_in_collection: 'ยังไม่มีฟิลด์ใน {collection}'
do_nothing: ไม่ต้องทำอะไร
generate_and_save_uuid: สร้างและบันทึก UUID โดยอัตโนมัติ
@@ -729,9 +787,15 @@ save_current_datetime: บันทึกวันเวลาปัจจุบ
block: ปิดกั้น
inline: แบบอินไลน์
comment: ความคิดเห็น
relational_triggers: ทริกเกอร์เชิงสัมพันธ์
referential_action_field_label_m2o: เมื่อลบ {collection}...
referential_action_field_label_o2m: เมื่อยกเลิกการเลือก {collection}...
referential_action_no_action: ป้องกันการลบ
referential_action_cascade: ลบ {collection} รายการ (เรียงซ้อน)
referential_action_set_null: "ทำให้ฟิลด์ {field} เป็นโมฆะ\n"
referential_action_set_default: ตั้งค่า {field} เป็นค่าเริ่มต้น
choose_action: เลือกการกระทำ
continue: ทำต่อ
continue_as: >-
บัญชี <b>{name}</b> ยังไม่ถูกยืนยัน ถ้าคุณคุ้นเคยกับบัญชีนี้ คลิ๊กทำต่อ
editing_role: 'บทบาท {role}'
creating_webhook: สร้าง webhook
default: ค่าเริ่มต้น
@@ -793,9 +857,25 @@ view_project: ดูโครงการ
report_error: แจ้งข้อผิดพลาด
interfaces:
presentation-links:
presentation-links: ปุ่มลิงก์
links: ลิงค์
description: ปุ่มลิงค์ที่สามารถตั้งค่าได้เพื่อเปิด URLs
style: สไตล์
primary: หลัก
link: ลิงค์
button: ปุ่ม
error: ไม่สามารถดำเนินการได้
select-multiple-checkbox:
checkboxes: ตัวเลือกแบบกล่อง
description: เลือกได้มากกว่าครั้งละ 1 ตัวเลือก ด้วยตัวเลือกแบบกล่อง
allow_other: อนุญาตคนอื่น
show_more: 'แสดงเพิ่มอีก {count}'
items_shown: ไอเท็มที่ถูกแสดง
input-code:
code: โค้ด
description: เขียนหรือแชร์โค้ดสั้นๆ
line_number: หมายเลขบรรทัด
placeholder: กรอกโค้ดที่นี่
system-collection:
collection: คอลเลกชัน
description: เลือคอลเลกชันที่มีอยู่แล้ว
@@ -806,6 +886,11 @@ interfaces:
include_system_collections: รวมถึงคอลเลกชันของระบบ
select-color:
color: สี
description: กรอกหรือเลือกค่าของสี
placeholder: เลือกสี...
preset_colors: สีที่กำหนดไว้ล่วงหน้า
preset_colors_add_label: เพิ่มสีใหม่...
name_placeholder: กรอกชื่อสี
datetime:
datetime: วันที่และเวลา
description: กรอกวันที่และเวลา
@@ -814,12 +899,30 @@ interfaces:
use_24: ใช้รูปแบบ 24 ชั่วโมง
system-display-template:
display-template: แม่แบบการแสดงผล
description: ผสมระหว่างข้อความและค่าของฟิลด์
collection_field: ฟิลด์ตาราง
collection_field_not_setup: การตั้งค่าของฟิลด์ยังไม่ถูกต้อง
select_a_collection: เลือกคอลเลกชัน
presentation-divider:
divider: ตัวแบ่ง
description: แบ่งฟิลด์ออกเป็นส่วนๆ
title_placeholder: ป้อนชื่อเรื่อง
inline_title: ชื่อเรื่องแบบบรรทัดเดียวกัน
inline_title_label: แสดงชื่อในเส้นแบ่ง
margin_top: ระยะห่างด้านบน
margin_top_label: เพิ่มระยะห่างด้านบน
select-dropdown:
description: เลือกค่าจากตัวเลือก
choices_placeholder: เพิ่มตัวเลือกใหม่
allow_other: อนุญาตคนอื่น
allow_other_label: อนุญาตให้เพิ่มค่าอื่นได้
allow_none: ไม่อนุญาตเลย
allow_none_label: อนุญาตให้ไม่เลือกอะไรเลย
choices_name_placeholder: กรอกชื่อ...
choices_value_placeholder: กรุณาใส่ค่า
select-multiple-dropdown:
select-multiple-dropdown: ตัวเลือก (เลือกได้มากกว่า 1 ค่า)
description: เลือกได้มากกว่า 1 ค่าจากตัวเลือก
file:
file: ไฟล์
description: เลือกหรืออัพโหลดไฟล์
@@ -828,14 +931,57 @@ interfaces:
description: เลือกหรืออัพโหลดทีละหลายไฟล์
input-hash:
hash: ค่าแฮช
description: กรอกค่าที่ต้องการเข้ารหัส
masked: กำหนดหน้ากาก
masked_label: ซ่อนค่าจริง
select-icon:
icon: ไอคอน
description: เลือกไอคอนจากตัวเลือก
search_for_icon: ค้นหาไอคอน...
file-image:
image: รูปภาพ
description: เลือกหรืออัพโหลดรูปภาพ
system-interface:
interface: อินเทอร์เฟซ
description: เลือกอินเทอเฟสที่มีอยู่แล้ว
placeholder: เลือกอินเทอร์เฟส...
system-interface-options:
interface-options: ตัวเลือกอินเทอร์เฟส
description: กล่องข้อความสำหรับเลือกตัวเลือกของอินเทอร์เฟส
list-m2m:
many-to-many: Many to Many
description: เลือกหลายไอเท็มที่เกี่ยวข้อง
select-dropdown-m2o:
many-to-one: Many to One
description: เลือก 1 ไอเท็มที่เกี่ยวข้อง
display_template: แม่แบบการแสดงผล
input-rich-text-md:
markdown: Markdown
description: กรอกและดู Markdown ล่วงหน้าก่อน
customSyntax: บล็อกที่ออกแบบเอง
customSyntax_label: เพิ่ม syntax ที่ออกแบบเอง
customSyntax_add: เพิ่ม syntax ที่ออกแบบเอง
box: กล่อง/บรรทัดเดียวกัน
imageToken: Image Token
imageToken_label: Image Token ที่จะถูกต่อเข้าไปที่แห่งที่มาของรูปภาพ
presentation-notice:
notice: แจ้งให้ทราบ
description: แสดงการแจ้งให้ทราบแบบย่อ
text: กรอกเนื้อหาการแจ้งให้ทราบที่นี่...
list-o2m:
one-to-many: One to Many
description: เลือกหลายไอเท็มที่เกี่ยวข้อง
no_collection: ไม่พบตาราง
select-radio:
radio-buttons: ปุ่มตัวเลือก
description: เลือกได้ 1 จากหลายตัวเลือก
list:
repeater: การทำซ้ำ
description: สร้างหลายรายการโดยใช้โครงสร้างเดียวกัน
edit_fields: แก้ไขฟิลด์
add_label: 'ป้ายกำกับ "สร้างใหม่"'
field_name_placeholder: กรอกชื่อฟิลด์
field_note_placeholder: กรอกโน้ตให้ฟิลด์
slider:
slider: แถบเลื่อน
description: เลือกตัวเลขโดยใช้แถบเลื่อน
@@ -854,12 +1000,34 @@ interfaces:
alphabetize: เรียงตามตัวอักษร
alphabetize_label: บังคับเรียงตามตัวอักษร
add_tags: เพิ่มแท็ก...
input:
input: Input
description: ป้อนค่าด้วยตนเอง
trim: ตัดขอบ
trim_label: ตัดช่องว่างหน้าหลัง
mask: กำหนดหน้ากาก
mask_label: ซ่อนค่าจริง
clear: ล้างค่า
clear_label: บันทึกเป็นค่าว่าง
minimum_value: ค่าน้อยสุด
maximum_value: ค่ามากสุด
step_interval: ขั้นในการเพิ่มลดค่า
slug: Slugify
slug_label: ทำให้ URL ค่าที่ป้อนปลอดภัย
input-multiline:
textarea: กล่องข้อความ
description: กรอกข้อความได้แบบหลายบรรทัด
boolean:
toggle: สลับ
description: สลับระหว่าง เปิด และ ปิด
label_placeholder: กรอกชื่อ...
label_default: เปิดใช้งาน
translations:
display_template: แม่แบบการแสดงผล
no_collection: ไม่มีคอลเลกชัน
list-o2m-tree-view:
description: มุมมองแผนภูมิสำหรับรายการข้อมูลหนึ่งต่อหลายรายการแบบซ้ำซ้อน
recursive_only: อินเทอร์เฟซมุมมองแผนภูมิใช้งานได้กับความสัมพันธ์แบบซ้ำซ้อนเท่านั้น
user:
user: ผู้ใช้
description: เลือกผู้ใช้ของ directus ที่มีอยู่แล้ว
@@ -868,6 +1036,19 @@ interfaces:
auto: อัตโนมัติ
dropdown: ตัวเลือกแบบดรอปดาวน์
modal: กล่อง
input-rich-text-html:
wysiwyg: WYSIWYG
description: เขียนข้อความที่สามารถตกแต่งได้ด้วย HTML
toolbar: แถบเครื่องมือ
custom_formats: รูปแบบที่กำหนดเอง
options_override: ตัวเลือกแทนที่
input-autocomplete-api:
input-autocomplete-api: ป้อนข้อมูลอัตโนมัติ (API)
description: การพิมพ์ล่วงหน้าสำหรับค่า API ภายนอก
results_path: ส่วนผลลัพธ์
value_path: ส่วนค่า
trigger: Trigger
rate: ' ให้คะแนน'
displays:
boolean:
boolean: ค่าบูลีน
@@ -970,3 +1151,7 @@ layouts:
comfortable: สบายตา
compact: กระชับ
cozy: สบาย
calendar:
calendar: ปฏิทิน
start_date_field: ฟิลด์วันที่เริ่มต้น
end_date_field: ฟิลด์วันที่สิ้นสุด

View File

@@ -359,6 +359,7 @@ card_size: Kart Boyutu
sort_field: Sıralama Alanı
sort: Sırala
status: Durum
remove: Kaldır
toggle_manual_sorting: Elle sıralamayı aç/kapa
bookmark_doesnt_exist_cta: Koleksiyona dön
select_an_item: Seçim Yapın...
@@ -611,8 +612,6 @@ do_nothing: Hiçbir Şey Yapma
block: Blok
comment: Yorum
continue: Devam
continue_as: >-
<b>{name}</b> bu projede oturum açmış durumda. Bu hesabı tanıyorsanız lütfen Devam butonuna basınız.
editing_role: '{role} Rolü'
creating_webhook: Webhook Oluştur
default: Öntanımlı

View File

@@ -93,6 +93,7 @@ user_directory: Директорія користувачів
duration: Тривалість
modified_on: Час зміни
status: Статус
remove: Видалити
errors:
ROUTE_NOT_FOUND: Не знайдено
create_bookmark: Створити закладку

View File

@@ -301,6 +301,7 @@ sort_field: Sắp xếp theo trường
add_sort_field: Sắp xếp theo trường
sort: Sắp xếp
status: Trạng thái
remove: Xoá
toggle_manual_sorting: Kích hoạt sắp xếp thủ công
bookmark_doesnt_exist: Bookmark Không Tồn Tại
bookmark_doesnt_exist_copy: Không thể tìm thấy Bookmark bạn đang muốn mở.

View File

@@ -79,7 +79,7 @@ validationError:
unique: 值必须唯一
all_access: 订购全部功能
no_access: 无访问权限
use_custom: 使用自定义颜色
use_custom: 自定义
field_standard: 标准
field_presentation: 展示字段
field_file: 单个文件
@@ -349,6 +349,7 @@ card_size: 卡片大小
sort_field: 排序字段
sort: 排序
status: 状态
remove: 移除
toggle_manual_sorting: 切换手动排序
bookmark_doesnt_exist: 书签不存在
bookmark_doesnt_exist_copy: 找不到您试图打开的书签。

View File

@@ -108,6 +108,8 @@ time: 時間
uuid: UUID
hash: 哈希
create_translations: 創建翻譯
refresh_interval: 重新整理間隔
no_refresh: 不要重新整理
auto_generate: 自動產生
click_here: 點擊此處
fields_group: 欄位群組
@@ -286,6 +288,7 @@ card_size: 卡片大小
sort_field: 排序欄位
sort: 排序
status: 狀態
remove: 移除
toggle_manual_sorting: 啟用手動排序
bookmark_doesnt_exist: 書籤不存在
select_an_item: 選取一個項目...

View File

@@ -9,7 +9,7 @@
<script lang="ts">
import { defineComponent, toRefs } from 'vue';
import { useLayoutState } from '@/composables/use-layout';
import { useLayoutState } from '@directus/shared/composables';
export default defineComponent({
setup() {

View File

@@ -9,7 +9,7 @@ import { useI18n } from 'vue-i18n';
import { defineComponent, onMounted, onUnmounted, toRefs } from 'vue';
import '@fullcalendar/core/vdom';
import { useLayoutState } from '@/composables/use-layout';
import { useLayoutState } from '@directus/shared/composables';
export default defineComponent({
setup() {

View File

@@ -12,7 +12,8 @@ import listPlugin from '@fullcalendar/list';
import interactionPlugin from '@fullcalendar/interaction';
import { ref, watch, toRefs, computed, Ref } from 'vue';
import { useAppStore } from '@/stores/app';
import { Item, Filter, Field } from '@/types';
import { Field } from '@/types';
import { Item, Filter } from '@directus/shared/types';
import useItems from '@/composables/use-items';
import useCollection from '@/composables/use-collection';
import { formatISO } from 'date-fns';
@@ -21,6 +22,7 @@ import { renderPlainStringTemplate } from '@/utils/render-string-template';
import { getFieldsFromTemplate } from '@/utils/get-fields-from-template';
import api from '@/api';
import { unexpectedError } from '@/utils/unexpected-error';
import getFullcalendarLocale from '@/utils/get-fullcalendar-locale';
type LayoutOptions = {
template?: string;
@@ -43,7 +45,7 @@ export default defineLayout<LayoutOptions>({
actions: CalendarActions,
},
setup(props) {
const { t } = useI18n();
const { t, locale } = useI18n();
const calendarEl = ref<HTMLElement>();
const calendar = ref<Calendar>();
@@ -242,6 +244,17 @@ export default defineLayout<LayoutOptions>({
{ deep: true, immediate: true }
);
watch(
[calendar, locale],
async () => {
if (calendar.value) {
const calendarLocale = await getFullcalendarLocale(locale.value);
calendar.value.setOption('locale', calendarLocale);
}
},
{ immediate: true }
);
const showingCount = computed(() => {
if (!itemCount.value) return null;

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