Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-06-07 09:31:44 -04:00
142 changed files with 9129 additions and 5252 deletions

View File

@@ -1,5 +0,0 @@
---
'@directus/drive-gcs': patch
---
Make writableStream non-resumable

View File

@@ -1,10 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "directus/directus" }],
"commit": false,
"linked": [["*"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -1,5 +0,0 @@
---
'@directus/schema': patch
---
Ignore views in schema overview for MS SQL Server

View File

@@ -1,6 +0,0 @@
---
'@directus/app': patch
---
Fixed issue where using an existing junction collection wouldn't allow you to continue in the many to many field setup
flow

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Fixed issue where v-error would overflow beyond its bounds on long error messages

View File

@@ -1,6 +0,0 @@
---
'@directus/app': patch
---
Render inline previews of translated values in translations display, based on two templates (one for language, the other
for translation preview)

View File

@@ -1,5 +0,0 @@
---
'directus': patch
---
Fixed issue that would prevent collections that had a relationship pointing to them from being deleted.

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Prevent non-usable system collections from being selected in the relational field setup flow

View File

@@ -1,5 +0,0 @@
---
'@directus/gatsby-source-directus': patch
---
Fixed issue where using a static token could throw a 401 unauthorized

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Fixed issue where boolean type fields without an interface configured would default to the wrong interface.

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Fixed issue where custom field translations would be lost in session when reordering the fields in settings

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Fixed issue that would prevent multiple, or nested relational fields to be opened within the advanced filter setup

View File

@@ -1,5 +0,0 @@
---
'directus': patch
---
Throw ServiceUnavailableException when file uploads crash on the storage provider, instead of a 500

View File

@@ -1,6 +0,0 @@
---
'@directus/app': patch
---
Fixed issue on the calendar layout that would attempt to save drag-and-drop time changes with a timezone to datetime
type fields

View File

@@ -1,21 +0,0 @@
{
"mode": "pre",
"tag": "rc",
"initialVersions": {
"directus": "9.0.0-rc.69",
"@directus/app": "9.0.0-rc.69",
"@directus/docs": "9.0.0-rc.69",
"@directus/cli": "9.0.0-rc.69",
"create-directus-project": "9.0.0-rc.69",
"@directus/drive": "9.0.0-rc.69",
"@directus/drive-azure": "9.0.0-rc.69",
"@directus/drive-gcs": "9.0.0-rc.69",
"@directus/drive-s3": "9.0.0-rc.69",
"@directus/format-title": "9.0.0-rc.69",
"@directus/gatsby-source-directus": "9.0.0-rc.69",
"@directus/schema": "9.0.0-rc.69",
"@directus/sdk": "9.0.0-rc.69",
"@directus/specs": "9.0.0-rc.69"
},
"changesets": []
}

View File

@@ -1,5 +0,0 @@
---
'@directus/drive-s3': patch
---
Fixed video streaming capabilities of s3 storage provider

View File

@@ -1,5 +0,0 @@
---
'directus': patch
---
Don't use NonNull for update input types

View File

@@ -1,5 +0,0 @@
---
'directus': patch
---
Support filtering the root items based on nested many to any items through the item scope

View File

@@ -1,8 +0,0 @@
---
'@directus/app': patch
---
Set the default value for a newly added boolean filter under advanced filters to true. Prevents confusion around
selected state of the toggle.
https://github.com/directus/directus/issues/5638

View File

@@ -1,6 +0,0 @@
---
'@directus/app': patch
---
Fixed an issue that prevented the collections search from functioning when using a custom collection navigation
structure

View File

@@ -1,5 +0,0 @@
---
'@directus/drive-s3': patch
---
Fixed an issue where an empty ACL setting would cause issues in certain S3 compatible services

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Improve v-select UX by ignoring clicks outside of clickable targets

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Show On Create / On Update for many to one fields relating to users/roles

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Fixed issue that would put manually sorted custom fields in the wrong place on system detail pages (like users/files)

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Fix issue where creating multiple relational default fields on new collection create would fail

View File

@@ -1,5 +0,0 @@
---
'directus': patch
---
Fix an issue where the validation system could short-circuit when using \_or statements.

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Prevent duplicate m2o field name on auto-fill recursive m2m

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Don't allow contains on a UUID type

View File

@@ -1,29 +1,23 @@
#
# Builder
#
FROM node:14-alpine AS builder
# Builder image
FROM alpine:latest AS builder
ARG VERSION
ARG REPOSITORY=directus/directus
RUN \
apk update && \
apk upgrade && \
apk add bash
SHELL ["/bin/bash", "-c"]
# Get runtime dependencies from optional dependencies
# defined in package.json of Directus API package
WORKDIR /directus
RUN apk add --no-cache jq \
&& wget -O directus-api-package.json "https://raw.githubusercontent.com/${REPOSITORY}/${VERSION}/api/package.json" \
&& jq '{ \
name: "directus-project", \
version: "1.0.0", \
description: "Directus Project", \
dependencies: .optionalDependencies \
}' \
directus-api-package.json > package.json
COPY package.json .
RUN for i in {1..60}; do npm install "directus@${VERSION}" && break || sleep 30; done
RUN cat package.json
#
# Image
#
# Directus image
FROM node:14-alpine
ARG VERSION
@@ -32,6 +26,8 @@ ARG REPOSITORY=directus/directus
LABEL directus.version="${VERSION}"
LABEL org.opencontainers.image.source https://github.com/${REPOSITORY}
# Default environment variables
# (see https://docs.directus.io/reference/environment-variables/)
ENV \
PORT="8055" \
PUBLIC_URL="/" \
@@ -58,41 +54,53 @@ ENV \
EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
RUN \
apk update && \
apk upgrade && \
apk add bash ssmtp util-linux
SHELL ["/bin/bash", "-c"]
# Install system dependencies
# - 'bash' for entrypoint script
# - 'ssmtp' to be able to send mails
# - 'util-linux' not sure if this is required
apk upgrade --no-cache && apk add --no-cache \
bash \
ssmtp \
util-linux \
# Install global node dependencies
&& npm install -g \
yargs \
pino \
pino-colada \
# Create directory for Directus with corresponding ownership
# (can be omitted on newer Docker versions since WORKDIR below will do the same)
&& mkdir /directus && chown node:node /directus
# Switch to user 'node' and directory '/directus'
USER node
WORKDIR /directus
# Global requirements
RUN npm install -g yargs pino pino-colada
# Get package.json from builder image
COPY --from=builder --chown=node:node /directus/package.json .
# Install Directus
COPY --from=builder /directus/package.json .
RUN npm install
# Copy files
COPY ./rootfs /
# Keep the updated package.json
COPY --from=builder /directus/package.json .
RUN chmod +x /usr/bin/entrypoint && chmod +x /usr/bin/print
# Create directories
RUN \
mkdir -p extensions/displays && \
mkdir -p extensions/interfaces && \
mkdir -p extensions/layouts && \
mkdir -p extensions/modules && \
mkdir -p database && \
mkdir -p uploads
# Install Directus and runtime dependencies
# (retry if it fails for some reason, e.g. release not published yet)
for i in $(seq 10); do npm install "directus@${VERSION}" && break || sleep 30; done && \
npm install \
# Create data directories
&& mkdir -p \
database \
extensions/displays \
extensions/interfaces \
extensions/layouts \
extensions/modules \
uploads
EXPOSE 8055
# Expose data directories as volumes
VOLUME \
/directus/database \
/directus/extensions \
/directus/uploads
# Copy rootfs files
COPY ./rootfs /
EXPOSE 8055
SHELL ["/bin/bash", "-c"]
ENTRYPOINT ["entrypoint"]

View File

@@ -1,17 +0,0 @@
{
"name": "directus-project",
"version": "1.0.0",
"description": "Directus Project",
"dependencies": {
"@keyv/redis": "^2.1.2",
"ioredis": "^4.19.2",
"keyv-memcache": "^1.0.1",
"memcached": "^2.2.2",
"tedious": "^11.0.5",
"mysql": "^2.18.1",
"oracledb": "^5.0.0",
"pg": "^8.4.2",
"sqlite3": "^5.0.2",
"yargs": "^16.0.3"
}
}

View File

@@ -3,9 +3,6 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
tests:
runs-on: ubuntu-latest

View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-rc.69",
"version": "9.0.0-rc.73",
"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.",
@@ -66,21 +66,20 @@
"example.env"
],
"dependencies": {
"@directus/app": "9.0.0-rc.69",
"@directus/drive": "9.0.0-rc.69",
"@directus/drive-azure": "9.0.0-rc.69",
"@directus/drive-gcs": "9.0.0-rc.69",
"@directus/drive-s3": "9.0.0-rc.69",
"@directus/format-title": "9.0.0-rc.69",
"@directus/schema": "9.0.0-rc.69",
"@directus/specs": "9.0.0-rc.69",
"@godaddy/terminus": "^4.7.2",
"argon2": "^0.27.0",
"@directus/app": "9.0.0-rc.73",
"@directus/drive": "9.0.0-rc.73",
"@directus/drive-azure": "9.0.0-rc.73",
"@directus/drive-gcs": "9.0.0-rc.73",
"@directus/drive-s3": "9.0.0-rc.73",
"@directus/format-title": "9.0.0-rc.73",
"@directus/schema": "9.0.0-rc.73",
"@directus/specs": "9.0.0-rc.73",
"@godaddy/terminus": "^4.9.0",
"argon2": "^0.28.1",
"async": "^3.2.0",
"async-mutex": "^0.3.1",
"atob": "^2.1.2",
"axios": "^0.21.0",
"body-parser": "^1.19.0",
"busboy": "^0.3.1",
"camelcase": "^6.2.0",
"chalk": "^4.1.1",
@@ -98,10 +97,10 @@
"express": "^4.17.1",
"express-pino-logger": "^6.0.0",
"express-session": "^1.17.2",
"fs-extra": "^9.1.0",
"fs-extra": "^10.0.0",
"grant": "^5.4.14",
"graphql": "^15.5.0",
"graphql-compose": "^8.1.0",
"graphql-compose": "^9.0.1",
"icc": "^2.0.0",
"inquirer": "^8.1.0",
"joi": "^17.3.0",
@@ -111,11 +110,11 @@
"jsonwebtoken": "^8.5.1",
"keyv": "^4.0.3",
"knex": "^0.95.6",
"knex-schema-inspector": "^1.5.4",
"knex-schema-inspector": "^1.5.6",
"liquidjs": "^9.25.0",
"lodash": "^4.17.21",
"macos-release": "^2.4.1",
"mime-types": "^2.1.27",
"mime-types": "^2.1.31",
"ms": "^2.1.3",
"nanoid": "^3.1.23",
"node-machine-id": "^1.1.12",
@@ -157,7 +156,7 @@
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.10",
"@types/destroy": "^1.0.0",
"@types/express": "^4.17.11",
"@types/express": "^4.17.12",
"@types/express-pino-logger": "^4.0.2",
"@types/express-session": "^1.17.3",
"@types/fs-extra": "^9.0.11",
@@ -169,7 +168,7 @@
"@types/lodash": "^4.14.170",
"@types/mime-types": "^2.1.0",
"@types/ms": "^0.7.31",
"@types/node": "^15.6.0",
"@types/node": "^15.12.0",
"@types/nodemailer": "^6.4.1",
"@types/qs": "^6.9.6",
"@types/sharp": "^0.28.1",
@@ -179,6 +178,6 @@
"copyfiles": "^2.4.0",
"cross-env": "^7.0.2",
"ts-node-dev": "^1.0.0",
"typescript": "^4.0.5"
"typescript": "^4.3.2"
}
}

View File

@@ -1,6 +1,5 @@
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import express from 'express';
import express, { RequestHandler } from 'express';
import expressLogger from 'express-pino-logger';
import fse from 'fs-extra';
import path from 'path';
@@ -72,12 +71,14 @@ export default async function createApp(): Promise<express.Application> {
await emitAsyncSafe('middlewares.init.before', { app });
app.use(expressLogger({ logger }));
app.use(expressLogger({ logger }) as RequestHandler);
app.use((req, res, next) => {
bodyParser.json({
limit: env.MAX_PAYLOAD_SIZE,
})(req, res, (err) => {
(
express.json({
limit: env.MAX_PAYLOAD_SIZE,
}) as RequestHandler
)(req, res, (err: any) => {
if (err) {
return next(new InvalidPayloadException(err.message));
}

View File

@@ -4,6 +4,8 @@ import installDatabase from '../../../database/seeds/run';
import env from '../../../env';
import logger from '../../../logger';
import { getSchema } from '../../../utils/get-schema';
import { RolesService, UsersService, SettingsService } from '../../../services';
import getDatabase, { isInstalled, hasDatabaseConnection } from '../../../database';
export default async function bootstrap(): Promise<void> {
logger.info('Initializing bootstrap...');
@@ -13,10 +15,7 @@ export default async function bootstrap(): Promise<void> {
process.exit(1);
}
const { isInstalled, default: database } = require('../../../database');
const { RolesService } = require('../../../services/roles');
const { UsersService } = require('../../../services/users');
const { SettingsService } = require('../../../services/settings');
const database = getDatabase();
if ((await isInstalled()) === false) {
logger.info('Installing Directus system tables...');
@@ -66,8 +65,6 @@ export default async function bootstrap(): Promise<void> {
}
async function isDatabaseAvailable() {
const { hasDatabaseConnection } = require('../../../database');
const tries = 5;
const secondsBetweenTries = 5;

View File

@@ -1,5 +1,7 @@
import getDatabase from '../../../database';
export default async function count(collection: string): Promise<void> {
const database = require('../../../database/index').default;
const database = getDatabase();
if (!collection) {
console.error('Collection is required');

View File

@@ -1,9 +1,9 @@
import { Knex } from 'knex';
import runMigrations from '../../../database/migrations/run';
import installSeeds from '../../../database/seeds/run';
import getDatabase from '../../../database';
export default async function start(): Promise<void> {
const database = require('../../../database/index').default as Knex;
const database = getDatabase();
try {
await installSeeds(database);

View File

@@ -1,7 +1,8 @@
import run from '../../../database/migrations/run';
import getDatabase from '../../../database';
export default async function migrate(direction: 'latest' | 'up' | 'down'): Promise<void> {
const database = require('../../../database').default;
const database = getDatabase();
try {
console.log('✨ Running migrations...');

View File

@@ -1,8 +1,9 @@
import { getSchema } from '../../../utils/get-schema';
import { RolesService } from '../../../services';
import getDatabase from '../../../database';
export default async function rolesCreate({ role: name, admin }: { role: string; admin: boolean }): Promise<void> {
const { default: database } = require('../../../database/index');
const { RolesService } = require('../../../services/roles');
const database = getDatabase();
if (!name) {
console.error('Name is required');

View File

@@ -1,4 +1,6 @@
import { getSchema } from '../../../utils/get-schema';
import { UsersService } from '../../../services';
import getDatabase from '../../../database';
export default async function usersCreate({
email,
@@ -9,8 +11,7 @@ export default async function usersCreate({
password?: string;
role?: string;
}): Promise<void> {
const { default: database } = require('../../../database/index');
const { UsersService } = require('../../../services/users');
const database = getDatabase();
if (!email || !password || !role) {
console.error('Email, password, role are required');

View File

@@ -1,9 +1,10 @@
import argon2 from 'argon2';
import { getSchema } from '../../../utils/get-schema';
import { UsersService } from '../../../services';
import getDatabase from '../../../database';
export default async function usersPasswd({ email, password }: { email?: string; password?: string }): Promise<void> {
const { default: database } = require('../../../database/index');
const { UsersService } = require('../../../services/users');
const database = getDatabase();
if (!email || !password) {
console.error('Email and password are required');

View File

@@ -4,7 +4,7 @@ import { pick } from 'lodash';
import ms from 'ms';
import validate from 'uuid-validate';
import { ASSET_TRANSFORM_QUERY_KEYS, SYSTEM_ASSET_ALLOW_LIST } from '../constants';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import { ForbiddenException, InvalidQueryException, RangeNotSatisfiableException } from '../exceptions';
import useCollection from '../middleware/use-collection';
@@ -32,11 +32,11 @@ router.get(
* This is a little annoying. Postgres will error out if you're trying to search in `where`
* with a wrong type. In case of directus_files where id is a uuid, we'll have to verify the
* validity of the uuid ahead of time.
* @todo move this to a validation middleware function
*/
const isValidUUID = validate(id, 4);
if (isValidUUID === false) throw new ForbiddenException();
const database = getDatabase();
const file = await database.select('id', 'storage', 'filename_disk').from('directus_files').where({ id }).first();
if (!file) throw new ForbiddenException();
@@ -51,6 +51,7 @@ router.get(
const payloadService = new PayloadService('directus_settings', { schema: req.schema });
const defaults = { storage_asset_presets: [], storage_asset_transform: 'all' };
const database = getDatabase();
const savedAssetSettings = await database
.select('storage_asset_presets', 'storage_asset_transform')
.from('directus_settings')

View File

@@ -67,7 +67,7 @@ const newFieldSchema = Joi.object({
type: Joi.string()
.valid(...types, ...ALIAS_TYPES)
.allow(null)
.required(),
.optional(),
schema: Joi.object({
default_value: Joi.any(),
max_length: [Joi.number(), Joi.string(), Joi.valid(null)],

View File

@@ -1,60 +1,98 @@
import SchemaInspector from '@directus/schema';
import dotenv from 'dotenv';
import { knex, Knex } from 'knex';
import path from 'path';
import { performance } from 'perf_hooks';
import env from '../env';
import logger from '../logger';
import { getConfigFromEnv } from '../utils/get-config-from-env';
import { validateEnv } from '../utils/validate-env';
dotenv.config({ path: path.resolve(__dirname, '../../', '.env') });
let database: Knex | null = null;
let inspector: ReturnType<typeof SchemaInspector> | null = null;
const connectionConfig: Record<string, any> = getConfigFromEnv('DB_', [
'DB_CLIENT',
'DB_SEARCH_PATH',
'DB_CONNECTION_STRING',
'DB_POOL',
]);
export default function getDatabase(): Knex {
if (database) {
return database;
}
const poolConfig = getConfigFromEnv('DB_POOL');
const connectionConfig: Record<string, any> = getConfigFromEnv('DB_', [
'DB_CLIENT',
'DB_SEARCH_PATH',
'DB_CONNECTION_STRING',
'DB_POOL',
]);
validateEnv(['DB_CLIENT']);
const poolConfig = getConfigFromEnv('DB_POOL');
const knexConfig: Knex.Config = {
client: env.DB_CLIENT,
searchPath: env.DB_SEARCH_PATH,
connection: env.DB_CONNECTION_STRING || connectionConfig,
log: {
warn: (msg) => logger.warn(msg),
error: (msg) => logger.error(msg),
deprecate: (msg) => logger.info(msg),
debug: (msg) => logger.debug(msg),
},
pool: poolConfig,
};
const requiredEnvVars = ['DB_CLIENT'];
if (env.DB_CLIENT === 'sqlite3') {
knexConfig.useNullAsDefault = true;
poolConfig.afterCreate = (conn: any, cb: any) => {
conn.run('PRAGMA foreign_keys = ON', cb);
if (env.DB_CLIENT && env.DB_CLIENT === 'sqlite3') {
requiredEnvVars.push('DB_FILENAME');
} else if (env.DB_CLIENT && env.DB_CLIENT === 'oracledb') {
requiredEnvVars.push('DB_USER', 'DB_PASSWORD', 'DB_CONNECT_STRING');
} else {
if (env.DB_CLIENT === 'pg') {
if (!env.DB_CONNECTION_STRING) {
requiredEnvVars.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER');
}
} else {
requiredEnvVars.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
}
}
validateEnv(requiredEnvVars);
const knexConfig: Knex.Config = {
client: env.DB_CLIENT,
searchPath: env.DB_SEARCH_PATH,
connection: env.DB_CONNECTION_STRING || connectionConfig,
log: {
warn: (msg) => logger.warn(msg),
error: (msg) => logger.error(msg),
deprecate: (msg) => logger.info(msg),
debug: (msg) => logger.debug(msg),
},
pool: poolConfig,
};
if (env.DB_CLIENT === 'sqlite3') {
knexConfig.useNullAsDefault = true;
poolConfig.afterCreate = (conn: any, cb: any) => {
conn.run('PRAGMA foreign_keys = ON', cb);
};
}
database = knex(knexConfig);
const times: Record<string, number> = {};
database
.on('query', (queryInfo) => {
times[queryInfo.__knexUid] = performance.now();
})
.on('query-response', (response, queryInfo) => {
const delta = performance.now() - times[queryInfo.__knexUid];
logger.trace(`[${delta.toFixed(3)}ms] ${queryInfo.sql} [${queryInfo.bindings.join(', ')}]`);
delete times[queryInfo.__knexUid];
});
return database;
}
const database = knex(knexConfig);
export function getSchemaInspector(): ReturnType<typeof SchemaInspector> {
if (inspector) {
return inspector;
}
const times: Record<string, number> = {};
const database = getDatabase();
database
.on('query', (queryInfo) => {
times[queryInfo.__knexUid] = performance.now();
})
.on('query-response', (response, queryInfo) => {
const delta = performance.now() - times[queryInfo.__knexUid];
logger.trace(`[${delta.toFixed(3)}ms] ${queryInfo.sql} [${queryInfo.bindings.join(', ')}]`);
});
inspector = SchemaInspector(database);
return inspector;
}
export async function hasDatabaseConnection(): Promise<boolean> {
const database = getDatabase();
try {
if (env.DB_CLIENT === 'oracledb') {
await database.raw('select 1 from DUAL');
@@ -77,13 +115,11 @@ export async function validateDBConnection(): Promise<void> {
}
}
export const schemaInspector = SchemaInspector(database);
export async function isInstalled(): Promise<boolean> {
const inspector = getSchemaInspector();
// The existence of a directus_collections table alone isn't a "proper" check to see if everything
// is installed correctly of course, but it's safe enough to assume that this collection only
// exists when using the installer CLI.
return await schemaInspector.hasTable('directus_collections');
return await inspector.hasTable('directus_collections');
}
export default database;

View File

@@ -1,14 +1,15 @@
import { Knex } from 'knex';
import SchemaInspector from 'knex-schema-inspector';
import { schemaInspector } from '..';
import logger from '../../logger';
import { RelationMeta } from '../../types';
import { getDefaultIndexName } from '../../utils/get-default-index-name';
export async function up(knex: Knex): Promise<void> {
const inspector = SchemaInspector(knex);
const foreignKeys = await inspector.foreignKeys();
const relations = await knex
.select<RelationMeta[]>('many_collection', 'many_field', 'one_collection')
.select<RelationMeta[]>('id', 'many_collection', 'many_field', 'one_collection')
.from('directus_relations');
const constraintsToAdd = relations.filter((relation) => {
@@ -18,45 +19,82 @@ export async function up(knex: Knex): Promise<void> {
return exists === false;
});
await knex.transaction(async (trx) => {
for (const constraint of constraintsToAdd) {
if (!constraint.one_collection) continue;
const corruptedRelations: number[] = [];
const currentPrimaryKeyField = await schemaInspector.primary(constraint.many_collection);
const relatedPrimaryKeyField = await schemaInspector.primary(constraint.one_collection);
if (!currentPrimaryKeyField || !relatedPrimaryKeyField) continue;
for (const constraint of constraintsToAdd) {
if (!constraint.one_collection) continue;
const rowsWithIllegalFKValues = await trx
.select(`${constraint.many_collection}.${currentPrimaryKeyField}`)
.from(constraint.many_collection)
.leftJoin(
constraint.one_collection,
`${constraint.many_collection}.${constraint.many_field}`,
`${constraint.one_collection}.${relatedPrimaryKeyField}`
)
.whereNull(`${constraint.one_collection}.${relatedPrimaryKeyField}`);
if (
(await inspector.hasTable(constraint.many_collection)) === false ||
(await inspector.hasTable(constraint.one_collection)) === false
) {
logger.warn(
`Ignoring ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection}. Tables don't exist.`
);
if (rowsWithIllegalFKValues.length > 0) {
const ids: (string | number)[] = rowsWithIllegalFKValues.map<string | number>(
(row) => row[currentPrimaryKeyField]
);
corruptedRelations.push(constraint.id);
continue;
}
await trx(constraint.many_collection)
const currentPrimaryKeyField = await inspector.primary(constraint.many_collection);
const relatedPrimaryKeyField = await inspector.primary(constraint.one_collection);
if (constraint.many_field === currentPrimaryKeyField) {
logger.warn(
`Illegal relationship ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection} encountered. Many field equals collections primary key.`
);
corruptedRelations.push(constraint.id);
continue;
}
if (!currentPrimaryKeyField || !relatedPrimaryKeyField) continue;
const rowsWithIllegalFKValues = await knex
.select(`main.${currentPrimaryKeyField}`)
.from({ main: constraint.many_collection })
.leftJoin(
{ related: constraint.one_collection },
`main.${constraint.many_field}`,
`related.${relatedPrimaryKeyField}`
)
.whereNull(`related.${relatedPrimaryKeyField}`);
if (rowsWithIllegalFKValues.length > 0) {
const ids: (string | number)[] = rowsWithIllegalFKValues.map<string | number>(
(row) => row[currentPrimaryKeyField]
);
try {
await knex(constraint.many_collection)
.update({ [constraint.many_field]: null })
.whereIn(currentPrimaryKeyField, ids);
} catch (err) {
logger.error(
`${constraint.many_collection}.${constraint.many_field} contains illegal foreign keys which couldn't be set to NULL. Please fix these references and rerun this migration to complete the upgrade.`
);
if (ids.length < 25) {
logger.error(`Items with illegal foreign keys: ${ids.join(', ')}`);
} else {
logger.error(`Items with illegal foreign keys: ${ids.slice(0, 25).join(', ')} and ${ids.length} others`);
}
throw 'Migration aborted';
}
}
// Can't reliably have circular cascade
const action = constraint.many_collection === constraint.one_collection ? 'NO ACTION' : 'SET NULL';
// Can't reliably have circular cascade
const action = constraint.many_collection === constraint.one_collection ? 'NO ACTION' : 'SET NULL';
// MySQL doesn't accept FKs from `int` to `int unsigned`. `knex` defaults `.increments()`
// to `unsigned`, but defaults `.integer()` to `int`. This means that created m2o fields
// have the wrong type. This step will force the m2o `int` field into `unsigned`, but only
// if both types are integers, and only if we go from `int` to `int unsigned`.
const columnInfo = await schemaInspector.columnInfo(constraint.many_collection, constraint.many_field);
const relatedColumnInfo = await schemaInspector.columnInfo(constraint.one_collection!, relatedPrimaryKeyField);
// MySQL doesn't accept FKs from `int` to `int unsigned`. `knex` defaults `.increments()`
// to `unsigned`, but defaults `.integer()` to `int`. This means that created m2o fields
// have the wrong type. This step will force the m2o `int` field into `unsigned`, but only
// if both types are integers, and only if we go from `int` to `int unsigned`.
const columnInfo = await inspector.columnInfo(constraint.many_collection, constraint.many_field);
const relatedColumnInfo = await inspector.columnInfo(constraint.one_collection!, relatedPrimaryKeyField);
await trx.schema.alterTable(constraint.many_collection, (table) => {
try {
await knex.schema.alterTable(constraint.many_collection, (table) => {
if (
columnInfo.data_type !== relatedColumnInfo.data_type &&
columnInfo.data_type === 'int' &&
@@ -65,21 +103,48 @@ export async function up(knex: Knex): Promise<void> {
table.specificType(constraint.many_field, 'int unsigned').alter();
}
const indexName = getDefaultIndexName('foreign', constraint.many_collection, constraint.many_field);
table
.foreign(constraint.many_field)
.foreign(constraint.many_field, indexName)
.references(relatedPrimaryKeyField)
.inTable(constraint.one_collection!)
.onDelete(action);
});
} catch (err) {
logger.warn(
`Couldn't add foreign key constraint for ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection}`
);
logger.warn(err);
}
});
}
if (corruptedRelations.length > 0) {
logger.warn(
`Encountered one or more corrupted relationships. Please check the following rows in "directus_relations": ${corruptedRelations.join(
', '
)}`
);
}
}
export async function down(knex: Knex): Promise<void> {
const relations = await knex.select<RelationMeta[]>('many_collection', 'many_field').from('directus_relations');
const relations = await knex
.select<RelationMeta[]>('many_collection', 'many_field', 'one_collection')
.from('directus_relations');
for (const relation of relations) {
await knex.schema.alterTable(relation.many_collection, (table) => {
table.dropForeign([relation.many_field]);
});
if (!relation.one_collection) continue;
try {
await knex.schema.alterTable(relation.many_collection, (table) => {
table.dropForeign([relation.many_field]);
});
} catch (err) {
logger.warn(
`Couldn't drop foreign key constraint for ${relation.many_collection}.${relation.many_field}<->${relation.one_collection}`
);
logger.warn(err);
}
}
}

View File

@@ -1,4 +1,5 @@
import { Knex } from 'knex';
import logger from '../../logger';
/**
* Things to keep in mind:
@@ -80,22 +81,84 @@ const updates = [
export async function up(knex: Knex): Promise<void> {
for (const update of updates) {
await knex.schema.alterTable(update.table, (table) => {
for (const constraint of update.constraints) {
table.dropForeign([constraint.column]);
table.foreign(constraint.column).references(constraint.references).onDelete(constraint.on_delete);
for (const constraint of update.constraints) {
try {
await knex.schema.alterTable(update.table, (table) => {
table.dropForeign([constraint.column]);
});
} catch (err) {
logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}
});
/**
* MySQL won't delete the index when you drop the foreign key constraint. Gotta make
* sure to clean those up as well
*/
if (knex.client.constructor.name === 'Client_MySQL') {
try {
await knex.schema.alterTable(update.table, (table) => {
// Knex uses a default convention for index names: `table_column_type`
table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`);
});
} catch (err) {
logger.warn(
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
);
logger.warn(err);
}
}
try {
await knex.schema.alterTable(update.table, (table) => {
table.foreign(constraint.column).references(constraint.references).onDelete(constraint.on_delete);
});
} catch (err) {
logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}
}
}
}
export async function down(knex: Knex): Promise<void> {
for (const update of updates) {
await knex.schema.alterTable(update.table, (table) => {
for (const constraint of update.constraints) {
table.dropForeign([constraint.column]);
table.foreign(constraint.column).references(constraint.references);
for (const constraint of update.constraints) {
try {
await knex.schema.alterTable(update.table, (table) => {
table.dropForeign([constraint.column]);
});
} catch (err) {
logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}
});
/**
* MySQL won't delete the index when you drop the foreign key constraint. Gotta make
* sure to clean those up as well
*/
if (knex.client.constructor.name === 'Client_MySQL') {
try {
await knex.schema.alterTable(update.table, (table) => {
// Knex uses a default convention for index names: `table_column_type`
table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`);
});
} catch (err) {
logger.warn(
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
);
logger.warn(err);
}
}
try {
await knex.schema.alterTable(update.table, (table) => {
table.foreign(constraint.column).references(constraint.references);
});
} catch (err) {
logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}
}
}
}

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_collections', (table) => {
table.string('color').nullable();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_collections', (table) => {
table.dropColumn('color');
});
}

View File

@@ -5,7 +5,7 @@ import { Item, Query, SchemaOverview } from '../types';
import { AST, FieldNode, NestedCollectionNode } from '../types/ast';
import applyQuery from '../utils/apply-query';
import { toArray } from '../utils/to-array';
import database from './index';
import getDatabase from './index';
type RunASTOptions = {
/**
@@ -39,7 +39,7 @@ export default async function runAST(
): Promise<null | Item | Item[]> {
const ast = cloneDeep(originalAST);
const knex = options?.knex || database;
const knex = options?.knex || getDatabase();
if (ast.type === 'm2a') {
const results: { [collection: string]: null | Item | Item[] } = {};
@@ -295,7 +295,7 @@ function mergeWithParentItems(
});
// We re-apply the requested limit here. This forces the _n_ nested items per parent concept
if (nested) {
if (nested && nestedNode.query.limit !== -1) {
itemChildren = itemChildren.slice(0, nestedNode.query.limit ?? 100);
}

View File

@@ -18,16 +18,22 @@ fields:
readonly: true
width: half
- field: note
interface: input
options:
placeholder: A description of this collection...
width: half
- field: icon
interface: select-icon
options:
width: half
- field: note
interface: input
- field: color
interface: select-color
options:
placeholder: A description of this collection...
width: full
placeholder: Choose a color...
width: half
- field: display_template
interface: system-display-template

View File

@@ -71,7 +71,7 @@ const defaults: Record<string, any> = {
// Allows us to force certain environment variable into a type, instead of relying
// on the auto-parsed type in processValues. ref #3705
const typeMap: Record<string, string> = {
PORT: 'number',
PORT: 'string',
DB_NAME: 'string',
DB_USER: 'string',
@@ -92,6 +92,22 @@ env = processValues(env);
export default env;
/**
* When changes have been made during runtime, like in the CLI, we can refresh the env object with
* the newly created variables
*/
export function refreshEnv(): void {
env = {
...defaults,
...getEnv(),
...process.env,
};
process.env = env;
env = processValues(env);
}
function getEnv() {
const configPath = path.resolve(process.env.CONFIG_PATH || defaults.CONFIG_PATH);

View File

@@ -1,4 +1,4 @@
import database from '../../../database';
import getDatabase from '../../../database';
import { ContainsNullValuesException } from '../contains-null-values';
import { InvalidForeignKeyException } from '../invalid-foreign-key';
import { NotNullViolationException } from '../not-null-violation';
@@ -56,6 +56,8 @@ async function uniqueViolation(error: MSSQLError) {
const keyName = quoteMatches[1];
const database = getDatabase();
const constraintUsage = await database
.select('*')
.from('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE')

View File

@@ -43,28 +43,51 @@ function uniqueViolation(error: MySQLError) {
if (!matches) return error;
const collection = matches[1].slice(1, -1).split('.')[0];
let field = null;
/**
* MySQL's error doesn't return the field name in the error. In case the field is created through
* Directus (/ Knex), the key name will be `<collection>_<field>_unique` in which case we can pull
* the field name from the key name
*/
const indexName = matches[1].slice(1, -1).split('.')[1];
if (indexName?.startsWith(`${collection}_`) && indexName.endsWith('_unique')) {
field = indexName.slice(collection.length + 1, -7);
/** MySQL 8+ style error message */
if (matches[1].includes('.')) {
const collection = matches[1].slice(1, -1).split('.')[0];
let field = null;
const indexName = matches[1].slice(1, -1).split('.')[1];
if (indexName?.startsWith(`${collection}_`) && indexName.endsWith('_unique')) {
field = indexName.slice(collection.length + 1, -7);
}
const invalid = matches[0].slice(1, -1);
return new RecordNotUniqueException(field, {
collection,
field,
invalid,
});
} else {
/** MySQL 5.7 style error message */
const indexName = matches[1].slice(1, -1);
const collection = indexName.split('_')[0];
let field = null;
if (indexName?.startsWith(`${collection}_`) && indexName.endsWith('_unique')) {
field = indexName.slice(collection.length + 1, -7);
}
const invalid = matches[0].slice(1, -1);
return new RecordNotUniqueException(field, {
collection,
field,
invalid,
});
}
const invalid = matches[0].slice(1, -1);
return new RecordNotUniqueException(field, {
collection,
field,
invalid,
});
}
function numericValueOutOfRange(error: MySQLError) {

View File

@@ -1,4 +1,4 @@
import database from '../../database';
import getDatabase from '../../database';
import { extractError as mssql } from './dialects/mssql';
import { extractError as mysql } from './dialects/mysql';
import { extractError as oracle } from './dialects/oracle';
@@ -16,6 +16,8 @@ import { SQLError } from './dialects/types';
* - Value Too Long
*/
export async function translateDatabaseError(error: SQLError): Promise<any> {
const database = getDatabase();
switch (database.client.constructor.name) {
case 'Client_MySQL':
return mysql(error);

View File

@@ -1,7 +1,7 @@
import express, { Router } from 'express';
import { ensureDir } from 'fs-extra';
import path from 'path';
import database from './database';
import getDatabase from './database';
import emitter from './emitter';
import env from './env';
import * as exceptions from './exceptions';
@@ -93,7 +93,7 @@ function registerHooks(hooks: string[]) {
}
}
const events = register({ services, exceptions, env, database, getSchema });
const events = register({ services, exceptions, env, database: getDatabase(), getSchema });
for (const [event, handler] of Object.entries(events)) {
emitter.on(event, handler);
}
@@ -126,6 +126,6 @@ function registerEndpoints(endpoints: string[], router: Router) {
const scopedRouter = express.Router();
router.use(`/${endpoint}/`, scopedRouter);
register(scopedRouter, { services, exceptions, env, database, getSchema });
register(scopedRouter, { services, exceptions, env, database: getDatabase(), getSchema });
}
}

View File

@@ -1,6 +1,6 @@
import { RequestHandler } from 'express';
import jwt, { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import { InvalidCredentialsException } from '../exceptions';
import asyncHandler from '../utils/async-handler';
@@ -21,6 +21,8 @@ const authenticate: RequestHandler = asyncHandler(async (req, res, next) => {
if (!req.token) return next();
const database = getDatabase();
if (isJWT(req.token)) {
let payload: { id: string };

View File

@@ -1,9 +1,11 @@
import { RequestHandler } from 'express';
import database from '../database';
import getDatabase from '../database';
import { InvalidIPException } from '../exceptions';
import asyncHandler from '../utils/async-handler';
export const checkIP: RequestHandler = asyncHandler(async (req, res, next) => {
const database = getDatabase();
const role = await database
.select('ip_access')
.from('directus_roles')

View File

@@ -1,7 +1,7 @@
import expressSession, { Store } from 'express-session';
import env from '../env';
import { getConfigFromEnv } from '../utils/get-config-from-env';
import database from '../database';
import getDatabase from '../database';
let store: Store | undefined = undefined;
if (env.SESSION_STORE === 'redis') {
@@ -20,7 +20,7 @@ if (env.SESSION_STORE === 'memcache') {
if (env.SESSION_STORE === 'database') {
const KnexSessionStore = require('connect-session-knex')(expressSession);
store = new KnexSessionStore({
knex: database,
knex: getDatabase(),
tablename: 'oauth_sessions', // optional. Defaults to 'sessions'
});
}

View File

@@ -6,7 +6,7 @@ import { once } from 'lodash';
import qs from 'qs';
import url from 'url';
import createApp from './app';
import database from './database';
import getDatabase from './database';
import { emitAsyncSafe } from './emitter';
import logger from './logger';
@@ -94,6 +94,7 @@ export default async function createServer(): Promise<http.Server> {
}
async function onSignal() {
const database = getDatabase();
await database.destroy();
logger.info('Database connections destroyed');
}

View File

@@ -2,7 +2,7 @@ import { Range, StatResponse } from '@directus/drive';
import { Knex } from 'knex';
import path from 'path';
import sharp, { ResizeOptions } from 'sharp';
import database from '../database';
import getDatabase from '../database';
import { RangeNotSatisfiableException, IllegalAssetTransformation } from '../exceptions';
import storage from '../storage';
import { AbstractServiceOptions, Accountability, Transformation } from '../types';
@@ -23,7 +23,7 @@ export class AssetsService {
authorizationService: AuthorizationService;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.authorizationService = new AuthorizationService(options);
}
@@ -44,7 +44,7 @@ export class AssetsService {
await this.authorizationService.checkAccess('read', 'directus_files', id);
}
const file = (await database.select('*').from('directus_files').where({ id }).first()) as File;
const file = (await this.knex.select('*').from('directus_files').where({ id }).first()) as File;
if (range) {
if (range.start >= file.filesize || (range.end && range.end >= file.filesize)) {

View File

@@ -4,7 +4,7 @@ import { Knex } from 'knex';
import ms from 'ms';
import { nanoid } from 'nanoid';
import { authenticator } from 'otplib';
import database from '../database';
import getDatabase from '../database';
import emitter, { emitAsyncSafe } from '../emitter';
import env from '../env';
import {
@@ -37,7 +37,7 @@ export class AuthenticationService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.activityService = new ActivityService({ knex: this.knex, schema: options.schema });
this.schema = options.schema;
@@ -59,7 +59,7 @@ export class AuthenticationService {
const { email, password, ip, userAgent, otp } = options;
let user = await database
let user = await this.knex
.select('id', 'password', 'role', 'tfa_secret', 'status')
.from('directus_users')
.whereRaw('LOWER(??) = ?', ['email', email.toLowerCase()])
@@ -114,7 +114,7 @@ export class AuthenticationService {
try {
await loginAttemptsLimiter.consume(user.id);
} catch (err) {
await database('directus_users').update({ status: 'suspended' }).where({ id: user.id });
await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id });
user.status = 'suspended';
// This means that new attempts after the user has been re-activated will be accepted
@@ -164,7 +164,7 @@ export class AuthenticationService {
const refreshToken = nanoid(64);
const refreshTokenExpiration = new Date(Date.now() + ms(env.REFRESH_TOKEN_TTL as string));
await database('directus_sessions').insert({
await this.knex('directus_sessions').insert({
token: refreshToken,
user: user.id,
expires: refreshTokenExpiration,
@@ -172,7 +172,7 @@ export class AuthenticationService {
user_agent: userAgent,
});
await database('directus_sessions').delete().where('expires', '<', new Date());
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
if (this.accountability) {
await this.activityService.createOne({
@@ -204,7 +204,7 @@ export class AuthenticationService {
throw new InvalidCredentialsException();
}
const record = await database
const record = await this.knex
.select<Session & { email: string; id: string }>(
'directus_sessions.*',
'directus_users.email',

View File

@@ -1,6 +1,6 @@
import { Knex } from 'knex';
import { cloneDeep, flatten, merge, uniq, uniqWith } from 'lodash';
import database from '../database';
import getDatabase from '../database';
import { FailedValidationException, ForbiddenException } from '../exceptions';
import {
AbstractServiceOptions,
@@ -28,7 +28,7 @@ export class AuthorizationService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
this.payloadService = new PayloadService('directus_permissions', {

View File

@@ -2,7 +2,7 @@ import SchemaInspector from '@directus/schema';
import { Knex } from 'knex';
import cache from '../cache';
import { ALIAS_TYPES } from '../constants';
import database, { schemaInspector } from '../database';
import getDatabase, { getSchemaInspector } from '../database';
import { systemCollectionRows } from '../database/system-data/collections';
import env from '../env';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
@@ -27,13 +27,13 @@ export type RawCollection = {
export class CollectionsService {
knex: Knex;
accountability: Accountability | null;
schemaInspector: typeof schemaInspector;
schemaInspector: ReturnType<typeof SchemaInspector>;
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : schemaInspector;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
this.schema = options.schema;
}

View File

@@ -3,7 +3,7 @@ import { Knex } from 'knex';
import { Column } from 'knex-schema-inspector/dist/types/column';
import cache from '../cache';
import { ALIAS_TYPES } from '../constants';
import database, { schemaInspector } from '../database';
import getDatabase, { getSchemaInspector } from '../database';
import { systemFieldRows } from '../database/system-data/fields/';
import emitter, { emitAsyncSafe } from '../emitter';
import env from '../env';
@@ -26,12 +26,12 @@ export class FieldsService {
accountability: Accountability | null;
itemsService: ItemsService;
payloadService: PayloadService;
schemaInspector: typeof schemaInspector;
schemaInspector: ReturnType<typeof SchemaInspector>;
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : schemaInspector;
this.knex = options.knex || getDatabase();
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
this.accountability = options.accountability || null;
this.itemsService = new ItemsService('directus_fields', options);
this.payloadService = new PayloadService('directus_fields', options);

View File

@@ -102,8 +102,9 @@ export class FilesService extends ItemsService {
if (meta.iptc) {
try {
payload.metadata.iptc = parseIPTC(meta.iptc);
payload.title = payload.title || payload.metadata.iptc.headline;
payload.title = payload.metadata.iptc.headline || payload.title;
payload.description = payload.description || payload.metadata.iptc.caption;
payload.tags = payload.metadata.iptc.keywords;
} catch (err) {
logger.warn(`Couldn't extract IPTC information from file`);
logger.warn(err);

View File

@@ -44,7 +44,7 @@ import {
import { Knex } from 'knex';
import { flatten, get, mapKeys, merge, set, uniq } from 'lodash';
import ms from 'ms';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import { BaseException, GraphQLValidationException, InvalidPayloadException } from '../exceptions';
import { listExtensions } from '../extensions';
@@ -115,7 +115,7 @@ export class GraphQLService {
constructor(options: AbstractServiceOptions & { scope: 'items' | 'system' }) {
this.accountability = options?.accountability || null;
this.knex = options?.knex || database;
this.knex = options?.knex || getDatabase();
this.schema = options.schema;
this.scope = options.scope;
}

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import database from '../database';
import getDatabase from '../database';
import { AbstractServiceOptions, Accountability, SchemaOverview } from '../types';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import StreamArray from 'stream-json/streamers/StreamArray';
@@ -15,7 +15,7 @@ export class ImportService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
}

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex';
import { clone, cloneDeep, merge, pick, without } from 'lodash';
import cache from '../cache';
import database from '../database';
import getDatabase from '../database';
import runAST from '../database/run-ast';
import emitter, { emitAsyncSafe } from '../emitter';
import env from '../env';
@@ -55,7 +55,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
constructor(collection: string, options: AbstractServiceOptions) {
this.collection = collection;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.eventScope = this.collection.startsWith('directus_') ? this.collection.substring(9) : 'items';
this.schema = options.schema;
@@ -204,7 +204,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
schema: this.schema,
// This hook is called async. If we would pass the transaction here, the hook can be
// called after the transaction is done #5460
database: database,
database: getDatabase(),
});
}
@@ -516,7 +516,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
schema: this.schema,
// This hook is called async. If we would pass the transaction here, the hook can be
// called after the transaction is done #5460
database: database,
database: getDatabase(),
});
}
@@ -665,7 +665,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
schema: this.schema,
// This hook is called async. If we would pass the transaction here, the hook can be
// called after the transaction is done #5460
database: database,
database: getDatabase(),
});
}

View File

@@ -2,7 +2,7 @@ import fse from 'fs-extra';
import { Knex } from 'knex';
import { Liquid } from 'liquidjs';
import path from 'path';
import database from '../../database';
import getDatabase from '../../database';
import env from '../../env';
import { InvalidPayloadException } from '../../exceptions';
import logger from '../../logger';
@@ -30,7 +30,7 @@ export class MailService {
constructor(opts: AbstractServiceOptions) {
this.schema = opts.schema;
this.accountability = opts.accountability || null;
this.knex = opts?.knex || database;
this.knex = opts?.knex || getDatabase();
}
async send(options: EmailOptions): Promise<void> {

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import database from '../database';
import getDatabase from '../database';
import { ForbiddenException } from '../exceptions';
import { AbstractServiceOptions, Accountability, SchemaOverview } from '../types';
import { Query } from '../types/query';
@@ -12,7 +12,7 @@ export class MetaService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
}

View File

@@ -2,9 +2,9 @@ import argon2 from 'argon2';
import { format, formatISO, parse, parseISO } from 'date-fns';
import Joi from 'joi';
import { Knex } from 'knex';
import { clone, cloneDeep, isObject, isPlainObject } from 'lodash';
import { clone, cloneDeep, isObject, isPlainObject, omit } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import database from '../database';
import getDatabase from '../database';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import { AbstractServiceOptions, Accountability, Item, PrimaryKey, Query, SchemaOverview } from '../types';
import { toArray } from '../utils/to-array';
@@ -43,7 +43,7 @@ export class PayloadService {
constructor(collection: string, options: AbstractServiceOptions) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.collection = collection;
this.schema = options.schema;
@@ -331,7 +331,13 @@ export class PayloadService {
.first());
if (exists) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord);
const fieldsToUpdate = omit(relatedRecord, relatedPrimary);
if (Object.keys(fieldsToUpdate).length > 0) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
});
}
} else {
relatedPrimaryKey = await itemsService.createOne(relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
@@ -393,9 +399,13 @@ export class PayloadService {
.first());
if (exists) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
});
const fieldsToUpdate = omit(relatedRecord, relatedPrimaryKeyField);
if (Object.keys(fieldsToUpdate).length > 0) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
});
}
} else {
relatedPrimaryKey = await itemsService.createOne(relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),

View File

@@ -7,20 +7,21 @@ import { ItemsService, QueryOptions } from './items';
import { PermissionsService } from './permissions';
import SchemaInspector from '@directus/schema';
import { ForeignKey } from 'knex-schema-inspector/dist/types/foreign-key';
import database, { schemaInspector } from '../database';
import getDatabase, { getSchemaInspector } from '../database';
import { getDefaultIndexName } from '../utils/get-default-index-name';
export class RelationsService {
knex: Knex;
permissionsService: PermissionsService;
schemaInspector: typeof schemaInspector;
schemaInspector: ReturnType<typeof SchemaInspector>;
accountability: Accountability | null;
schema: SchemaOverview;
relationsItemService: ItemsService<RelationMeta>;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.permissionsService = new PermissionsService(options);
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : schemaInspector;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
this.schema = options.schema;
this.accountability = options.accountability || null;
this.relationsItemService = new ItemsService('directus_relations', {
@@ -159,8 +160,10 @@ export class RelationsService {
await trx.schema.alterTable(relation.collection!, async (table) => {
this.alterType(table, relation);
const constraintName: string = getDefaultIndexName('foreign', relation.collection!, relation.field!);
table
.foreign(relation.field!)
.foreign(relation.field!, constraintName)
.references(
`${relation.related_collection!}.${this.schema.collections[relation.related_collection!].primary}`
)
@@ -168,7 +171,15 @@ export class RelationsService {
});
}
await this.relationsItemService.createOne(metaRow);
const relationsItemService = new ItemsService('directus_relations', {
knex: trx,
schema: this.schema,
// We don't set accountability here. If you have read access to certain fields, you are
// allowed to extract the relations regardless of permissions to directus_relations. This
// happens in `filterForbidden` down below
});
await relationsItemService.createOne(metaRow);
});
}
@@ -201,15 +212,18 @@ export class RelationsService {
await this.knex.transaction(async (trx) => {
if (existingRelation.related_collection) {
await trx.schema.alterTable(collection, async (table) => {
let constraintName: string = getDefaultIndexName('foreign', collection, field);
// If the FK already exists in the DB, drop it first
if (existingRelation?.schema) {
table.dropForeign(field);
constraintName = existingRelation.schema.constraint_name || constraintName;
table.dropForeign(field, constraintName);
}
this.alterType(table, relation);
table
.foreign(field)
.foreign(field, constraintName || undefined)
.references(
`${existingRelation.related_collection!}.${
this.schema.collections[existingRelation.related_collection!].primary
@@ -219,11 +233,19 @@ export class RelationsService {
});
}
const relationsItemService = new ItemsService('directus_relations', {
knex: trx,
schema: this.schema,
// We don't set accountability here. If you have read access to certain fields, you are
// allowed to extract the relations regardless of permissions to directus_relations. This
// happens in `filterForbidden` down below
});
if (relation.meta) {
if (existingRelation?.meta) {
await this.relationsItemService.updateOne(existingRelation.meta.id, relation.meta);
await relationsItemService.updateOne(existingRelation.meta.id, relation.meta);
} else {
await this.relationsItemService.createOne({
await relationsItemService.createOne({
...(relation.meta || {}),
many_collection: relation.collection,
many_field: relation.field,
@@ -259,9 +281,9 @@ export class RelationsService {
}
await this.knex.transaction(async (trx) => {
if (existingRelation.schema) {
if (existingRelation.schema?.constraint_name) {
await trx.schema.alterTable(existingRelation.collection, (table) => {
table.dropForeign(existingRelation.field);
table.dropForeign(existingRelation.field, existingRelation.schema!.constraint_name!);
});
}

View File

@@ -7,7 +7,7 @@ import { performance } from 'perf_hooks';
// @ts-ignore
import { version } from '../../package.json';
import cache from '../cache';
import database, { hasDatabaseConnection } from '../database';
import getDatabase, { hasDatabaseConnection } from '../database';
import env from '../env';
import logger from '../logger';
import { rateLimiter } from '../middleware/rate-limiter';
@@ -24,7 +24,7 @@ export class ServerService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
this.settingsService = new SettingsService({ knex: this.knex, schema: this.schema });
@@ -129,6 +129,7 @@ export class ServerService {
}
async function testDatabase(): Promise<Record<string, HealthCheck[]>> {
const database = getDatabase();
const client = env.DB_CLIENT;
const checks: Record<string, HealthCheck[]> = {};

View File

@@ -5,7 +5,7 @@ import { cloneDeep, mergeWith } from 'lodash';
import { OpenAPIObject, OperationObject, PathItemObject, SchemaObject, TagObject } from 'openapi3-ts';
// @ts-ignore
import { version } from '../../package.json';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import {
AbstractServiceOptions,
@@ -37,7 +37,7 @@ export class SpecificationService {
constructor(options: AbstractServiceOptions) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.schema = options.schema;
this.fieldsService = new FieldsService(options);
@@ -80,7 +80,7 @@ class OASSpecsService implements SpecificationSubService {
}
) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.schema = options.schema;
this.fieldsService = fieldsService;
@@ -541,7 +541,7 @@ class GraphQLSpecsService implements SpecificationSubService {
constructor(options: AbstractServiceOptions) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.schema = options.schema;
this.items = new GraphQLService({ ...options, scope: 'items' });

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import { Knex } from 'knex';
import { clone } from 'lodash';
import cache from '../cache';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import {
FailedValidationException,
@@ -29,7 +29,7 @@ export class UsersService extends ItemsService {
constructor(options: AbstractServiceOptions) {
super('directus_users', options);
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.service = new ItemsService('directus_users', options);
this.schema = options.schema;

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import database from '../database';
import getDatabase from '../database';
import { systemCollectionRows } from '../database/system-data/collections';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import { AbstractServiceOptions, Accountability, PrimaryKey, SchemaOverview } from '../types';
@@ -10,7 +10,7 @@ export class UtilsService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
}

View File

@@ -1,7 +1,6 @@
import { LocalFileSystemStorage, Storage, StorageManager, StorageManagerConfig } from '@directus/drive';
import { AzureBlobWebServicesStorage } from '@directus/drive-azure';
import { GoogleCloudStorage } from '@directus/drive-gcs';
/** @todo dynamically load these storage adapters */
import { AmazonWebServicesS3Storage } from '@directus/drive-s3';
import env from './env';
import { getConfigFromEnv } from './utils/get-config-from-env';

View File

@@ -0,0 +1,29 @@
import { customAlphabet } from 'nanoid';
const generateID = customAlphabet('abcdefghijklmnopqrstuvxyz', 5);
/**
* Generate an index name for a given collection + fields combination.
*
* Is based on the default index name generation of knex, but limits the index to a maximum of 64
* characters (the max length for MySQL and MariaDB).
*
* @see
* https://github.com/knex/knex/blob/fff6eb15d7088d4198650a2c6e673dedaf3b8f36/lib/schema/tablecompiler.js#L282-L297
*/
export function getDefaultIndexName(
type: 'unique' | 'foreign' | 'index',
collection: string,
fields: string | string[]
): string {
if (!Array.isArray(fields)) fields = fields ? [fields] : [];
const table = collection.replace(/\.|-/g, '_');
const indexName = (table + '_' + fields.join('_') + '_' + type).toLowerCase();
if (indexName.length <= 64) return indexName;
const suffix = `__${generateID()}_${type}`;
const prefix = indexName.substring(0, 64 - suffix.length);
return `${prefix}__${generateID()}_${type}`;
}

View File

@@ -98,6 +98,11 @@ export default function getLocalType(
return 'decimal';
}
/** Handle MS SQL varchar(MAX) (eg TEXT) types */
if (column.data_type === 'nvarchar' && column.max_length === -1) {
return 'text';
}
if (field?.special?.includes('json')) return 'json';
if (field?.special?.includes('hash')) return 'hash';
if (field?.special?.includes('csv')) return 'csv';

View File

@@ -11,13 +11,14 @@ import { toArray } from '../utils/to-array';
import getDefaultValue from './get-default-value';
import getLocalType from './get-local-type';
import { mergePermissions } from './merge-permissions';
import getDatabase from '../database';
export async function getSchema(options?: {
accountability?: Accountability;
database?: Knex;
}): Promise<SchemaOverview> {
// Allows for use in the CLI
const database = options?.database || (require('../database').default as Knex);
const database = options?.database || getDatabase();
const schemaInspector = SchemaInspector(database);
const result: SchemaOverview = {

View File

@@ -2,20 +2,6 @@ import env from '../env';
import logger from '../logger';
export function validateEnv(requiredKeys: string[]): void {
if (env.DB_CLIENT && env.DB_CLIENT === 'sqlite3') {
requiredKeys.push('DB_FILENAME');
} else if (env.DB_CLIENT && env.DB_CLIENT === 'oracledb') {
requiredKeys.push('DB_USER', 'DB_PASSWORD', 'DB_CONNECT_STRING');
} else {
if (env.DB_CLIENT === 'pg') {
if (!env.DB_CONNECTION_STRING) {
requiredKeys.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER');
}
} else {
requiredKeys.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
}
}
for (const requiredKey of requiredKeys) {
if (requiredKey in env === false) {
logger.error(`"${requiredKey}" Environment Variable is missing.`);

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import { ListenerFn } from 'eventemitter2';
import database from './database';
import getDatabase from './database';
import emitter from './emitter';
import logger from './logger';
import { Webhook } from './types';
@@ -10,6 +10,8 @@ let registered: { event: string; handler: ListenerFn }[] = [];
export async function register(): Promise<void> {
unregister();
const database = getDatabase();
const webhooks = await database.select<Webhook[]>('*').from('directus_webhooks').where({ status: 'active' });
for (const webhook of webhooks) {

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/app",
"version": "9.0.0-rc.69",
"version": "9.0.0-rc.73",
"private": false,
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
"author": "Rijk van Zanten <rijk@rngr.org>",
@@ -27,13 +27,13 @@
},
"gitHead": "24621f3934dc77eb23441331040ed13c676ceffd",
"devDependencies": {
"@directus/docs": "9.0.0-rc.69",
"@directus/format-title": "9.0.0-rc.69",
"@fullcalendar/core": "^5.7.0",
"@fullcalendar/daygrid": "^5.7.0",
"@fullcalendar/interaction": "^5.7.0",
"@fullcalendar/list": "^5.7.0",
"@fullcalendar/timegrid": "^5.7.0",
"@directus/docs": "9.0.0-rc.73",
"@directus/format-title": "9.0.0-rc.73",
"@fullcalendar/core": "^5.7.2",
"@fullcalendar/daygrid": "^5.7.2",
"@fullcalendar/interaction": "^5.7.2",
"@fullcalendar/list": "^5.7.2",
"@fullcalendar/timegrid": "^5.7.2",
"@popperjs/core": "^2.9.1",
"@sindresorhus/slugify": "^2.1.0",
"@tinymce/tinymce-vue": "^3.2.8",
@@ -63,13 +63,13 @@
"copyfiles": "^2.4.1",
"cropperjs": "^1.5.11",
"date-fns": "^2.21.1",
"dompurify": "^2.2.8",
"dompurify": "^2.2.9",
"escape-string-regexp": "^5.0.0",
"front-matter": "^4.0.2",
"html-entities": "^2.3.2",
"joi": "^17.4.0",
"jsonlint-mod": "^1.7.6",
"marked": "^2.0.5",
"marked": "^2.0.7",
"micromustache": "^8.0.3",
"mitt": "^2.1.0",
"nanoid": "^3.1.23",
@@ -81,7 +81,7 @@
"raw-loader": "^4.0.2",
"resize-observer": "^1.0.2",
"rimraf": "^3.0.2",
"sass": "^1.34.0",
"sass": "^1.34.1",
"sass-loader": "^9.0.2",
"stylelint": "^13.13.1",
"tiny-async-pool": "^1.2.0",
@@ -89,7 +89,7 @@
"vue": "^2.6.12",
"vue-cli-plugin-yaml": "^1.0.2",
"vue-i18n": "^8.24.4",
"vue-loader": "^15.9.3",
"vue-loader": "^15.9.7",
"vue-router": "^3.4.8",
"vue-template-compiler": "^2.6.10",
"vuedraggable": "^2.24.3",

View File

@@ -5,6 +5,7 @@
:role="hasClick ? 'button' : null"
@click="emitClick"
:tabindex="hasClick ? 0 : null"
:style="{ '--v-icon-color': color }"
>
<component v-if="customIconName" :is="customIconName" />
<i v-else :class="{ filled }">{{ name }}</i>
@@ -98,6 +99,9 @@ export default defineComponent({
type: Boolean,
default: false,
},
color: {
type: String,
},
...sizeProps,
},

View File

@@ -133,7 +133,6 @@ body {
color: var(--v-list-item-color);
text-decoration: none;
border-radius: var(--v-list-item-border-radius);
pointer-events: all;
&.dashed {
&::after {

View File

@@ -80,7 +80,6 @@ body {
color: var(--v-list-color);
line-height: 22px;
border-radius: var(--border-radius);
pointer-events: none;
&.large {
--v-list-padding: 12px;

View File

@@ -1,17 +1,10 @@
<template>
<div class="file">
<v-menu attached :disabled="disabled || loading">
<v-menu attached :disabled="loading">
<template #activator="{ toggle }">
<div>
<v-skeleton-loader type="input" v-if="loading" />
<v-input
v-else
@click="toggle"
readonly
:placeholder="$t('no_file_selected')"
:disabled="disabled"
:value="file && file.title"
>
<v-input v-else @click="toggle" readonly :placeholder="$t('no_file_selected')" :value="file && file.title">
<template #prepend>
<div
class="preview"
@@ -30,7 +23,13 @@
<template #append>
<template v-if="file">
<v-icon name="open_in_new" class="edit" v-tooltip="$t('edit')" @click.stop="editDrawerActive = true" />
<v-icon class="deselect" name="close" @click.stop="$emit('input', null)" v-tooltip="$t('deselect')" />
<v-icon
v-if="!disabled"
class="deselect"
name="close"
@click.stop="$emit('input', null)"
v-tooltip="$t('deselect')"
/>
</template>
<v-icon v-else name="attach_file" />
</template>
@@ -45,33 +44,35 @@
<v-list-item-content>{{ $t('download_file') }}</v-list-item-content>
</v-list-item>
<v-divider />
<v-divider v-if="!disabled" />
</template>
<v-list-item @click="activeDialog = 'upload'">
<v-list-item-icon><v-icon name="phonelink" /></v-list-item-icon>
<v-list-item-content>
{{ $t(file ? 'replace_from_device' : 'upload_from_device') }}
</v-list-item-content>
</v-list-item>
<template v-if="!disabled">
<v-list-item @click="activeDialog = 'upload'">
<v-list-item-icon><v-icon name="phonelink" /></v-list-item-icon>
<v-list-item-content>
{{ $t(file ? 'replace_from_device' : 'upload_from_device') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="activeDialog = 'choose'">
<v-list-item-icon><v-icon name="folder_open" /></v-list-item-icon>
<v-list-item-content>
{{ $t(file ? 'replace_from_library' : 'choose_from_library') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="activeDialog = 'choose'">
<v-list-item-icon><v-icon name="folder_open" /></v-list-item-icon>
<v-list-item-content>
{{ $t(file ? 'replace_from_library' : 'choose_from_library') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="activeDialog = 'url'">
<v-list-item-icon><v-icon name="link" /></v-list-item-icon>
<v-list-item-content>
{{ $t(file ? 'replace_from_url' : 'import_from_url') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="activeDialog = 'url'">
<v-list-item-icon><v-icon name="link" /></v-list-item-icon>
<v-list-item-content>
{{ $t(file ? 'replace_from_url' : 'import_from_url') }}
</v-list-item-content>
</v-list-item>
</template>
</v-list>
</v-menu>
<drawer-item
v-if="!disabled && file"
v-if="file"
:active.sync="editDrawerActive"
collection="directus_files"
:primary-key="file.id"

View File

@@ -20,7 +20,7 @@
:force-fallback="true"
:value="sortedItems"
@input="sortItems($event)"
handler=".drag-handle"
handle=".drag-handle"
:disabled="!junction.meta.sort_field"
>
<v-list-item

View File

@@ -20,7 +20,7 @@
:force-fallback="true"
:value="sortedItems"
@input="sortItems($event)"
handler=".drag-handle"
handle=".drag-handle"
:disabled="!relation.meta.sort_field"
>
<v-list-item

View File

@@ -5,7 +5,7 @@
</v-notice>
<v-list v-if="value && value.length > 0">
<draggable :force-fallback="true" :value="value" @input="$emit('input', $event)" handler=".drag-handle">
<draggable :force-fallback="true" :value="value" @input="$emit('input', $event)" handle=".drag-handle">
<v-list-item
:dense="value.length > 4"
v-for="(item, index) in value"

View File

@@ -3,7 +3,7 @@
<template #activator>
<v-input
:disabled="disabled"
:placeholder="$t('interfaces.color.placeholder')"
:placeholder="$t('interfaces.select-color.placeholder')"
v-model="hex"
:pattern="/#([a-f\d]{2}){3}/i"
class="color-input"

View File

@@ -1,5 +1,6 @@
af-ZA: Afrikaans (South Africa)
ar-SA: Arabic (Saudi Arabia)
bn-IN: Bengali (India)
bg-BG: Bulgarian (Bulgaria)
ca-ES: Catalan (Spain)
zh-CN: Chinese (Simplified)

View File

@@ -0,0 +1 @@
---

View File

@@ -20,33 +20,83 @@ create_role: Vytvořit roli
create_user: Vytvořit uživatele
create_webhook: Vytvořit webhook
invite_users: Pozvat uživatele
email_examples: 'admin@example.com, user@example.com...'
invite: Pozvat
email_already_invited: E-mail "{email}" již byl pozván
emails: E-mail
connection_excellent: Vynikající připojení
connection_good: Dobré připojení
connection_fair: Zhoršené připojení
connection_poor: Špatné připojení
primary: Primární
rename_folder: Přejmenovat složku
delete_folder: Smazat složku
prefix: Prefix
suffix: Přípona
reset_bookmark: Obnovit záložku
rename_bookmark: Přejmenovat záložku
update_bookmark: Aktualizovat záložku
delete_bookmark: Smazat záložku
delete_bookmark_copy: >-
Jste si jisti, že chcete odstranit záložku "{bookmark}"? Tuto akci nelze vrátit zpět.
logoutReason:
SIGN_OUT: Odhlášen
SESSION_EXPIRED: Relace vypršela
public: Veřejné
public_description: Určuje, která API data jsou dostupná bez ověřování.
not_allowed: Nepovoleno
directus_version: Verze Directusu
node_version: Node verze
node_uptime: Doba provozu Node
os_type: Typ OS
os_version: Verze OS
os_uptime: Dobra provozu operačního systému
os_totalmem: Pamět OS
archive: Archivovat
archive_confirm: Skutečně chcete tuto položku archivovat?
archive_confirm_count: >-
Nevybrány žádné položky | Jste si jisti, že chcete archivovat tuto položku? | Jste si jisti, že chcete archivovat tyto {count} položky?
reset_system_permissions_to: 'Resetovat systémová oprávnění na:'
reset_system_permissions_copy: Tato akce přepíše libovolná vlastní oprávnění, která jste možná použili na kolekce systému. Jste si jisti?
the_following_are_minimum_permissions: Následující oprávnění jsou vyžadována v případě, že je povolen "Přístup k aplikaci". Můžete rozšířit oprávnění nad tuto úroveň, ale ne níže.
recommended_defaults: Doporučené výchozí hodnoty
unarchive: Vyjmout z archivu
unarchive_confirm: Skutečně chcete tuto položku obnovit z archivu?
nested_files_folders_will_be_moved: Vnořené soubory a složky budou přesunuty o úroveň výše.
unknown_validation_errors: 'Došlo k chybám validace pro následující skrytá pole:'
validationError:
eq: Hodnota musí být {valid}
neq: Hodnota nemůže být {invalid}
in: Hodnota musí být jedna z {valid}
nin: Hodnota nemůže být jedna z {invalid}
contains: Hodnota musí obsahovat {substring}
ncontains: Hodnota nesmí obsahovat {substring}
gt: Hodnota musí být větší než {valid}
gte: Hodnota musí být větší nebo rovna {valid}
lt: Hodnota musí být menší než {valid}
lte: Hodnota musí být menší nebo rovna {valid}
empty: Hodnota musí být prázdná
nempty: Hodnota nesmí být prázdná
null: Hodnota musí být null
nnull: Hodnota nesmí být null
required: Hodnota je povinná
unique: Hodnota musí být unikátní
regex: Hodnota nemá správný formát
all_access: Veškerý přístup
no_access: Žádný přístup
use_custom: Použít vlastní
allow_null_value: Povolit NULL hodnotu
enter_value_to_replace_nulls: Zadejte novou hodnotu, která nahradí všechny NULL, které jsou aktuálně v tomto poli.
field_standard: Standardní
field_m2o: M2O vazba
field_m2a: M2A vazba
field_o2m: O2M vazba
field_m2m: M2M vazba
item_permissions: Oprávnění položky
field_permissions: Oprávnění pole
field_validation: Validace pole
permissions_for_role: 'Položky, které {role} role může {action}.'
fields_for_role: 'Pole, které {role} role může {action}.'
delete_field: Smazat pole
language: Jazyk
global: Globální
@@ -142,6 +192,7 @@ upload_files_indeterminate: 'Nahrávám soubory {done}/{total}'
upload_files_success: 'Nahráno {count} souborů'
drag_file_here: Přetáhněte sem soubor
layout_options: Možnosti rozvržení
value_unique: Hodnota musí být unikátní
all_files: Všechny soubory
my_files: Mé soubory
recent_files: Nedávné soubory
@@ -288,6 +339,8 @@ sort_direction: Směr řazení
translation: Překlad
value: Hodnota
interfaces:
presentation-links:
primary: Primární
collection:
collection: Kategorie
system-collections:

View File

@@ -22,6 +22,7 @@ create_webhook: Crear Webhook
invite_users: Invitar Usuarios
email_examples: 'admin@example.com, user@example.com...'
invite: Invitar
email_already_invited: El correo electrónico "{email}" ya ha sido invitado
emails: Correos Electrónicos
connection_excellent: Excelente Conexión
connection_good: Buena Conexión
@@ -81,9 +82,13 @@ validationError:
nnull: El valor no puede ser nulo
required: El valor es requerido
unique: El Valor tiene que ser único
regex: El valor no tiene el formato correcto
all_access: Todos los accesos
no_access: Sin Acceso
use_custom: Personalizar
nullable: Acepta valores nulos
allow_null_value: Permitir valor NULL
enter_value_to_replace_nulls: Por favor, introduzca un nuevo valor para reemplazar cualquier valor NULO actualmente dentro de este campo.
field_standard: Estándar
field_presentation: Presentación y Alias
field_file: Un Archivo
@@ -138,6 +143,7 @@ decimal: Decimal
float: Número Flotante
integer: Número Entero
json: JSON
xml: XML
string: Cadena de Texto
text: Texto
time: Hora
@@ -160,6 +166,7 @@ click_to_manage_translated_fields: >-
fields_group: Grupo de Campos
no_collections_found: No se encontraron colecciones.
new_data_alert: 'Los siguientes se crearán dentro de su Modelo de Datos:'
search_collection: Buscar colección...
new_field: 'Nuevo Campo'
new_collection: 'Colección Nueva'
add_m2o_to_collection: 'Agregar Muchos-A-Uno a "{collection}"'
@@ -226,6 +233,7 @@ item_delete_success: Elemento Eliminado | Elementos Eliminados
this_collection: Esta Colección
related_collection: Colección Relacionada
related_collections: Colecciones Relacionadas
translations_collection: Traducción de Colección
languages_collection: Colección de Idiomas
export_data: Exportar Datos
format: Formato
@@ -312,6 +320,10 @@ save_and_create_new: Guardar y Crear Nuevo
save_and_stay: Guardar y Permanecer
save_as_copy: Guardar como Copia
add_existing: Agregar Existente
creating_items: Creando elementos
enable_create_button: Habilitar el botón Crear
selecting_items: Seleccionando elementos
enable_select_button: Habilitar el botón Seleccionar
comments: Comentarios
no_comments: Aún Sin Comentarios
click_to_expand: Haga Clic para Expandir
@@ -326,6 +338,7 @@ interface_not_found: 'Interfaz "{interface}" no encontrada.'
reset_interface: Restablecer Interfaz
display_not_found: 'Presentación "{display}" no encontrada.'
reset_display: Restablecer Presentación
list-m2a: Constructor (M2A)
item_count: 'Sin Elementos | Un Elemento | {count} Elementos'
no_items_copy: Aún no hay Elementos en esta colección.
file_count: 'Sin Archivos | Un Archivo | {count} Archivos'
@@ -392,6 +405,8 @@ errors:
ITEM_NOT_FOUND: Elemento no encontrado
ROUTE_NOT_FOUND: No encontrado
RECORD_NOT_UNIQUE: Se ha detectado un valor duplicado
USER_SUSPENDED: Usuario Suspendido
CONTAINS_NULL_VALUES: El campo contiene valores nulos
UNKNOWN: Error Inesperado
INTERNAL_SERVER_ERROR: Error Inesperado
value_hashed: Valor Hasheado de Manera Segura
@@ -469,6 +484,8 @@ operators:
has: Contiene alguna de estas llaves
loading: Cargando...
drop_to_upload: Arrastrar para Subir
item: Elemento
items: Elementos
upload_file: Subir Archivo
upload_file_indeterminate: Subiendo Archivo...
upload_file_success: Archivo Subido
@@ -486,6 +503,8 @@ value_unique: El Valor tiene que ser único
all_activity: Toda la Actividad
create_item: Crear Elemento
display_template: Plantilla de Presentación
language_display_template: Plantilla de visualización de idioma
translations_display_template: Plantilla de visualización de traducciones
n_items_selected: 'No hay Elementos Seleccionados | 1 Elemento Seleccionado | {n} Elementos Seleccionados'
per_page: Por Página
all_files: Todos los Archivos
@@ -516,8 +535,21 @@ toggle: Alternar
icon_on: Icono Activado
icon_off: Icono Desactivado
label: Etiqueta
image_url: Url de la imagen
alt_text: Texto Alternativo
media: Medios
width: Ancho
height: Alto
source: Fuente
url_placeholder: Introduzca una url...
display_text: Mostrar texto
display_text_placeholder: Introduzca un texto de visualización...
tooltip: Sugerencia
tooltip_placeholder: Introduzca una sugerencia...
unlimited: Ilimitado
open_link_in: Abrir enlace en
new_tab: Nueva pestaña
current_tab: Pestaña actual
wysiwyg_options:
aligncenter: Alinear al Centro
alignjustify: Alinear Justificado
@@ -537,7 +569,10 @@ wysiwyg_options:
bullist: Lista con viñetas
numlist: Lista Numerada
hr: Regla Horizontal
link: Añadir/Editar enlace
unlink: Remover Enlace
media: Añadir/Editar medios
image: Añadir/Editar Imagen
copy: Copiar
cut: Cortar
paste: Pegar
@@ -559,6 +594,7 @@ wysiwyg_options:
selectall: Seleccionar Todo
table: Tabla
visualaid: Ver elementos invisibles
source_code: Editar código fuente
fullscreen: Pantalla Completa
directionality: Direccionalidad
dropdown: Lista Desplegable
@@ -571,6 +607,8 @@ adding_user: Agregando Usuario
unknown_user: Usuario Desconocido
creating_in: 'Creando Elemento en {collection}'
editing_in: 'Editando Elemento en {collection}'
creating_unit: 'Creando {unit}'
editing_unit: 'Editando {unit}'
editing_in_batch: 'Editando {count} Elementos por lote'
no_options_available: Sin opciones disponibles
settings_data_model: Modelo de Datos
@@ -578,10 +616,13 @@ settings_permissions: Roles y Permisos
settings_project: Configuración del Proyecto
settings_webhooks: Webhooks
settings_presets: Predefinidos y Marcadores
one_or_more_options_are_missing: Falta una o más opciones
scope: Alcance
select: Seleccionar...
layout: Diseño
tree_view: Vista en árbol
changes_are_permanent: Los cambios son permanentes
preset_name_placeholder: Valor predeterminado cuando está vacío...
preset_search_placeholder: Consulta de búsqueda...
editing_preset: Editar Predefinido
layout_preview: Vista Previa de Diseño
@@ -651,11 +692,14 @@ fields:
display_template: Plantilla de Presentación
hidden: Oculto
singleton: Singleton
translations: Traducciones para Nombre de Colección
archive_app_filter: Archivar filtros de App
archive_value: Archivar valor
unarchive_value: Desarchivar valor
sort_field: Campo Ordenamiento
accountability: Actividad y seguimiento de revisión
directus_files:
$thumbnail: Miniatura
title: Título
description: Descripción
tags: Etiquetas
@@ -728,6 +772,11 @@ fields:
users: Usuarios en el Rol
module_list: Navegación en Módulo
collection_list: Navegación en Colección
field_options:
directus_collections:
track_activity_revisions: Rastrear actividad y revisiones
only_track_activity: Sólo seguimiento de actividad
do_not_track_anything: No realizar seguimiento
no_fields_in_collection: 'Aún no hay campos en "{collection}"'
do_nothing: No hacer nada
generate_and_save_uuid: Generar y Guardar UUID
@@ -737,6 +786,13 @@ save_current_datetime: Guardar Fecha y Hora Actual
block: Bloque
inline: Alineado
comment: Comentar
relational_triggers: Disparadores Relacionales
referential_action_field_label_m2o: Al eliminar {collection}...
referential_action_field_label_o2m: Al deseleccionar {collection}...
referential_action_no_action: Evitar la eliminación
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.
@@ -801,9 +857,25 @@ view_project: Ver Proyecto
report_error: Reportar Error
interfaces:
presentation-links:
presentation-links: Botón de enlace
links: Enlaces
description: Botones de enlace configurables para abrir URLs dinámicas
style: Estilo
primary: Principal
link: Enlaces
button: Botones
error: No se puede realizar la acción
select-multiple-checkbox:
checkboxes: Casillas de selección
description: Elegir entre múltiples opciones a través de casillas de selección
allow_other: Permitir otro
show_more: 'Mostrar {count} más'
items_shown: Elementos Mostrados
input-code:
code: Código
description: Escribir o compartir fragmentos de código
line_number: Número de línea
placeholder: Ingrese el código aquí...
collection:
collection: Colección
description: Seleccionar entre colecciones existentes
@@ -814,6 +886,11 @@ interfaces:
include_system_collections: Incluir Colecciones del Sistema
select-color:
color: Color
description: Introduzca o seleccione un valor de color
placeholder: Seleccione un color...
preset_colors: Colores preestablecidos
preset_colors_add_label: Añadir nuevo color...
name_placeholder: Introduce el nombre del color...
datetime:
datetime: Fecha/hora
description: Ingrese fechas y horas
@@ -822,11 +899,13 @@ interfaces:
use_24: Usar Formato 24 horas
system-display-template:
display-template: Plantilla de Presentación
description: Combinar texto estático y valores de campos dinámicos
collection_field_not_setup: La opción del campo de colección está mal configurada
select_a_collection: Seleccionar una Colección
presentation-divider:
divider: Separador
select-dropdown:
allow_other: Permitir otro
choices_value_placeholder: Ingrese un valor...
file:
file: Archivo

View File

@@ -1,6 +1,6 @@
---
edit_field: Redigeeri
item_revision: Üksuse läbivaatlus
edit_field: Muuda välja
item_revision: Kirje parandamine
duplicate_field: Korduv väli
half_width: Pool laiust
full_width: Täislaius
@@ -43,7 +43,7 @@ logoutReason:
SIGN_OUT: Välja logitud
SESSION_EXPIRED: Sessioon aegunud
public: Avalik
public_description: Kontrollib, mis API andmed on saadaval audentimata.
public_description: Kontrollib, millised API andmed on saadaval autentimata kasutajale.
not_allowed: Ei ole lubatud
directus_version: Directuse versioon
node_version: Node versioon
@@ -58,6 +58,8 @@ archive_confirm_count: >-
Üksuseid pole valitud | Oled kindel, et soovid arhiveerida seda üksust? | Oled kindel, et soovid arhiveerida neid {count} üksuseid?
reset_system_permissions_to: 'Lähtesta süsteemi load:'
reset_system_permissions_copy: See tegevus kirjutab üle kõik eelnevad süsteemsetele kogudele kohandatud load. Kas oled kindel?
the_following_are_minimum_permissions: Need on vähimad load, mida on vaja "Rakenduse ligipääsu" võimaldamiseks. Sa võid ligipääsulube lisada, kuid mitte vähendada.
app_access_minimum: Rakenduse ligipääsu miinimum
recommended_defaults: Soovituslikud vaikeväärtused
unarchive: Eemalda arhiivist
unarchive_confirm: Oled kindel, et soovid selle üksuse arhiivist eemaldada?
@@ -86,6 +88,7 @@ no_access: Ei ole ligipääsu
use_custom: Kasuta kohandatud
nullable: Nullitav
allow_null_value: Luba NULL väärtus
enter_value_to_replace_nulls: Palun sisesta uus väärtus, et asendada kõik senised NULL väärtused sellel väljal.
field_standard: Standard
field_presentation: Kujundus ja aliased
field_file: Üksik fail
@@ -319,6 +322,7 @@ save_as_copy: Salvesta koopiana
add_existing: Lisa olemasolev
creating_items: Kirjete loomine
enable_create_button: Luba lisamise nupp
selecting_items: Kirjete valimine
enable_select_button: Luba valimise nupp
comments: Kommentaarid
no_comments: Kommentaarid puuduvad
@@ -628,14 +632,48 @@ unsaved_changes_copy: Oled sa kindel, et soovid sellelt lehelt lahkuda?
discard_changes: Loobu muudatustest
keep_editing: Jätka muutmist
page_help_collections_overview: '**Andmekogude ülevaade** — Nimekiri andmekogudest, millele sul on ligipääs'
page_help_collections_collection: >-
**Vaata kirjeid* - näitab kõiki {collection} andmekogu kirjeid, millele sul on ligipääs. Kiiremaks ligipääsuks saad seadistada vaateid, filtreid, sorteerimist ning lisada järjehoidjaid.
page_help_collections_item: >-
**Kirje detailid** - Selle kirje vaatamise ja muutmise vorm. Külgribal on ühtlasi kirje muudatuste ajalugu ning lisatud kommentaarid.
page_help_activity_collection: >-
**Vaata aktiivsust* - Põhjalik nimekiri kõigist toimingutest, mis süsteemis on tehtud.
page_help_docs_global: >-
**Dokumentatsioon** - Selle projekti ülesehitust kirjeldav teave
page_help_files_collection: >-
**Failikogu* - Näitab kõiki faile ja pilte selles projektis. Muuta saab vaateid, filtreid jm ning lisada järjehoidjaid
page_help_files_item: >-
**Faili detailid* - Vorm, milles saab muuta faili metaandmeid, ligipääse jm
page_help_settings_project: "**Projekti seaded** - Sinu projekti üldised seaded"
page_help_settings_datamodel_collections: >-
**Andmekogud** - Näitab kõiki andmekogusid (sh süsteemseid)
page_help_settings_datamodel_fields: >-
**Andmekogu** - Ükskiku andmekogu ja tema väljade muutmise vorm
page_help_settings_roles_collection: '**Vaata rolle** - Näitab adminne, avalikku jm kasutajarolle.'
page_help_settings_roles_item: "**Rolli detailid** - Muuda rolli ligipääse ja muid seadistusti."
page_help_settings_presets_collection: >-
**Näita eelseadistusi** - Näitab kõiki projekti eelseadistusi, sh kasutajad, rollid, järjehoidjad ja vaikimisi vaated
page_help_settings_presets_item: >-
**Eelseadistuse detailid" - Vorm järjehoidjate ja andmekogude eelseadistuste muutmiseks.
page_help_settings_webhooks_collection: '**Vaata veebikonkse** - Näitab veebikonkse.'
page_help_settings_webhooks_item: '**Veebikonksu detailid** - Vorm veebikonksude lisamiseks ja muutmiseks.'
page_help_users_collection: '**Kasutajad** - Näitab kõiki kasutajaid.'
page_help_users_item: >-
**Kasutaja detailid** - Näitab konto infot.
activity_feed: Tegevusvoog
add_new: Lisa uus
create_new: Loo Uus
all: Kõik
none: Puudub
no_layout_collection_selected_yet: Vaadet/andmekogu pole valitud
batch_delete_confirm: >-
Ühtegi kirjet pole valitud | Oled sa kindel, et soovid selle kirje kustutada? Seda toimingut ei saa tagasi võtta. | Oled sa kindel, et soovid need {count} ühikut kustutada? Seda toimingut ei saa tagasi võtta.
cancel: Loobu
collection: Kogud
collections: Kogud
singleton: Üksik
singleton_label: Käsitle üksiku objektina
system_fields_locked: Süsteemsed välja on lukus ja neid ei saa muuta
fields:
directus_activity:
item: Kirje peamine võti
@@ -646,18 +684,27 @@ fields:
comment: Kommentaar
user_agent: Kasutaja agent
ip: IP aadress
revisions: Versioonid
directus_collections:
collection: Kogud
icon: Ikoon
note: Märkus
display_template: Vaate mall
hidden: Peidetud
singleton: Üksik
translations: Andmekogu nimetuste tõlked
archive_app_filter: Arhiveeri rakenduse filter
archive_value: Arhiveeri väärtus
unarchive_value: Aktiveeri väärtus
sort_field: Sorteerimisväli
accountability: Tegevuste ja muudatuste jälgimine
directus_files:
$thumbnail: Pisipilt
title: Pealkiri
description: Kirjeldus
tags: Sildid
location: Asukoht
storage: Salvestusruum
filename_disk: Failinimi (kettal)
filename_download: Faili nimi (alla laadimisel)
metadata: Metaandmed
@@ -703,24 +750,59 @@ fields:
public_note: Avalik märkus
auth_password_policy: Salasõna reeglid
auth_login_attempts: Sisselogimise katseid
storage_asset_presets: Salvestusruumi eelseadistused
storage_asset_transform: Salvestusruumi kirjete töötlus
custom_css: Kohandatud CSS
directus_fields:
collection: Andmekogu nimetus
icon: Andmekogu pilt
note: Märkus
hidden: Peidetud
singleton: Üksik
translation: Väljade tõlked
display_template: Mall
directus_roles:
name: Rolli nimi
icon: Rolli pilt
description: Kirjeldus
app_access: Rakenduse ligipääs
admin_access: Admini ligipääs
ip_access: IP ligipääs
enforce_tfa: 2FA on nõutav
users: Kasutajad selles rollis
module_list: Moodulite näitamine
collection_list: Andmekogude näitamine
field_options:
directus_collections:
track_activity_revisions: Salvesta muudatuste statistika
only_track_activity: Jälgi ainult aktiivsust
do_not_track_anything: Ära jälgi midagi
no_fields_in_collection: 'Andmekogus "{collection}" puuduvad kirjed'
do_nothing: Ära tee midagi
generate_and_save_uuid: Genereeri ja salvesta UUID
save_current_user_id: Salvesta praeguse kasutaja ID
save_current_user_role: Salvesta praeguse kasutaja roll
save_current_datetime: Salvesta hetke kuupäev/kellaaeg
block: Blokeeri
inline: Tekstisisene
comment: Kommentaar
relational_triggers: Suhtelised lülitid
referential_action_field_label_m2o: Andmekogu {collection} kustutamisel...
referential_action_field_label_o2m: Andmekogu {collection} mittevalimisel...
referential_action_no_action: Enneta kustutamist
referential_action_cascade: Kustuta {collection} kirje (kaskaadina)
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
delete: Kustuta
delete_are_you_sure: >-
See tegevus on lõplik ja seda ei saa tühistada. Kas oled kindel, et jätkame?
delete_field_are_you_sure: >-
Kas soovid kindlasti välja "{field}" kustutada? Seda toimingut ei saa tagasi võtta.
description: Kirjeldus
@@ -728,6 +810,7 @@ done: Valmis
duplicate: Loo koopia
email: E-post
embed: Põimitud
fallback_icon: Tagavaraikoon
field: Väli | Väljad
file: Fail
file_library: Failikogu
@@ -735,10 +818,16 @@ forgot_password: Unustasin parooli
hidden: Peidetud
icon: Ikoon
info: Info
normal: Tavaline
success: Õnnestus
warning: Hoiatus
danger: Oht
junction_collection: Seoste andmekogu
latency: Latsentsus
login: Logi sisse
my_activity: Minu tegevus
not_authenticated: Pole autenditud
authenticated: Autenditud
options: Valikud
otp: Ühekordne parool
password: Parool
@@ -746,11 +835,13 @@ permissions: Õigused
relationship: Seos
reset: Lähtesta
reset_password: Lähtesta parool
revisions: Versioonid
revert: Taasta
save: Salvesta
schema: Skeem
search: Otsi
select_existing: Vali olemasolev
select_field_type: Vali välja tüüp
select_interface: Vali kasutajaliides
settings: Seaded
sign_in: Logi sisse
@@ -763,85 +854,296 @@ sort_desc: Sorteeri kahanevalt
template: Mall
translation: Tõlge
value: Väärtus
view_project: Näita projekti
weeks: { }
report_error: Raporteeri viga
interfaces:
presentation-links:
presentation-links: Nuppude lingid
links: Lingid
description: Seadistatavad nupud dünaamiliste URLide avamiseks
style: Stiil
primary: Peamine
link: Lingid
button: Nupud
error: Toimingut ei saa teha
select-multiple-checkbox:
checkboxes: Märkeruudud
description: Tee valikud märkeruutude abil
allow_other: Luba muud
show_more: 'Näita veel {count}'
items_shown: Nähtavad kirjed
input-code:
code: Kood
description: Kirjuta või jaga koodilõike
line_number: Rea number
placeholder: Sisesta kood siia...
collection:
collection: Kogud
description: Vali olemasolevate andmekogude seast
include_system_collections: Lisa süsteemsed andmekogud
system-collections:
collections: Kogud
description: Vali olemasolevate andmekogude seast
include_system_collections: Lisa süsteemsed andmekogud
select-color:
color: Värv
description: Sisesta või vali värv
placeholder: Vali värv...
preset_colors: Eelseadistuse värvid
preset_colors_add_label: Lisa uus värv...
name_placeholder: Sisesta värvi nimi...
datetime:
datetime: Kuupäev-kellaaeg
description: Sisesta kuupäevad ja kellaajad
include_seconds: Lisa sekundid
set_to_now: Seadista praegusele hetkele
use_24: Kasuta 24h formaati
system-display-template:
display-template: Vaate mall
description: Kombineeri staatilist teksti ja dünaamiliste väljadega
collection_field: Andmekogu väli
collection_field_not_setup: Andmekogu väli on valesti seadistatud
select_a_collection: Vali andmekogu
presentation-divider:
divider: Eraldaja
description: Nimeta ja eralda välju sektsioonideks
title_placeholder: Sisesta pealkiri...
inline_title: Pealkiri joone sees
inline_title_label: Näita pealkirja keset joont
margin_top: Ülemine vahe
margin_top_label: Suurenda ülemist vahet
select-dropdown:
description: Vali väärtust rippmenüüst
choices_placeholder: Lisa uus valik
allow_other: Luba muud
allow_other_label: Luba teised väärtused
allow_none: Luba tühiväärtus
allow_none_label: Luba jätta valimata
choices_name_placeholder: Sisesta nimi...
choices_value_placeholder: Sisesta väärtus...
select-multiple-dropdown:
select-multiple-dropdown: Rippmenüü (mitmikvalikuga)
description: Vali mitu väärtust rippmenüüst
file:
file: Fail
description: Vali olemasolev fail või lae üles uus
files:
files: Failid
description: Vali või lae üles mitu faili
input-hash:
hash: Hash (räsi)
description: Sisesta väärtus, mida maskeerida
masked: Maskeeritud
masked_label: Peida tõese väärtus korral
select-icon:
icon: Ikoon
description: Vali ikoon rippmenüüst
search_for_icon: Otsi ikooni...
file-image:
image: Pilt
description: Vali või lae üles pilt
system-interface:
interface: Kasutajaliides
description: Vali olemasolev liides
placeholder: Vali kasutajaliides...
system-interface-options:
interface-options: Kasutajaliidese valikud
description: Kasutajaliidese valikute tegemise hüpikaken
list-m2m:
many-to-many: Mitu mitmele
description: Vali mitu seoetud kirjet
select-dropdown-m2o:
many-to-one: Mitu ühele
description: Vali üksik seotud kirje
display_template: Vaate mall
input-rich-text-md:
markdown: Markdown
description: Sisesta ja vaata markdown'i
customSyntax: Kohandatud plokid
customSyntax_label: Lisa kohandatud süntaksi tüüpe
customSyntax_add: Lisa kohandatud süntaks
box: Plokk / Tekstisisene
imageToken: Pildi kood
imageToken_label: Milline (staatiline) kood lisada piltide allikatele
presentation-notice:
notice: Teade
description: Näita lühikest teadet
text: Sisesta teate sisu...
list-o2m:
one-to-many: Üks-mitmele
description: Vali mitu seotud kirjet
no_collection: Andmekogu ei leitud
select-radio:
radio-buttons: Raadionupud
description: Vali üks mitmest valikust
list:
repeater: Kordaja
description: Loo mitu kirjet sama struktuuriga
edit_fields: Muuda välju
add_label: '"Lisa uus" nimetus'
field_name_placeholder: Sisesta välja nimi...
field_note_placeholder: Sisesta välja märkus...
slider:
slider: Liugur
description: Vali number slaiderilt
always_show_value: Näita alati väärtust
tags:
tags: Sildid
description: Vali või sisesta märksõnad
whitespace: Tühikud
hyphen: Asenda sidekriipsuga
underscore: Asenda alakriipsuga
remove: Eemalda tühikud
capitalization: Esitähed
uppercase: Muuda suurtähtedeks
lowercase: Muuda väiketähtedeks
auto_formatter: Kasuta pealkirja stiili
alphabetize: Järjesta tähestikuliselt
alphabetize_label: Sorteeri tähestikuliselt ümber
add_tags: Lisa märksõnu...
input:
input: Sisend
description: Sisesta väärtus käsitsi
trim: Kärbi
trim_label: Lõika maha tühi algus ja lõpp
mask: Maskeeritud
mask_label: Peida algne väärtus
clear: Tühjendatud väärtus
clear_label: Salvesta tühja stringina
minimum_value: Minimaalne väärtus
maximum_value: Maksimaalne väärtus
step_interval: Sammu intervall
slug: Muuda URL-sõbralikuks
slug_label: Muuda väärtus URL-sõbralikuks (slug)
input-multiline:
textarea: Tekstiala
description: Sisesta mitmerealine tavatekst
boolean:
toggle: Lülita
description: Lülita sisse ja välja
label_placeholder: Sisesta silt...
label_default: lubatud
translations:
display_template: Vaate mall
no_collection: Andmekogusid pole
list-o2m-tree-view:
description: Puuvaade mitmetasandilistele üks-mitmele kirjetele
recursive_only: Puuvaate kasutajaliides töötab ainult rekursiivsete seoste korral.
user:
user: Kasutaja
description: Vali olemasolev Directuse kasutaja
select_mode: Vali režiim
modes:
auto: Automaatne
dropdown: Rippmenüü
modal: Hüpikaken
input-rich-text-html:
wysiwyg: WYSIWYG
description: Tekstiredaktor HTML sisu loomiseks
toolbar: Tööriistariba
custom_formats: Kohandatud vormingud
options_override: Valikute ülekirjutamine
input-autocomplete-api:
input-autocomplete-api: Automaatselt täidetav väli (API)
description: Otsingutulemused välisest APIst
results_path: Tulemuste asukoht (path)
value_path: Väärtuse väli
trigger: Päästik
rate: Hinnang
displays:
boolean:
boolean: Boolean
description: Näita sees/väljas seisu
label_on: '"Sees" nimetus'
label_on_placeholder: Sisesta nimetus...
label_off: '"Väljas" nimetus'
label_off_placeholder: Sisesta nimetus...
icon_on: Ikoon sees
icon_off: Ikoon väljas
color_on: '"Sees" värv'
color_off: '"Väljas" värv'
collection:
collection: Kogud
description: Näita andmekogu
icon_label: Näita andmekogu ikooni
color:
color: Värv
description: Näita värvilist punkti
default_color: Vaikevärv
datetime:
datetime: Kuupäev-kellaaeg
description: Näita suhtelisi aegu
format: Vorming
format_note: >-
Ajaformaadi valikud __[Date Field Symbol Table](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)__
long: Pikk
short: Lühike
relative: Suhteline
relative_label: 'Näita suhtelist aega, nt 5 minutit tagasi'
file:
file: Fail
description: Näita faile
filesize:
filesize: Failisuurus
description: Näita faili suurust
formatted-value:
formatted-value: Vormistatud väärtus
description: Näita teksti vormistatud väärtust
format_title: Vorminda pealkiri
format_title_label: Paranda esitähed suureks
bold_label: Paksus kirjas
formatted-json-value:
formatted-json-value: Vormista JSON väärtusena
description: Näita objekti vormistatud versiooni
icon:
icon: Ikoon
description: Kuva ikooni
filled: Täidetud
filled_label: Kasuta täidetud varianti
image:
image: Pilt
description: Kasuta väikest pildi eelvaadet
circle: Ring
circle_label: Näita ümmarguselt
labels:
labels: Sildid
description: Näita kas üksikuna või siltide nimekirjana
default_foreground: Vaikimisi esiplaan
default_background: Vaikimisi taust
format_label: Vormista iga silt
show_as_dot: Näita punktina
choices_value_placeholder: Sisesta väärtus...
choices_text_placeholder: Sisesta tekst...
mime-type:
mime-type: MIME Tüüp
description: Näita faili MIME-tüüpi
extension_only: Ainult laiend
extension_only_label: Näita ainult faili laiendit
rating:
rating: Hinnang
description: Näita tähekestena
simple: Lihtne
simple_label: Näita tähekesi lihtsas formaadis
raw:
raw: Algne väärtus
related-values:
related-values: Seotud väärtused
description: Näita seotud väärtusi
user:
user: Kasutaja
description: Näita Directuse kasutajat
avatar: Avatar
name: Nimi
both: Mõlemad
circle_label: Näita kasutajat ringikesena
layouts:
cards:
cards: Kaardid
image_source: Pildi allikas
image_fit: Pildi mahutamine
crop: Kärpimine
contain: Sisaldab
title: Pealkiri
subtitle: Alampealkiri
tabular:
@@ -850,4 +1152,8 @@ layouts:
spacing: Vahed
comfortable: Mugav
compact: Kompaktne
cozy: Kodune
cozy: Tavaline
calendar:
calendar: Kalender
start_date_field: Alguse kuupäev
end_date_field: Lõpu kuupäev

View File

@@ -784,6 +784,9 @@ save_current_datetime: Tallenna nykyinen päivämäärä/aika
block: Estä
inline: Rivitetty
comment: Kommentti
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.

View File

@@ -1 +1,241 @@
---
edit_field: एडिट फील्ड
item_revision: आइटम संशोधन
duplicate_field: डुप्लिकेट फ़ील्ड
half_width: आधी चौड़ाई
full_width: पुरी चौड़ाई
fill_width: संपूर्ण चौड़ाई
field_name_translations: फ़ील्ड नाम अनुवाद
enter_password_to_enable_tfa: टू-फैक्टर ऑथेंटिकेशन को सक्षम करने के लिए अपना पासवर्ड दर्ज करें
add_field: नया क्षेत्र
role_name: भूमिका नाम
db_only_click_to_configure: 'केवल डेटाबेस: कॉन्फ़िगर करने के लिए क्लिक करें '
show_archived_items: आर्काइव आइटम दिखाएं
edited: डेटा संपादित
required: आवश्यक
required_for_app_access: ऐप एक्सेस के लिए आवश्यक
requires_value: डेटा की आवश्यकता है
create_preset: प्रीसेट बनाएं
create_role: भूमिका बनाएं
create_user: यूजर बनाएं
create_webhook: Webhook बनाएं
invite_users: यूज़र आमंत्रित करें
email_examples: 'admin@example.com, user@example.com...'
invite: आमंत्रित करें
email_already_invited: ईमेल "{email}" पहले ही आमंत्रित किया जा चुका है
emails: इमेल्स
connection_excellent: उत्तम कनेक्शन
connection_good: अच्छा कनेक्शन
connection_fair: ठीक ठाक कनेक्शन
connection_poor: खराब कनेक्शन
primary: प्राइमरी
rename_folder: रीनेम फोल्डर
delete_folder: डिलीट फोल्डर
prefix: प्रीफिक्स
suffix: सफिक्स
reset_bookmark: रीसेट बुकमार्क
rename_bookmark: रीनेम बुकमार्क
update_bookmark: अपडेट बुकमार्क
delete_bookmark: डिलीट बुकमार्क
delete_bookmark_copy: >-
क्या आप वाकई "{bookmark}" बुकमार्क हटाना चाहते हैं? इसको आप अनडू नहीं कर सकते
logoutReason:
SIGN_OUT: साइंड आउट
SESSION_EXPIRED: समय बीत गया
public: पब्लिक
public_description: यह नियंत्रित करता है कि authentication के बिना कौन सा API डेटा उपलब्ध है
not_allowed: अनुमति नहीं हैं
directus_version: Directus वर्शन
node_version: Node वर्शन
node_uptime: Node उप टाइम
os_type: OS टाइप
os_version: OS वर्शन
os_uptime: OS उप टाइम
os_totalmem: OS मेमोरी
archive: आर्काइव
archive_confirm: क्या आप वाकई इस आइटम को आर्काइव करना चाहते हैं?
archive_confirm_count: >-
कोई आइटम नहीं चुना गया | क्या आप वाकई इस आइटम को आर्काइव करना चाहते हैं? | क्या आप वाकई इन {count} आइटम्स को आर्काइव करना चाहते हैं?
reset_system_permissions_to: 'सिस्टम अनुमतियां रीसेट करें:'
reset_system_permissions_copy: यह क्रिया किसी भी कस्टम अनुमतियों को अधिलेखित कर देगी जिन्हें आपने सिस्टम संग्रह पर लागू किया होगा। क्या आप वास्तव में इसे करना चाहते हैं?
the_following_are_minimum_permissions: '"App Access" सक्षम होने पर निम्नलिखित न्यूनतम अनुमतियों की आवश्यकता होती है। आप इससे आगे की अनुमतियां बढ़ा सकते हैं, लेकिन नीचे नहीं।'
app_access_minimum: App Access न्यूनतम
recommended_defaults: अनुशंसित डिफ़ॉल्ट
unarchive: अनआर्काइव
unarchive_confirm: क्या आप वाकई इस आइटम को अनआर्काइव करना चाहते हैं?
nested_files_folders_will_be_moved: नेस्टेड फ़ाइलें और फ़ोल्डर एक स्तर ऊपर ले जाया जाएगा
unknown_validation_errors: 'निम्न छिपा क्षेत्रों में मान्यकरण त्रुटियों थे:'
validationError:
eq: मान {valid} होना चाहिए
neq: मान {invalid} नहीं हो सकता
in: मान {valid} में से एक होना चाहिए
nin: मान {invalid} में से एक नहीं हो सकता
contains: मान में {substring} शामिल होना चाहिए
ncontains: मान में {substring} नहीं हो सकता
gt: मान {valid} से ज़्यादा होना चाहिए
gte: मान {valid} से बड़ा या उसके बराबर होना चाहिए
lt: मान {valid} से कम होना चाहिए
lte: मान {valid} से कम या उसके बराबर होना चाहिए
empty: मान खाली होना चाहिए
nempty: मान खाली नहीं हो सकता
null: मान null होना चाहिए
nnull: मान null नहीं हो सकता
required: मान आवश्यक है
unique: मान अद्वितीय होना चाहिए
regex: मान का प्रारूप सही नहीं है
all_access: ऑल एक्सेस
no_access: नो एक्सेस
use_custom: कस्टम
nullable: Nullable
allow_null_value: NULL मान की अनुमति दें
enter_value_to_replace_nulls: इस क्षेत्र में वर्तमान में किसी भी NULL मान को बदलने के लिए कृपया एक नया मान दर्ज करें
field_standard: नार्मल फील्ड
field_presentation: प्रस्तुति और उपनाम
field_m2o: M2O रिलेशन
field_m2a: M2A रिलेशन
field_o2m: O2M रिलेशन
field_m2m: M2M रिलेशन
item_permissions: आइटम अनुमतियां
field_permissions: फील्ड अनुमतियाँ
field_validation: फील्ड मान्यकरण
field_presets: फील्ड प्रीसेट
permissions_for_role: 'आइटम्स जो {role} भूमिका {action} कर सकते हैं'
fields_for_role: 'फ़ील्ड्स जो {role} भूमिका {action} कर सकते हैं'
validation_for_role: 'फील्ड {action} नियमों जो {role} भूमिका पालन करना चाहिए'
presets_for_role: '{role} भूमिका के लिए फ़ील्ड मान जो डिफ़ॉल्ट होगा'
presentation_and_aliases: प्रस्तुति और उपनाम
revision_post_update: यहां देखें कि अपडेट के बाद यह आइटम कैसा दिखता है
language: भाषा
global: ग्लोबल
admins_have_all_permissions: Admins के पास सभी अनुमतियां हैं
camera: कैमरा
exposure: एक्सपोज़र
shutter: शटर
iso: ISO
focal_length: फोकल लेंथ
schema_setup_key: इस फ़ील्ड का डेटाबेस कॉलम नाम और API key
create_field: फ़ील्ड बनाएं
creating_new_field: 'नया फ़ील्ड ({collection})'
field_in_collection: '{field} ({collection})'
reset_page_preferences: पेज सेटिंग्स रिसेट करे
hidden_field: हिडन फील्ड
hidden_on_detail: विवरण पर हिडन
disabled_editing_value: डेटा संपादित करने से रोकें
key: Key
alias: Alias
bigInteger: Big Integer
boolean: Boolean
date: Date
datetime: DateTime
decimal: Decimal
float: Float
integer: Integer
json: JSON
xml: XML
string: String
text: Text
time: Time
timestamp: Timestamp
uuid: UUID
hash: Hash
date-fns_time: 'h:mm:ss a'
date-fns_time_no_seconds: 'h:mm a'
date-fns_date_short: 'MMM d, u'
date-fns_time_short: 'h:mma'
date-fns_date_short_no_year: MMM d
month: महीना
year: साल
select_all: सभी चुनें
months:
january: जनवरी
february: फ़रवरी
march: मार्च
april: अप्रैल
may: मई
june: जून
july: जुलाई
august: अगस्त
september: सितम्बर
october: अक्तूबर
november: नवम्बर
december: दिसंबर
url: URL
size: साइज़
owner: मालिक
download: डाउनलोड
name: नाम
csv: CSV
webhooks: Webhooks
sidebar: साइड बार
duration: अवधि
modified_on: संशोधन का समय
card_size: Card Size
sort_field: Sort Field
add_sort_field: Add Sort Field
sort: सॉर्ट
flip_horizontal: फ्लिप हॉरिजॉन्टल
one_item: '1 समान'
font: फ़ॉन्ट
sans_serif: Sans Serif
serif: Serif
monospace: Monospace
divider: Divider
color: Color
circle: Circle
operators:
eq: Equals
loading: लोड हो रहा है...
item: समान
columns: कॉलमस
value_unique: मान अद्वितीय होना चाहिए
all_files: सभी फाइलें
my_files: मेरी फ़ाइलें
recent_files: हाल की फ़ाइल
create_folder: फोल्डर बनाएं
folder_name: फ़ोल्डर का नाम
add_file: ऐड फाइल
replace_file: फ़ाइल बदलें
no_results: कोई परिणाम नहीं मिला
role: भूमिका
user: यूज़र
create: Create
read: Read
update: Update
wysiwyg_options:
selectall: सभी चुनें
settings_webhooks: Webhooks
fields:
directus_collections:
sort_field: Sort Field
directus_files:
modified_on: संशोधन का समय
duration: अवधि
directus_users:
language: भाषा
role: भूमिका
directus_fields:
translation: फ़ील्ड नाम अनुवाद
directus_roles:
name: भूमिका नाम
delete: Delete
interfaces:
presentation-links:
primary: प्राइमरी
select-color:
color: Color
presentation-divider:
divider: Divider
input-hash:
hash: Hash
user:
user: यूज़र
displays:
boolean:
boolean: Boolean
color:
color: Color
image:
circle: Circle
user:
user: यूज़र
name: नाम

View File

@@ -0,0 +1,508 @@
---
edit_field: Izmijeni
item_revision: Revizija stavke
duplicate_field: Dupliraj
half_width: Pola širine
full_width: Puna širina
fill_width: Popunjeno
enter_password_to_enable_tfa: Unesite vašu lozinku kako biste uključili dvostruku potvrdu autentičnosti
add_field: Dodaj Polje
role_name: Naziv uloge
db_only_click_to_configure: 'Samo za bazu podataka: Kliknite za Podešavanje '
show_archived_items: Prikaži arhivirane stavke
edited: Vrijednost izmijenjena
required: Obavezno
required_for_app_access: Obavezno za pristup aplikaciji
requires_value: Zahtijeva vrijednost
create_role: Napravi ulogu
create_user: Napravi korisnika
invite_users: Pozovi korisnike
invite: Pozovi
email_already_invited: Na ovu "{email}" adresu je već poslat poziv
emails: Email adrese
connection_excellent: Odlična konekcija
connection_good: Dobra konekcija
connection_fair: Solidna konekcija
connection_poor: Loša konekcija
primary: Primarni
rename_folder: Preimenuj Fasciklu
delete_folder: Obriši Fasciklu
prefix: Prefiks
suffix: Sufiks
reset_bookmark: Poništi postavljanje oznake
rename_bookmark: Preimenuj oznaku
update_bookmark: Ažuriraj oznaku
delete_bookmark: Obriši oznaku
delete_bookmark_copy: >-
Da li ste sigurni da želite da obrišete "{bookmark}" oznaku? Ova operacija je trajna.
logoutReason:
SIGN_OUT: Odjavljen
SESSION_EXPIRED: Sesija je istekla
public: Javno
public_description: Kontroliše koji podaci sa API su dostupni bez potrebe za provjerom autentičnosti korisnika.
not_allowed: Nije dozvoljeno
directus_version: Directus verzija
archive: Arhiviraj
archive_confirm: Da li ste sigurni da želite da arhivirate ovu stavku?
archive_confirm_count: >-
Nema odabranih stavki | Da li ste sigurni da želite da arhivirate ovu stavku? | Da li ste sigurni da želite da arhivirate ovih {count} stavki?
reset_system_permissions_to: 'Poništi Sistemske Dozvole na:'
reset_system_permissions_copy: Ova operacija će prepisati sve prilagođene dozvole koje ste primjenili na sistemske kolekcije. Da li ste sigurni?
the_following_are_minimum_permissions: Sljedeće su minimalne dozvole koje su potrebne kada je omogućen "Pristup Aplikaciji". Možete proširiti dozvole i izvan ovoga, ali ne i ispod.
app_access_minimum: Minimalan pristup aplikaciji
recommended_defaults: Preporučene postavke
unarchive: Vrati iz arhive
unarchive_confirm: Da li ste sigurni da želite da vratite iz arhive ovu stavku?
nested_files_folders_will_be_moved: Ugnježđeni fajlovi i fascikle će biti pomjereni na jedan nivo iznad.
unknown_validation_errors: 'Došlo je do greške u validaciji kod sljedećih sakrivenih polja:'
validationError:
eq: Vrijednost treba biti {valid}
neq: Vrijednost ne može biti {invalid}
in: Vrijednost mora da bude jedna od {valid}
nin: Vrijednost ne može biti nijedna od {invalid}
contains: Vrijednost mora da sadrži {substring}
ncontains: Vrijednost ne može da sadrži {substring}
gt: Vrijednost mora da bude veća od {valid}
gte: Vrijednost mora da bude veća ili jednaka {valid}
lt: Vrijednost mora da bude manja od {valid}
lte: Vrijednost mora da bude manja ili jednaka {valid}
empty: Vrijednost mora biti prazna
nempty: Vrijednost ne može biti prazna
null: Vrijednost mora biti null
nnull: Vrijednost ne može biti null
required: Vrijednost je obavezna
unique: Vrijednost mora biti jedinstvena
regex: Vrijednost ne sadrži ispravan format
all_access: Potpun Pristup
no_access: Bez Pristupa
allow_null_value: Dozvoli NULL vrijednost
enter_value_to_replace_nulls: Molimo unesite novu vrijednost kako biste zamijenili sve NULL vrijednosti u sklopu ovog polja.
field_standard: Uobičajeno
field_file: Single File
field_files: Multiple Files
field_m2o: M2O Relacija
field_m2a: M2A Relacija
field_o2m: O2M Relacija
field_m2m: M2M Relacija
field_translations: Prevodi
item_permissions: Dozvole na stavkama
field_permissions: Dozvole na Poljima
field_validation: Validacija Polja
permissions_for_role: 'Sve stavke {role} Role mogu {action}.'
fields_for_role: 'Sva polja {role} Role mogu {action}.'
revision_post_update: Ovako će ova stavka izgledati nakon ažuriranja...
changes_made: Postoje specifične izmjene koji su napravljene...
no_relational_data: Zapamtite da ovo ne uključuje relacione podatke.
hide_field_on_detail: Sakrij Polje u sekciji Detalji
show_field_on_detail: Prikaži Polje u sekciji Detalji
delete_field: Obriši Polje
fields_and_layout: Polja & Izgled
field_create_success: 'Kreirano Polje: "{field}"'
field_update_success: 'Ažurirano Polje: "{field}"'
duplicate_where_to: U koju tabelu želite da smjestite ovu dupliranu kolonu?
language: Jezik
global: Globalno
admins_have_all_permissions: Administratori imaju sve dozvole
camera: Kamera
exposure: Ekspozicija
shutter: Okidač
iso: ISO
create_field: Kreiraj Polje
creating_new_field: 'Novo Polje ({collection})'
field_in_collection: '{field} ({collection})'
reset_page_preferences: Resetuj Podešavanja Stranice
hidden_field: Sakriveno Polje
hidden_on_detail: Sakriveno u sekciji Detalji
key: Ključ
alias: Alias
bigInteger: Big Integer
boolean: Boolean
date: Date
datetime: DateTime
decimal: Decimal
float: Float
integer: Integer
json: JSON
xml: XML
string: String
text: Text
time: Time
timestamp: Timestamp
uuid: UUID
hash: Hash
not_available_for_type: Nije Dostupno za ovaj Tip
create_translations: Kreiraj Prevode
auto_refresh: Automatsko osvjеžavanjе
refresh_interval: Period Osvježavanja
refresh_interval_seconds: Trenutno Osvježavanje | Svake Sekunde | Svakih {seconds} Sekundi
refresh_interval_minutes: Svake Minute | Svakih {minutes} Minuta
auto_generate: Automatsko generisanje
this_will_auto_setup_fields_relations: Ovo će automatski podesiti sva obavezna polja i relacije.
click_here: Klikni ovdje
to_manually_setup_translations: za ručno podešavanje prevoda.
fields_group: Grupe Polja
no_collections_found: Kolekcije nisu pronađene.
new_data_alert: 'Sljedeće će biti kreirano u sklopu Modela Podataka:'
search_collection: Pretraži Kolekciju...
new_field: 'Novo Polje'
new_collection: 'Nova Kolekcija'
choose_a_type: Izaberi Tip...
determined_by_relationship: Određeno Relacijom
add_note: Dodaj korisnu napomenu korisnicima...
default_value: Podrazumijevana Vrijednost
standard_field: Standardno Polje
single_file: Single File
multiple_files: Multiple Files
invalid_item: Nevažeća Stavka
next: Sljedeća
field_name: Naziv Polja
translations: Prevodi
note: Napomena
enter_a_value: Unesi vrijednost...
length: Dužina
unique: Jedinstveno
updated_on: Ažurirano
updated_by: Ažurirano od strane
finish_setup: Završi podešavanje
dismiss: Ignoriši
clear_value: Obriši vrijednost
reset_to_default: Vrati na podrazumijevano
undo_changes: Poništi izmjene
notifications: Obavještenja
show_all_activity: Prikaži svu aktivnost
page_not_found: Stranica nijе pronađеna
page_not_found_body: Stranica koju tražite nije moguće pronaći.
confirm_revert: Potvrdi Vraćanje
display: Prikaz
settings_update_success: Podešavanja ažurirana
title: Naslov
revision_delta_created: Kreiran
revision_delta_updated: 'Ažurirano 1 Polje | Ažuirano {count} Polja'
revision_delta_deleted: Obrisano
revision_delta_reverted: Vraćeno na staro
revision_delta_by: '{date} po {user}'
private_user: Privatni Korisnik
updates_made: Napravljena Ažuriranja
leave_comment: Ostavi komentar...
post_comment_success: Komentar objavljen
item_create_success: Stavka Kreirana | Stavke Kreirane
item_update_success: Stavka Ažurirana | Stavke Ažurirane
item_delete_success: Stavka Obrisana | Stavke Obrisane
this_collection: Ova Kolekcija
related_collections: Povezane Kolekcije
languages_collection: Jezička Kolekcija
export_data: Izvoz Podataka
format: Format
use_current_filters_settings: Koristi Trenutne Filtere & Podešavanja
export_collection: 'Izvoz {collection}'
last_page: Posljednja Stranica
last_access: Posljednji Pristup
fill_template: Popuni sa vrijednošću šablona
a_unique_table_name: Jedinstven naziv tabele...
a_unique_column_name: Jedinstven naziv kolone...
enable_custom_values: Omogući prilagođene vrijednosti
submit: Pošalji
move_to_folder: Prebaci u fasciklu
move: Premjesti
system: Sistem
interface: Interfejs
today: Danas
yesterday: Juče
delete_comment: Obriši komentar
date-fns_date: PPP
date-fns_time: 'h:mm:ss a'
date-fns_time_no_seconds: 'h:mm a'
date-fns_date_short: 'MMM d, u'
date-fns_time_short: 'h:mma'
date-fns_date_short_no_year: MMM d
month: Mjesec
year: Godina
select_all: Odaberi Sve
months:
january: Januar
february: Februar
march: Mart
april: April
may: Maj
june: Jun
july: Jul
august: Avgust
september: Septembar
october: Oktobar
november: Novembar
december: Decembar
drag_mode: Režim Prevlačenja
cancel_crop: Poništi Isjecanje
original: Originalno
url: URL adresa
import: Uvezi
file_details: Detalji Fajla
dimensions: Dimenzije
size: Veličina
created: Kreiran
modified: Izmijenjeno
checksum: Kontrolni zbir
owner: Vlasnik
edited_by: Izmijenjeno od
folder: Fascikla
zoom: Uvеćanjе
download: Preuzmi
open: Otvori
open_in_new_window: Otvori u Novom Prozoru
upload_from_device: Otpremi Fajl sa Uređaja
choose_from_library: Odaberi Fajl iz Biblioteke
import_from_url: Uvezi Fajl sa URL adrese
replace_from_device: Zamijeni Fajl iz Uređaja
replace_from_library: Zamijeni Fajl iz Biblioteke
no_file_selected: Nema odabranih fajlova
download_file: Preuzmi Fajl
collection_key: Ključ Kolekcije
name: Ime
type: Tip
creating_new_collection: Kreiranje Nove Kolekcije
created_by: Kreirao
created_on: Kreirano
generated_uuid: Kreiran UUID
save_and_create_new: Sačuvaj i Napravi Novi
save_and_stay: Sačuvaj i Ostani
save_as_copy: Sačuvaj kao Kopiju
add_existing: Dodaj Postojeći
creating_items: Kreiranje stavki
enable_create_button: Omogući Dugme za Kreiranje
selecting_items: Odabir stavki
enable_select_button: Omogući Dugme za Odabir
comments: Komentari
no_comments: Nema komentara
click_to_expand: Klikni za proširivanje
select_item: Odaberi Stavku
no_items: Nema stavki
search_items: Pretraži Stavke...
disabled: Isključeno
information: Informacija
report_bug: Prijavi Propust
request_feature: Zatraži novu funkcionalnost
interface_not_found: 'Interfejs "{interface}" nije pronađen.'
reset_interface: Resetuj Interfejs
display_not_found: 'Prikaz "{display}" nije pronađen.'
reset_display: Resetuj Prikaz
item_count: 'Nema Stavki | Jedna Stavka | {count} stavki'
no_items_copy: Trenutno ne postoji nijedna stavka u ovoj kolekciji.
file_count: 'Nema Fajlova | Jedan Fajl | {count} Fajlova'
no_files_copy: Ovdje nema fajlova.
user_count: 'Nema Korisnika | Jedan Korisnik | {count} Korisnika'
no_users_copy: Ne postoji nijedan korisnik u ovoj roli.
all_items: Sve Stavke
csv: CSV
no_collections: Nema Kolekcija
create_collection: Napravi Kolekciju
no_collections_copy_admin: Trenutno nemate Kolekcija. Kliknite na dugme ispod da započnete dodavanje.
no_collections_copy: Trenutno nemate Kolekcija. Molimo kontaktirajte Vašeg sistemskog administratora.
relationship_not_setup: Relacija nije konfigurisana ispravno
display_template_not_setup: Šablon za grafički prikaz nije ispravno konfigurisan
collection_field_not_setup: Polje u kolekciji nije ispravno konfigurisano
select_a_collection: Izaberi Kolekciju
users: Korisnici
activity: Aktivnost
field_width: Širina Polja
add_filter: Dodaj Filter
upper_limit: Gornja granica...
lower_limit: Donja granica...
user_directory: Korisnički Direktorijum
documentation: Dokumentacija
sidebar: Bočna traka
duration: Trajanje
charset: Set karaktera
file_moved: Fajl je premješten
collection_created: Kolekcija Napravljena
modified_on: Izmijenjeno
card_size: Veličina Kartice
sort_field: Polje za Sortiranje
add_sort_field: Dodaj Polje za Sortiranje
sort: Sortiranje
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.
bookmark_doesnt_exist_cta: Povratak na kolekciju
select_an_item: Odaberi stavku...
edit: Izmijeni
enabled: Omogućen
disable_tfa: Isključi 2FA
enter_otp_to_disable_tfa: Unesite OTP kako biste isključili 2FA
create_account: Napravi Korisnički nalog
account_created_successfully: Korisnički nalog je uspješno kreiran
auto_fill: Automatsko popunjavanje
corresponding_field: Odgovarajuće polje
errors:
COLLECTION_NOT_FOUND: "Kolekcija ne postoji"
FIELD_NOT_FOUND: Polje nije pronađeno
FORBIDDEN: Zabranjeno
INVALID_CREDENTIALS: Pogrešno korisničko ime ili lozinka
INVALID_OTP: Pogrešna jednokratna lozinka
ITEM_NOT_FOUND: Stavka nije pronađena
ROUTE_NOT_FOUND: Nije pronađeno
RECORD_NOT_UNIQUE: Detektovana je dupla vrijednost
USER_SUSPENDED: Suspendovan Korisnik
CONTAINS_NULL_VALUES: Polje sadrži null vrijednosti
UNKNOWN: Neočekivana greška
INTERNAL_SERVER_ERROR: Neočekivana greška
bookmark_name: Naziv oznake...
create_bookmark: Napravi Oznaku
edit_bookmark: Ažuriraj Oznaku
bookmarks: Oznake
unexpected_error: Neočekivana greška
unexpected_error_copy: Desila se neočekivana greška. Molimo pokušajte kasnije.
copy_details: Kopiraj Detalje
no_app_access: Bez pristupa Aplikaciji
no_app_access_copy: Ovom korisniku nije dozvoljeno da koristi administratorsku aplikaciju.
password_reset_sent: Poslali smo Vam sigurnosni link za resetovanje lozinke
password_reset_successful: Lozinka je uspješno resetovana
back: Nazad
editing_image: Uređivanje Fotografije
square: Kvadrat
free: Slobodan
flip_horizontal: Okreni Horizontalno
flip_vertical: Okreni Vertikalno
rotate: Rotiraj
all_users: Svi korisnici
delete_collection: Obriši Kolekciju
update_collection_success: Ažurirana Kolekcija
delete_collection_success: Obrisana Kolekcija
start_end_of_count_items: '{start}-{end} od {count} stavki'
start_end_of_count_filtered_items: '{start}-{end} od {count} filtriranih stavki'
one_item: '1 Stavka'
one_filtered_item: '1 Filtrirana Stavka'
delete_collection_are_you_sure: >-
Da li ste sigurni da želite da obrišete ovu kolekciju? Ova operacija će obrisati kolekciju i sve stavke unutar iste. Ova operacija je trajna.
collections_shown: Prikazano Kolekcija
visible_collections: Vidljive Kolekcije
hidden_collections: Sakrivene Kolekcije
show_hidden_collections: Prikaži Sakrivene Kolekcije
hide_hidden_collections: Sakrij Sakrivene Kolekcije
unmanaged_collections: Nekonfigurisane Kolekcije
system_collections: Sistemske Kolekcije
icon_left: Ikonica Lijevo
icon_right: Ikonica Desno
count_other_revisions: '{count} Ostalih Revizija'
font: Font
sans_serif: Sans Serif
serif: Serif
color: Boja
circle: Krug
empty_item: Prazna Stavka
log_in_with: 'Prijavite se sa {provider}'
advanced_filter: Napredni Filter
delete_advanced_filter: Obriši Filter
operators:
eq: Jednako
neq: Nije jednako
lt: Manje od
gt: Veće od
lte: Manje ili jednako od
gte: Veće ili jednako od
in: Je jedan od
nin: Nijedan od
null: Je null
nnull: Nije null
contains: Sadrži
ncontains: Ne sadrži
between: Je između
nbetween: Nije između
empty: Je Prazno
nempty: Nije Prazno
all: Sadrži sljedeće ključeve
has: Sadrži neke od sljedećih ključeva
loading: Učitavanje...
drop_to_upload: Prenesi za Otpremanje
item: Stavka
items: Stavke
upload_file_indeterminate: Otpremanje Fajla...
click_to_browse: Klikni za Pretraživanje
layout_options: Opcije Izgleda
rows: Redova
columns: Kolona
collection_setup: Podešavanje Kolekcije
optional_system_fields: Neobavezna Sistemska Polja
value_unique: Vrijednost mora biti jedinstvena
all_activity: Sva Aktivnost
create_item: Napravi Stavku
display_template: Šablon za Prikaz
language_display_template: Šablon za prikaz jezika
translations_display_template: Šablon za prikaz Prevoda
n_items_selected: 'Nema Odabranih Stavki | 1 Stavka Odabrana | {n} Stavki Odabrano'
per_page: Po Stranici
all_files: Svi Fajlovi
my_files: Moji Fajlovi
recent_files: Skorašnji Fajlovi
create_folder: Napravi Fasciklu
folder_name: Naziv Fascikle...
add_file: Dodaj Fajl
replace_file: Zamijeni Fajl
no_results: Nema rezultata
no_results_copy: Prilagodi ili obriši filtere pretrage kako biste vidjeli rezultate.
wysiwyg_options:
selectall: Odaberi Sve
settings_project: Podešavanje Projekta
fields:
directus_collections:
note: Napomena
display_template: Šablon za Prikaz
sort_field: Polje za Sortiranje
directus_files:
title: Naslov
modified_on: Izmijenjeno
created_on: Kreirano
created_by: Kreirao
folder: Fascikla
charset: Set karaktera
duration: Trajanje
directus_users:
title: Naslov
language: Jezik
last_page: Posljednja Stranica
last_access: Posljednji Pristup
directus_settings:
project_name: Ime Projekta
project_color: Boja Projekta
project_logo: Logotip Projekta
public_note: Opis Projekta
directus_fields:
note: Napomena
directus_roles:
name: Naziv uloge
interfaces:
presentation-links:
primary: Primarni
select-color:
color: Boja
system-display-template:
display-template: Šablon za Prikaz
collection_field_not_setup: Polje u kolekciji nije ispravno konfigurisano
select_a_collection: Izaberi Kolekciju
select-dropdown:
choices_value_placeholder: Unesi vrijednost...
input-hash:
hash: Hash
system-interface:
interface: Interfejs
select-dropdown-m2o:
display_template: Šablon za Prikaz
boolean:
label_default: Omogućen
translations:
display_template: Šablon za Prikaz
displays:
boolean:
boolean: Boolean
color:
color: Boja
datetime:
format: Format
image:
circle: Krug
labels:
choices_value_placeholder: Unesi vrijednost...
user:
name: Ime
layouts:
cards:
title: Naslov

View File

@@ -11,7 +11,7 @@
v-if="(group.name === undefined || group.name === null) && group.accordion === 'always_open' && index === 0"
>
<v-list-item :exact="exact" v-for="navItem in group.items" :key="navItem.to" :to="navItem.to">
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
<v-list-item-icon><v-icon :name="navItem.icon" :color="navItem.color" /></v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="navItem.name" />
</v-list-item-content>
@@ -27,7 +27,7 @@
@toggle="toggleActive(group.name)"
>
<v-list-item :exact="exact" v-for="navItem in group.items" :key="navItem.to" :to="navItem.to">
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
<v-list-item-icon><v-icon :name="navItem.icon" :color="navItem.color" /></v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="navItem.name" />
</v-list-item-content>
@@ -38,7 +38,7 @@
</template>
<v-list-item v-else :exact="exact" v-for="navItem in navItems" :key="navItem.to" :to="navItem.to">
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
<v-list-item-icon><v-icon :name="navItem.icon" :color="navItem.color" /></v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="navItem.name" />
</v-list-item-content>
@@ -72,7 +72,7 @@
:key="navItem.to"
:to="navItem.to"
>
<v-list-item-icon><v-icon :name="navItem.icon" /></v-list-item-icon>
<v-list-item-icon><v-icon :name="navItem.icon" :color="navItem.color" /></v-list-item-icon>
<v-list-item-content>
<v-text-overflow :text="navItem.name" />
</v-list-item-content>

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