mirror of
https://github.com/directus/directus.git
synced 2026-01-29 22:57:55 -05:00
Merge branch 'main' into aggregation
This commit is contained in:
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -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
61
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
|
||||
4
.github/workflows/e2e-tests.yml
vendored
4
.github/workflows/e2e-tests.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -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,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}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Transformation } from './types/assets';
|
||||
import { Transformation } from './types';
|
||||
|
||||
export const SYSTEM_ASSET_ALLOW_LIST: Transformation[] = [
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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': [
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)[]));
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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':
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const api = axios.create({
|
||||
baseURL: getRootPath(),
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
@@ -36,7 +36,7 @@ export default defineComponent({
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -39,7 +39,7 @@ export default defineComponent({
|
||||
},
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocation>,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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'],
|
||||
|
||||
124
app/src/interfaces/select-multiple-checkbox-tree/index.ts
Normal file
124
app/src/interfaces/select-multiple-checkbox-tree/index.ts
Normal 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'],
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: حقل تاريخ الإنتهاء
|
||||
|
||||
@@ -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: Задействане
|
||||
|
||||
@@ -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é
|
||||
|
||||
@@ -197,6 +197,7 @@ user_directory: Bruger Mappe
|
||||
duration: Varighed
|
||||
modified_on: Ændringsdato
|
||||
status: Status
|
||||
remove: Fjern
|
||||
bookmarks: Foretrukne
|
||||
back: Tilbage
|
||||
placeholder: Placeholder
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -107,6 +107,7 @@ activity: Δραστηριότητα
|
||||
field_width: Πλάτος πεδίου
|
||||
duration: Διάρκεια
|
||||
status: Κατάσταση
|
||||
remove: Διαγραφή
|
||||
bookmarks: Σελιδοδείκτες
|
||||
back: Πίσω
|
||||
operators:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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ó.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -188,6 +188,7 @@ modified_on: 修正日時
|
||||
sort_field: フィールドの並べ替え
|
||||
sort: 並べ替え
|
||||
status: ステータス
|
||||
remove: 削除
|
||||
toggle_manual_sorting: 手動ソートを有効にする
|
||||
bookmark_doesnt_exist: ブックマークは存在しません
|
||||
bookmark_doesnt_exist_copy: 開こうとしているブックマークが見つかりませんでした。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,6 +50,7 @@ activity: Aktiviti
|
||||
user_directory: Direktori pengguna
|
||||
duration: Durasi
|
||||
status: Status
|
||||
remove: Buang
|
||||
bookmarks: Penandabuku
|
||||
back: Kembali
|
||||
operators:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: По умолчанию
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ฟิลด์วันที่สิ้นสุด
|
||||
|
||||
@@ -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ı
|
||||
|
||||
@@ -93,6 +93,7 @@ user_directory: Директорія користувачів
|
||||
duration: Тривалість
|
||||
modified_on: Час зміни
|
||||
status: Статус
|
||||
remove: Видалити
|
||||
errors:
|
||||
ROUTE_NOT_FOUND: Не знайдено
|
||||
create_bookmark: Створити закладку
|
||||
|
||||
@@ -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ở.
|
||||
|
||||
@@ -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: 找不到您试图打开的书签。
|
||||
|
||||
@@ -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: 選取一個項目...
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user