mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge remote-tracking branch 'origin/main' into content-versioning-fix
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@directus/api": patch
|
||||
---
|
||||
|
||||
Updated dependency tar-fs
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
'@directus/storage-driver-cloudinary': patch
|
||||
'@directus/release-notes-generator': patch
|
||||
'@directus/storage-driver-supabase': patch
|
||||
'@directus/storage-driver-azure': patch
|
||||
'@directus/storage-driver-local': patch
|
||||
'@directus/storage-driver-gcs': patch
|
||||
'@directus/storage-driver-s3': patch
|
||||
'@directus/update-check': patch
|
||||
'@directus/system-data': patch
|
||||
'@directus/validation': patch
|
||||
'@directus/schema': patch
|
||||
'@directus/specs': patch
|
||||
'@directus/sdk': patch
|
||||
---
|
||||
|
||||
Upgraded dependencies
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'create-directus-project': patch
|
||||
---
|
||||
|
||||
Upgrade dependencies
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/api': patch
|
||||
---
|
||||
|
||||
Fixed admin users email not trimmed on project initialization
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/extensions-registry': patch
|
||||
---
|
||||
|
||||
Upgraded dependencies
|
||||
5
.changeset/khaki-carrots-tie.md
Normal file
5
.changeset/khaki-carrots-tie.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@directus/api": patch
|
||||
---
|
||||
|
||||
Updated nodemailer to us AWS SESv2
|
||||
5
.changeset/moody-facts-kick.md
Normal file
5
.changeset/moody-facts-kick.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Hide "Create new" and "Add existing" buttons on o2m fields with unique constraint
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed an issue that would cause the code editor interface to fail when the language prop was set to null
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
'@directus/extensions-sdk': major
|
||||
'@directus/storage-driver-cloudinary': patch
|
||||
'@directus/storage-driver-supabase': patch
|
||||
'@directus/storage-driver-azure': patch
|
||||
'@directus/storage-driver-local': patch
|
||||
'@directus/extensions-registry': patch
|
||||
'@directus/storage-driver-gcs': patch
|
||||
'@directus/storage-driver-s3': patch
|
||||
'@directus/composables': patch
|
||||
'@directus/extensions': patch
|
||||
'@directus/constants': patch
|
||||
'@directus/storage': patch
|
||||
'@directus/errors': patch
|
||||
'@directus/themes': patch
|
||||
'@directus/types': patch
|
||||
'@directus/api': patch
|
||||
'@directus/app': patch
|
||||
'@directus/sdk': patch
|
||||
---
|
||||
|
||||
Added TypeScript support for services within the extension context
|
||||
|
||||
::: notice
|
||||
|
||||
The services exposed to API extensions using TypeScript are now fully typed instead of `any`, which may cause new type errors when building extensions.
|
||||
|
||||
Arguments of service methods are now strictly typed, which can result in type errors for broader types that would not error before:
|
||||
- The ItemsService constructor now expects the collection name to be a `string` and will error on `string | undefined` (or other unions).
|
||||
- Similarly, functions like `service.readOne()`/`service.readMany()` now expect `string | number` for their primary keys and will error for nullable types
|
||||
|
||||
As a workaround, casting the services back to `any` will result in the original behavior. However, it is recommended to resolve the type errors instead.
|
||||
|
||||
:::
|
||||
5
.changeset/rare-icons-check.md
Normal file
5
.changeset/rare-icons-check.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Added currentItem id check to prevent in-flight api call from returning stale data
|
||||
5
.changeset/real-kings-learn.md
Normal file
5
.changeset/real-kings-learn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Added RTL support for popper context menu
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
'@directus/composables': patch
|
||||
'@directus/extensions': patch
|
||||
'@directus/pressure': patch
|
||||
'@directus/memory': patch
|
||||
'@directus/stores': patch
|
||||
'@directus/themes': patch
|
||||
'@directus/types': patch
|
||||
'@directus/utils': patch
|
||||
'@directus/api': patch
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Upgraded dependencies
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/extensions-sdk': patch
|
||||
---
|
||||
|
||||
Upgraded dependencies
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/errors': patch
|
||||
---
|
||||
|
||||
Moved dependency
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@directus/api': patch
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Updated dependency form-data
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/api': minor
|
||||
---
|
||||
|
||||
Added the ability to override the email `from` property
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Standardized batch mode for raw group fields
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@directus/env': patch
|
||||
---
|
||||
|
||||
Upgrade dependencies
|
||||
5
.changeset/yummy-tools-decide.md
Normal file
5
.changeset/yummy-tools-decide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/sdk': patch
|
||||
---
|
||||
|
||||
Fixed auth being cleared before `login`/`refresh` request succeeds
|
||||
4
.github/dependabot.yaml
vendored
4
.github/dependabot.yaml
vendored
@@ -44,7 +44,7 @@ updates:
|
||||
timezone: 'Etc/UTC'
|
||||
open-pull-requests-limit: 3
|
||||
reviewers:
|
||||
- 'directus/maintainers'
|
||||
- 'directus/platform'
|
||||
labels:
|
||||
- 'Dependency'
|
||||
commit-message:
|
||||
@@ -61,7 +61,7 @@ updates:
|
||||
timezone: 'Etc/UTC'
|
||||
open-pull-requests-limit: 2
|
||||
reviewers:
|
||||
- 'directus/maintainers'
|
||||
- 'directus/platform'
|
||||
labels:
|
||||
- 'Dependency'
|
||||
commit-message:
|
||||
|
||||
6
.github/workflows/check.yml
vendored
6
.github/workflows/check.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
uses: tj-actions/changed-files@v46
|
||||
|
||||
- name: Prepare
|
||||
uses: ./.github/actions/prepare
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
uses: tj-actions/changed-files@v46
|
||||
|
||||
- name: Prepare
|
||||
uses: ./.github/actions/prepare
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
uses: tj-actions/changed-files@v46
|
||||
|
||||
- name: Prepare
|
||||
uses: ./.github/actions/prepare
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/api",
|
||||
"version": "29.0.0",
|
||||
"version": "29.1.1",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -67,7 +67,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@authenio/samlify-node-xmllint": "catalog:",
|
||||
"@aws-sdk/client-ses": "catalog:",
|
||||
"@aws-sdk/client-sesv2": "catalog:",
|
||||
"@directus/app": "workspace:*",
|
||||
"@directus/constants": "workspace:*",
|
||||
"@directus/env": "workspace:*",
|
||||
|
||||
@@ -50,9 +50,19 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
||||
const env = useEnv();
|
||||
const logger = useLogger();
|
||||
|
||||
const { issuerUrl, clientId, clientSecret, provider, issuerDiscoveryMustSucceed } = config;
|
||||
const {
|
||||
issuerUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
clientPrivateKeys,
|
||||
clientTokenEndpointAuthMethod,
|
||||
provider,
|
||||
issuerDiscoveryMustSucceed,
|
||||
} = config;
|
||||
|
||||
if (!issuerUrl || !clientId || !clientSecret || !provider) {
|
||||
const isPrivateKeyJwtAuthMethod = clientTokenEndpointAuthMethod === 'private_key_jwt';
|
||||
|
||||
if (!issuerUrl || !clientId || !(clientSecret || (isPrivateKeyJwtAuthMethod && clientPrivateKeys)) || !provider) {
|
||||
logger.error('Invalid provider config');
|
||||
throw new InvalidProviderConfigError({ provider });
|
||||
}
|
||||
@@ -100,7 +110,11 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
||||
if (this.client) return this.client;
|
||||
|
||||
const logger = useLogger();
|
||||
const { issuerUrl, clientId, clientSecret, provider } = this.config;
|
||||
|
||||
const { issuerUrl, clientId, clientSecret, clientPrivateKeys, clientTokenEndpointAuthMethod, provider } =
|
||||
this.config;
|
||||
|
||||
const isPrivateKeyJwtAuthMethod = clientTokenEndpointAuthMethod === 'private_key_jwt';
|
||||
|
||||
// extract client http overrides/options
|
||||
const clientHttpOptions = getConfigFromEnv(`AUTH_${provider.toUpperCase()}_CLIENT_HTTP_`);
|
||||
@@ -127,18 +141,25 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
||||
|
||||
// extract client overrides/options excluding CLIENT_ID and CLIENT_SECRET as they are passed directly
|
||||
const clientOptionsOverrides = getConfigFromEnv(`AUTH_${provider.toUpperCase()}_CLIENT_`, {
|
||||
omitKey: [`AUTH_${provider.toUpperCase()}_CLIENT_ID`, `AUTH_${provider.toUpperCase()}_CLIENT_SECRET`],
|
||||
omitKey: [
|
||||
`AUTH_${provider.toUpperCase()}_CLIENT_ID`,
|
||||
`AUTH_${provider.toUpperCase()}_CLIENT_SECRET`,
|
||||
`AUTH_${provider.toUpperCase()}_CLIENT_PRIVATE_KEYS`,
|
||||
],
|
||||
omitPrefix: [`AUTH_${provider.toUpperCase()}_CLIENT_HTTP_`],
|
||||
type: 'underscore',
|
||||
});
|
||||
|
||||
const client = new issuer.Client({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uris: [this.redirectUrl],
|
||||
response_types: ['code'],
|
||||
...clientOptionsOverrides,
|
||||
});
|
||||
const client = new issuer.Client(
|
||||
{
|
||||
client_id: clientId,
|
||||
...(!isPrivateKeyJwtAuthMethod && { client_secret: clientSecret }),
|
||||
redirect_uris: [this.redirectUrl],
|
||||
response_types: ['code'],
|
||||
...clientOptionsOverrides,
|
||||
},
|
||||
isPrivateKeyJwtAuthMethod ? { keys: clientPrivateKeys } : undefined,
|
||||
);
|
||||
|
||||
if (clientHttpOptions) {
|
||||
client[custom.http_options] = (_, options) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { loadExtensions } from './load-extensions.js';
|
||||
|
||||
export async function createCli(): Promise<Command> {
|
||||
const program = new Command();
|
||||
program.allowExcessArguments();
|
||||
|
||||
await loadExtensions();
|
||||
|
||||
|
||||
85
api/src/mailer.test.ts
Normal file
85
api/src/mailer.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import getMailer from './mailer.js';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('@directus/env');
|
||||
vi.mock('./utils/get-config-from-env.js');
|
||||
|
||||
// Mock useEnv
|
||||
const mockUseEnv = vi.fn();
|
||||
vi.mocked(await import('@directus/env')).useEnv = mockUseEnv;
|
||||
|
||||
// Mock getConfigFromEnv
|
||||
const mockGetConfigFromEnv = vi.fn();
|
||||
vi.mocked(await import('./utils/get-config-from-env.js')).getConfigFromEnv = mockGetConfigFromEnv;
|
||||
|
||||
describe('getMailer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset the module to clear any cached transporter
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should not throw when creating SES transport', () => {
|
||||
mockUseEnv.mockReturnValue({
|
||||
EMAIL_TRANSPORT: 'ses',
|
||||
});
|
||||
|
||||
mockGetConfigFromEnv.mockReturnValue({
|
||||
region: 'us-east-1',
|
||||
credentials: {
|
||||
accessKeyId: 'access',
|
||||
secretAccessKey: 'secret',
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => getMailer()).not.toThrow();
|
||||
});
|
||||
|
||||
test('should not throw when creating sendmail transport', () => {
|
||||
mockUseEnv.mockReturnValue({
|
||||
EMAIL_TRANSPORT: 'sendmail',
|
||||
});
|
||||
|
||||
mockGetConfigFromEnv.mockReturnValue({
|
||||
newLine: 'unix',
|
||||
path: '/usr/sbin/sendmail',
|
||||
});
|
||||
|
||||
expect(() => getMailer()).not.toThrow();
|
||||
});
|
||||
|
||||
test('should not throw when creating SMTP transport', () => {
|
||||
mockUseEnv.mockReturnValue({
|
||||
EMAIL_TRANSPORT: 'smtp',
|
||||
});
|
||||
|
||||
mockGetConfigFromEnv.mockReturnValue({
|
||||
host: '0.0.0.0',
|
||||
port: '123',
|
||||
user: 'me',
|
||||
password: 'safe',
|
||||
name: 'test',
|
||||
});
|
||||
|
||||
expect(() => getMailer()).not.toThrow();
|
||||
});
|
||||
|
||||
test('should not throw when creating Mailgun transport', () => {
|
||||
mockUseEnv.mockReturnValue({
|
||||
EMAIL_TRANSPORT: 'mailgun',
|
||||
});
|
||||
|
||||
mockGetConfigFromEnv.mockReturnValue({
|
||||
apiKey: 'test',
|
||||
domain: 'test',
|
||||
host: 'api.mailgun.net',
|
||||
});
|
||||
|
||||
expect(() => getMailer()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -25,14 +25,14 @@ export default function getMailer(): Transporter {
|
||||
path: (env['EMAIL_SENDMAIL_PATH'] as string) || '/usr/sbin/sendmail',
|
||||
});
|
||||
} else if (transportName === 'ses') {
|
||||
const aws = require('@aws-sdk/client-ses');
|
||||
const { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-sesv2');
|
||||
|
||||
const sesOptions: Record<string, unknown> = getConfigFromEnv('EMAIL_SES_');
|
||||
|
||||
const ses = new aws.SES(sesOptions);
|
||||
const sesClient = new SESv2Client(sesOptions);
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
SES: { ses, aws },
|
||||
SES: { sesClient, SendEmailCommand },
|
||||
} as Record<string, unknown>);
|
||||
} else if (transportName === 'smtp') {
|
||||
let auth: boolean | { user?: string; pass?: string } = false;
|
||||
|
||||
79
api/src/operations/throw-error/index.test.ts
Normal file
79
api/src/operations/throw-error/index.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { InternalServerError } from '@directus/errors';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import config from './index.js';
|
||||
|
||||
const DEFAULT_ERROR = new InternalServerError();
|
||||
|
||||
describe('Operations / Throw Error', () => {
|
||||
test('Throws error with default values', () => {
|
||||
expect.assertions(3);
|
||||
|
||||
try {
|
||||
config.handler({} as any, {} as any);
|
||||
} catch (err: any) {
|
||||
expect(err.code).toBe(DEFAULT_ERROR.code);
|
||||
expect(err.status).toBe(DEFAULT_ERROR.status);
|
||||
expect(err.message).toBe(DEFAULT_ERROR.message);
|
||||
}
|
||||
});
|
||||
|
||||
test('Throws error with custom code', () => {
|
||||
expect.assertions(3);
|
||||
|
||||
try {
|
||||
config.handler({ code: 'CUSTOM_ERROR' } as any, {} as any);
|
||||
} catch (err: any) {
|
||||
expect(err.code).toBe('CUSTOM_ERROR');
|
||||
expect(err.status).toBe(DEFAULT_ERROR.status);
|
||||
expect(err.message).toBe(DEFAULT_ERROR.message);
|
||||
}
|
||||
});
|
||||
|
||||
test('Throws error with custom status', () => {
|
||||
expect.assertions(3);
|
||||
|
||||
try {
|
||||
config.handler({ status: '404' } as any, {} as any);
|
||||
} catch (err: any) {
|
||||
expect(err.code).toBe(DEFAULT_ERROR.code);
|
||||
expect(err.status).toBe(404);
|
||||
expect(err.message).toBe('An unexpected error occurred.');
|
||||
}
|
||||
});
|
||||
|
||||
test('Throws error with custom message', () => {
|
||||
expect.assertions(3);
|
||||
|
||||
try {
|
||||
config.handler({ message: 'Something went wrong' } as any, {} as any);
|
||||
} catch (err: any) {
|
||||
expect(err.code).toBe(DEFAULT_ERROR.code);
|
||||
expect(err.status).toBe(500);
|
||||
expect(err.message).toBe('Something went wrong');
|
||||
}
|
||||
});
|
||||
|
||||
test('Throws error with custom code, status, and message', () => {
|
||||
expect.assertions(3);
|
||||
|
||||
try {
|
||||
config.handler({ code: 'CUSTOM_ERROR', status: '400', message: 'Bad request' } as any, {} as any);
|
||||
} catch (err: any) {
|
||||
expect(err.code).toBe('CUSTOM_ERROR');
|
||||
expect(err.status).toBe(400);
|
||||
expect(err.message).toBe('Bad request');
|
||||
}
|
||||
});
|
||||
|
||||
test('Throws error with invalid status, falling back to default', () => {
|
||||
expect.assertions(3);
|
||||
|
||||
try {
|
||||
config.handler({ status: 'invalid' } as any, {} as any);
|
||||
} catch (err: any) {
|
||||
expect(err.code).toBe(DEFAULT_ERROR.code);
|
||||
expect(err.status).toBe(DEFAULT_ERROR.status);
|
||||
expect(err.message).toBe(DEFAULT_ERROR.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
26
api/src/operations/throw-error/index.ts
Normal file
26
api/src/operations/throw-error/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createError, InternalServerError } from '@directus/errors';
|
||||
import { defineOperationApi } from '@directus/extensions';
|
||||
|
||||
type Options = {
|
||||
code: string;
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const FALLBACK_ERROR = new InternalServerError();
|
||||
|
||||
export default defineOperationApi<Options>({
|
||||
id: 'throw-error',
|
||||
|
||||
handler: ({ code, status, message }) => {
|
||||
const statusCode = parseInt(status);
|
||||
|
||||
const error = createError(
|
||||
code ?? FALLBACK_ERROR.code,
|
||||
message ?? FALLBACK_ERROR.message,
|
||||
isNaN(statusCode) ? FALLBACK_ERROR.status : statusCode,
|
||||
);
|
||||
|
||||
throw new error();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/app",
|
||||
"version": "13.12.0",
|
||||
"version": "13.13.1",
|
||||
"description": "App dashboard for Directus",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -410,6 +410,10 @@ async function onClick(event: MouseEvent) {
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
html[dir='rtl'] & {
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner .v-progress-circular {
|
||||
|
||||
@@ -43,7 +43,7 @@ async function copyError() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v-error selectable">
|
||||
<div class="v-error">
|
||||
<output>[{{ code }}] {{ message }}</output>
|
||||
<v-icon
|
||||
v-if="isCopySupported"
|
||||
|
||||
@@ -333,8 +333,6 @@ function setContent() {
|
||||
border-radius: var(--theme--border-radius);
|
||||
transition: var(--fast) var(--transition);
|
||||
transition-property: background-color, color;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:deep(.selected-field:not(:disabled):hover) {
|
||||
|
||||
77
app/src/components/v-form/form-field.test.ts
Normal file
77
app/src/components/v-form/form-field.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import VMenu from '../v-menu.vue';
|
||||
import FormField from '@/components/v-form/form-field.vue';
|
||||
import { i18n } from '@/lang';
|
||||
import { Width } from '@directus/system-data';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
const baseField = {
|
||||
field: 'test',
|
||||
name: 'Test Field',
|
||||
collection: 'test_collection',
|
||||
meta: {
|
||||
width: 'full' as Width,
|
||||
readonly: false,
|
||||
hideLabel: false,
|
||||
special: [],
|
||||
note: '',
|
||||
validation_message: '',
|
||||
validation: undefined,
|
||||
},
|
||||
schema: {
|
||||
default_value: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const global = {
|
||||
components: { VMenu },
|
||||
plugins: [
|
||||
i18n,
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
describe('FormField', () => {
|
||||
it('should show FormFieldLabel in batch mode if field.meta.special does not include "no-data"', () => {
|
||||
const wrapper = mount(FormField, {
|
||||
props: {
|
||||
field: { ...baseField, hideLabel: true, meta: { ...baseField.meta, special: [] } },
|
||||
batchMode: true,
|
||||
batchActive: true,
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
// Label should be visible
|
||||
expect(wrapper.findComponent({ name: 'FormFieldLabel' }).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide FormFieldLabel in batch mode if field.meta.special includes "no-data"', () => {
|
||||
const wrapper = mount(FormField, {
|
||||
props: {
|
||||
field: { ...baseField, hideLabel: true, meta: { ...baseField.meta, special: ['no-data'] } },
|
||||
batchMode: true,
|
||||
batchActive: true,
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
// Label should be hidden
|
||||
expect(wrapper.findComponent({ name: 'FormFieldLabel' }).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should hide FormFieldLabel if field.hideLabel is true and not in batch mode', () => {
|
||||
const wrapper = mount(FormField, {
|
||||
props: {
|
||||
field: { ...baseField, hideLabel: true },
|
||||
batchMode: false,
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent({ name: 'FormFieldLabel' }).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -51,6 +51,11 @@ const isDisabled = computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const isLabelHidden = computed(() => {
|
||||
if (props.batchMode && !props.field.meta?.special?.includes('no-data')) return false;
|
||||
return props.field.hideLabel;
|
||||
});
|
||||
|
||||
const { internalValue, isEdited, defaultValue } = useComputedValues();
|
||||
|
||||
const { showRaw, copyRaw, pasteRaw, onRawValueSubmit } = useRaw();
|
||||
@@ -162,7 +167,7 @@ function useComputedValues() {
|
||||
class="field"
|
||||
:class="[field.meta?.width || 'full', { invalid: validationError }]"
|
||||
>
|
||||
<v-menu v-if="field.hideLabel !== true" placement="bottom-start" show-arrow arrow-placement="start">
|
||||
<v-menu v-if="!isLabelHidden" placement="bottom-start" show-arrow arrow-placement="start">
|
||||
<template #activator="{ toggle, active }">
|
||||
<form-field-label
|
||||
:field="field"
|
||||
@@ -223,7 +228,7 @@ function useComputedValues() {
|
||||
|
||||
<small v-if="field.meta && field.meta.note" v-md="{ value: field.meta.note, target: '_blank' }" class="type-note" />
|
||||
|
||||
<small v-if="validationError" class="validation-error selectable">
|
||||
<small v-if="validationError" class="validation-error">
|
||||
<template v-if="showCustomValidationMessage">
|
||||
{{ field.meta?.validation_message }}
|
||||
<v-icon v-tooltip="validationMessage" small right name="help" />
|
||||
|
||||
@@ -70,7 +70,7 @@ function showCustomValidationMessage(validationError: ValidationErrorWithDetails
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-notice type="danger" class="full selectable">
|
||||
<v-notice type="danger" class="full">
|
||||
<div>
|
||||
<p>{{ t('validation_errors_notice') }}</p>
|
||||
<ul class="validation-errors-list">
|
||||
|
||||
@@ -87,5 +87,9 @@ withDefaults(defineProps<Props>(), {
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
html[dir='rtl'] & {
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -211,8 +211,6 @@ function onClick(event: PointerEvent) {
|
||||
cursor: pointer;
|
||||
transition: var(--fast) var(--transition);
|
||||
transition-property: background-color, color;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
&:not(.disabled):not(.dense):not(.block):hover {
|
||||
color: var(--v-list-item-color-hover, var(--v-list-color-hover, var(--theme--foreground)));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
import TransitionBounce from './transition/bounce.vue';
|
||||
import VMenu from './v-menu.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
vi.mock('lodash', async () => {
|
||||
const mod = await vi.importActual<{ default: typeof import('lodash') }>('lodash');
|
||||
@@ -31,6 +32,11 @@ const mountOptions = {
|
||||
components: {
|
||||
TransitionBounce,
|
||||
},
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
slots: {
|
||||
default: Content,
|
||||
@@ -135,3 +141,248 @@ test('should have pointerenter and pointerleave event listener when trigger is "
|
||||
|
||||
expect(wrapper.props('modelValue')).toBe(false);
|
||||
});
|
||||
|
||||
test('should place menu at bottom-start when menu is attached and using ltr', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: true,
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuContent = wrapper.findComponent(TransitionBounce).find('.v-menu-popper');
|
||||
|
||||
expect(menuContent.attributes('data-placement')).toBe('bottom-start');
|
||||
});
|
||||
|
||||
test('should place menu at "bottom-start" when menu is not attached and placement is "bottom-start" and using ltr', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: false,
|
||||
placement: 'bottom-start',
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuContent = wrapper.findComponent(TransitionBounce).find('.v-menu-popper');
|
||||
|
||||
expect(menuContent.attributes('data-placement')).toBe('bottom-start');
|
||||
});
|
||||
|
||||
test('should place menu at "bottom-end" when menu is attached and using rtl', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: true,
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
global: {
|
||||
...mountOptions.global,
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
initialState: {
|
||||
userStore: {
|
||||
currentUser: { text_direction: 'rtl' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuContent = wrapper.findComponent(TransitionBounce).find('.v-menu-popper');
|
||||
|
||||
expect(menuContent.attributes('data-placement')).toBe('bottom-end');
|
||||
});
|
||||
|
||||
test('should place menu at "bottom-end" when menu is not attached and placement is "bottom-start" and using rtl', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: false,
|
||||
placement: 'bottom-start',
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
global: {
|
||||
...mountOptions.global,
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
initialState: {
|
||||
userStore: {
|
||||
currentUser: { text_direction: 'rtl' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuContent = wrapper.findComponent(TransitionBounce).find('.v-menu-popper');
|
||||
|
||||
expect(menuContent.attributes('data-placement')).toBe('bottom-end');
|
||||
});
|
||||
|
||||
test('should place menu arrow at left when using placement "top-start" and using ltr', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: false,
|
||||
placement: 'top-start',
|
||||
showArrow: true,
|
||||
arrowPlacement: 'start',
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuArrow = wrapper.findComponent(TransitionBounce).find('.arrow');
|
||||
|
||||
expect(menuArrow.attributes('style')).toContain('left: 0px');
|
||||
expect(menuArrow.attributes('style')).toContain('transform: translate3d(6px, 0px, 0)');
|
||||
});
|
||||
|
||||
test('should place menu arrow at left when using placement "bottom-start" and using ltr', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: false,
|
||||
placement: 'bottom-start',
|
||||
showArrow: true,
|
||||
arrowPlacement: 'start',
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuArrow = wrapper.findComponent(TransitionBounce).find('.arrow');
|
||||
|
||||
expect(menuArrow.attributes('style')).toContain('left: 0px');
|
||||
expect(menuArrow.attributes('style')).toContain('transform: translate3d(6px, 0px, 0)');
|
||||
});
|
||||
|
||||
test('should place menu arrow right when using placement "top-start" and using rtl', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: false,
|
||||
placement: 'top-start',
|
||||
showArrow: true,
|
||||
arrowPlacement: 'start',
|
||||
arrowPadding: 6,
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
global: {
|
||||
...mountOptions.global,
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
initialState: {
|
||||
userStore: {
|
||||
currentUser: { text_direction: 'rtl' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuArrow = wrapper.findComponent(TransitionBounce).find('.arrow');
|
||||
|
||||
expect(menuArrow.attributes('style')).toContain('left: unset');
|
||||
expect(menuArrow.attributes('style')).toContain('right: 0px');
|
||||
expect(menuArrow.attributes('style')).toContain('transform: translate3d(-6px, 0px, 0)');
|
||||
});
|
||||
|
||||
test('should place menu arrow right when using placement "bottom-start" and using rtl', async () => {
|
||||
const button = { template: '<button type="button">Content</button>' };
|
||||
|
||||
const wrapper = mount(VMenu, {
|
||||
...mountOptions,
|
||||
props: {
|
||||
trigger: 'click',
|
||||
attached: false,
|
||||
placement: 'bottom-start',
|
||||
showArrow: true,
|
||||
arrowPlacement: 'start',
|
||||
},
|
||||
slots: {
|
||||
default: button,
|
||||
},
|
||||
global: {
|
||||
...mountOptions.global,
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
initialState: {
|
||||
userStore: {
|
||||
currentUser: { text_direction: 'rtl' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.v-menu').trigger('click');
|
||||
|
||||
const menuArrow = wrapper.findComponent(TransitionBounce).find('.arrow');
|
||||
|
||||
expect(menuArrow.attributes('style')).toContain('left: unset');
|
||||
expect(menuArrow.attributes('style')).toContain('right: 0px');
|
||||
expect(menuArrow.attributes('style')).toContain('transform: translate3d(-6px, 0px, 0)');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useShortcut } from '@/composables/use-shortcut';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { Instance, Modifier, Placement, detectOverflow } from '@popperjs/core';
|
||||
import arrow from '@popperjs/core/lib/modifiers/arrow';
|
||||
import computeStyles from '@popperjs/core/lib/modifiers/computeStyles';
|
||||
@@ -65,6 +66,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isRTL = computed(() => userStore.textDirection === 'rtl');
|
||||
|
||||
const activator = ref<HTMLElement | null>(null);
|
||||
const reference = ref<HTMLElement | null>(null);
|
||||
|
||||
@@ -281,11 +286,23 @@ function usePopper(
|
||||
stop();
|
||||
});
|
||||
|
||||
function getPlacement() {
|
||||
if (isRTL.value) {
|
||||
if (options.value.attached) {
|
||||
return 'bottom-end';
|
||||
} else if (options.value.placement.includes('start') || options.value.placement.includes('end')) {
|
||||
return options.value.placement.replace(/start|end/g, (match) => (match === 'start' ? 'end' : 'start'));
|
||||
}
|
||||
}
|
||||
|
||||
return options.value.attached ? 'bottom-start' : options.value.placement;
|
||||
}
|
||||
|
||||
watch(
|
||||
options,
|
||||
() => {
|
||||
popperInstance.value?.setOptions({
|
||||
placement: options.value.attached ? 'bottom-start' : options.value.placement,
|
||||
placement: getPlacement() as Placement,
|
||||
modifiers: getModifiers(),
|
||||
});
|
||||
},
|
||||
@@ -301,7 +318,7 @@ function usePopper(
|
||||
function start() {
|
||||
return new Promise((resolve) => {
|
||||
popperInstance.value = createPopper(reference.value!, popper.value!, {
|
||||
placement: options.value.attached ? 'bottom-start' : options.value.placement,
|
||||
placement: getPlacement() as Placement,
|
||||
modifiers: getModifiers(resolve),
|
||||
strategy: 'fixed',
|
||||
});
|
||||
@@ -395,6 +412,10 @@ function usePopper(
|
||||
case 'bottom-start':
|
||||
x = props.arrowPadding;
|
||||
break;
|
||||
case 'top-end':
|
||||
case 'bottom-end':
|
||||
x = props.arrowPadding * -1;
|
||||
break;
|
||||
case 'left-start':
|
||||
case 'right-start':
|
||||
y = props.arrowPadding;
|
||||
@@ -402,6 +423,11 @@ function usePopper(
|
||||
}
|
||||
|
||||
state.styles.arrow.transform = `translate3d(${x}px, ${y}px, 0)`;
|
||||
|
||||
if (isRTL.value) {
|
||||
state.styles.arrow.right = state.styles.arrow.left;
|
||||
state.styles.arrow.left = `unset`;
|
||||
}
|
||||
}
|
||||
|
||||
arrowStyles.value = state.styles.arrow;
|
||||
|
||||
249
app/src/components/v-resizeable.test.ts
Normal file
249
app/src/components/v-resizeable.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { Focus } from '@/__utils__/focus';
|
||||
import { generateRouter } from '@/__utils__/router';
|
||||
import { Tooltip } from '@/__utils__/tooltip';
|
||||
import type { GlobalMountOptions } from '@/__utils__/types';
|
||||
import { i18n } from '@/lang';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { Router } from 'vue-router';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import VResizeable from './v-resizeable.vue';
|
||||
import type { ResizeableOptions } from './v-resizeable.vue';
|
||||
|
||||
// Mock the useUserStore
|
||||
vi.mock('@/stores/user', () => ({
|
||||
useUserStore: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock vueuse composables
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useElementVisibility: vi.fn(() => true),
|
||||
useEventListener: vi.fn(),
|
||||
}));
|
||||
|
||||
let router: Router;
|
||||
let global: GlobalMountOptions;
|
||||
let mockUserStore: any;
|
||||
|
||||
const defaultProps = {
|
||||
width: 200,
|
||||
minWidth: 100,
|
||||
maxWidth: 500,
|
||||
defaultWidth: 200,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
// Mock user store
|
||||
mockUserStore = {
|
||||
textDirection: 'ltr',
|
||||
};
|
||||
|
||||
vi.mocked(useUserStore).mockReturnValue(mockUserStore);
|
||||
|
||||
router = generateRouter();
|
||||
router.push('/');
|
||||
await router.isReady();
|
||||
|
||||
global = {
|
||||
stubs: [],
|
||||
directives: {
|
||||
focus: Focus,
|
||||
tooltip: Tooltip,
|
||||
},
|
||||
plugins: [router, pinia, i18n],
|
||||
};
|
||||
});
|
||||
|
||||
describe('VResizeable', () => {
|
||||
test('Mount component', () => {
|
||||
expect(VResizeable).toBeTruthy();
|
||||
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: defaultProps,
|
||||
slots: {
|
||||
default: '<div class="test-content">Test Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
expect(wrapper.find('.resize-wrapper').exists()).toBe(true);
|
||||
expect(wrapper.find('.test-content').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('renders slot content correctly', () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: defaultProps,
|
||||
slots: {
|
||||
default: '<div class="slot-content">Slot Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Slot Content');
|
||||
expect(wrapper.find('.slot-content').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('shows grab bar when not disabled', () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: defaultProps,
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
expect(wrapper.find('.grab-bar').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('does not show grab bar when disabled', () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
disabled: true,
|
||||
},
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
expect(wrapper.find('.grab-bar').exists()).toBe(false);
|
||||
expect(wrapper.find('.resize-wrapper').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('applies always-show class when alwaysShowHandle option is true', () => {
|
||||
const options: ResizeableOptions = {
|
||||
alwaysShowHandle: true,
|
||||
};
|
||||
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
options,
|
||||
},
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const grabBar = wrapper.find('.grab-bar');
|
||||
expect(grabBar.exists()).toBe(true);
|
||||
expect(grabBar.classes()).toContain('always-show');
|
||||
});
|
||||
|
||||
describe('Pointer interactions', () => {
|
||||
test('activates grab bar on pointer enter', async () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: defaultProps,
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const grabBar = wrapper.find('.grab-bar');
|
||||
|
||||
await grabBar.trigger('pointerenter');
|
||||
expect(grabBar.classes()).toContain('active');
|
||||
});
|
||||
|
||||
test('deactivates grab bar on pointer leave', async () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: defaultProps,
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const grabBar = wrapper.find('.grab-bar');
|
||||
|
||||
await grabBar.trigger('pointerenter');
|
||||
expect(grabBar.classes()).toContain('active');
|
||||
|
||||
await grabBar.trigger('pointerleave');
|
||||
expect(grabBar.classes()).not.toContain('active');
|
||||
});
|
||||
|
||||
test('starts dragging on pointer down', async () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: defaultProps,
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const grabBar = wrapper.find('.grab-bar');
|
||||
|
||||
await grabBar.trigger('pointerdown', {
|
||||
pageX: 100,
|
||||
});
|
||||
|
||||
// Check that dragging event is emitted
|
||||
expect(wrapper.emitted('dragging')).toBeTruthy();
|
||||
expect(wrapper.emitted('dragging')?.[0]).toEqual([true]);
|
||||
});
|
||||
|
||||
test('emits width update on double click (reset)', async () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
width: 300, // Different from defaultWidth
|
||||
},
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const grabBar = wrapper.find('.grab-bar');
|
||||
|
||||
await grabBar.trigger('dblclick');
|
||||
|
||||
expect(wrapper.emitted('update:width')).toBeTruthy();
|
||||
expect(wrapper.emitted('update:width')?.[0]).toEqual([defaultProps.defaultWidth]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transitions', () => {
|
||||
test('applies transition class when not dragging', () => {
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: defaultProps,
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const resizeWrapper = wrapper.find('.resize-wrapper');
|
||||
expect(resizeWrapper.classes()).toContain('transition');
|
||||
});
|
||||
|
||||
test('does not apply transition class when disableTransition is true', () => {
|
||||
const options: ResizeableOptions = {
|
||||
disableTransition: true,
|
||||
};
|
||||
|
||||
const wrapper = mount(VResizeable, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
options,
|
||||
},
|
||||
slots: {
|
||||
default: '<div>Content</div>',
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const resizeWrapper = wrapper.find('.resize-wrapper');
|
||||
expect(resizeWrapper.classes()).not.toContain('transition');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useSync } from '@directus/composables';
|
||||
import { useElementVisibility, useEventListener } from '@vueuse/core';
|
||||
import { clamp } from 'lodash';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useElementVisibility, useEventListener } from '@vueuse/core';
|
||||
import { useSync } from '@directus/composables';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
type SnapZone = {
|
||||
snapPos: number;
|
||||
@@ -69,6 +70,10 @@ useEventListener(target, 'transitionend', (event: TransitionEvent) => {
|
||||
|
||||
const internalWidth = useSync(props, 'width', emit);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isRTL = computed(() => userStore.textDirection === 'rtl');
|
||||
|
||||
watch(
|
||||
[internalWidth, target, () => props.maxWidth],
|
||||
([width, target, maxWidth]) => {
|
||||
@@ -104,7 +109,13 @@ function onPointerMove(event: PointerEvent) {
|
||||
if (!dragging.value) return;
|
||||
|
||||
animationFrameID = window.requestAnimationFrame(() => {
|
||||
const newWidth = clamp(dragStartWidth + (event.pageX - dragStartX), props.minWidth, props.maxWidth);
|
||||
const dragDelta = event.pageX - dragStartX;
|
||||
|
||||
const newWidth = clamp(
|
||||
isRTL.value ? dragStartWidth - dragDelta : dragStartWidth + dragDelta,
|
||||
props.minWidth,
|
||||
props.maxWidth,
|
||||
);
|
||||
|
||||
const snapZones = props.options?.snapZones;
|
||||
|
||||
@@ -200,12 +211,16 @@ function onPointerUp() {
|
||||
background-color: var(--theme--primary);
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
transform: translate(50%, 0);
|
||||
transition: opacity var(--fast) var(--transition);
|
||||
transition-delay: 0s;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
transform: translate(50%, 0);
|
||||
|
||||
html[dir='rtl'] & {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
|
||||
114
app/src/components/v-table/table-header.test.ts
Normal file
114
app/src/components/v-table/table-header.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Focus } from '@/__utils__/focus';
|
||||
import { generateRouter } from '@/__utils__/router';
|
||||
import { Tooltip } from '@/__utils__/tooltip';
|
||||
import type { GlobalMountOptions } from '@/__utils__/types';
|
||||
import { i18n } from '@/lang';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { Router } from 'vue-router';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import TableHeader from './table-header.vue';
|
||||
import type { Header, Sort } from './types';
|
||||
|
||||
// Mock the useUserStore
|
||||
vi.mock('@/stores/user', () => ({
|
||||
useUserStore: vi.fn(),
|
||||
}));
|
||||
|
||||
let router: Router;
|
||||
let global: GlobalMountOptions;
|
||||
let mockUserStore: any;
|
||||
|
||||
const defaultProps = {
|
||||
headers: [
|
||||
{
|
||||
text: 'Name',
|
||||
value: 'name',
|
||||
description: null,
|
||||
align: 'left' as const,
|
||||
sortable: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
text: 'Email',
|
||||
value: 'email',
|
||||
description: null,
|
||||
align: 'left' as const,
|
||||
sortable: true,
|
||||
width: 250,
|
||||
},
|
||||
] as Header[],
|
||||
sort: {
|
||||
by: null,
|
||||
desc: false,
|
||||
} as Sort,
|
||||
reordering: false,
|
||||
allowHeaderReorder: true,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
// Mock user store
|
||||
mockUserStore = {
|
||||
textDirection: 'ltr',
|
||||
};
|
||||
|
||||
vi.mocked(useUserStore).mockReturnValue(mockUserStore);
|
||||
|
||||
router = generateRouter();
|
||||
router.push('/');
|
||||
await router.isReady();
|
||||
|
||||
global = {
|
||||
stubs: ['v-icon', 'v-checkbox', 'v-menu'],
|
||||
directives: {
|
||||
focus: Focus,
|
||||
tooltip: Tooltip,
|
||||
},
|
||||
plugins: [router, pinia, i18n],
|
||||
};
|
||||
});
|
||||
|
||||
describe('TableHeader', () => {
|
||||
test('Mount component', () => {
|
||||
expect(TableHeader).toBeTruthy();
|
||||
|
||||
const wrapper = mount(TableHeader, {
|
||||
props: defaultProps,
|
||||
global,
|
||||
});
|
||||
|
||||
expect(wrapper.find('thead').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('renders headers correctly', () => {
|
||||
const wrapper = mount(TableHeader, {
|
||||
props: defaultProps,
|
||||
global,
|
||||
});
|
||||
|
||||
const headers = wrapper.findAll('th.cell');
|
||||
// Should have 2 headers + 1 spacer
|
||||
expect(headers).toHaveLength(3);
|
||||
|
||||
// Check header text content
|
||||
expect(wrapper.text()).toContain('Name');
|
||||
expect(wrapper.text()).toContain('Email');
|
||||
});
|
||||
|
||||
test('shows resize handles when showResize is true', () => {
|
||||
const wrapper = mount(TableHeader, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
showResize: true,
|
||||
},
|
||||
global,
|
||||
});
|
||||
|
||||
const resizeHandles = wrapper.findAll('.resize-handle');
|
||||
expect(resizeHandles).toHaveLength(2); // One for each header
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@/composables/use-event-listener';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { hideDragImage } from '@/utils/hide-drag-image';
|
||||
import { useSync } from '@directus/composables';
|
||||
import type { ShowSelect } from '@directus/types';
|
||||
@@ -40,6 +41,9 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits(['update:sort', 'toggle-select-all', 'update:headers', 'update:reordering']);
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isRTL = computed(() => userStore.textDirection === 'rtl');
|
||||
|
||||
const resizing = ref<boolean>(false);
|
||||
const resizeStartX = ref<number>(0);
|
||||
@@ -133,7 +137,8 @@ function onResizeHandleMouseDown(header: Header, event: PointerEvent) {
|
||||
|
||||
function onMouseMove(event: PointerEvent) {
|
||||
if (resizing.value === true) {
|
||||
const newWidth = resizeStartWidth.value + (event.pageX - resizeStartX.value);
|
||||
const deltaX = event.pageX - resizeStartX.value;
|
||||
const newWidth = resizeStartWidth.value + (isRTL.value ? -deltaX : deltaX);
|
||||
const currentHeaders = clone(props.headers);
|
||||
|
||||
const newHeaders = currentHeaders.map((existing: Header) => {
|
||||
|
||||
@@ -343,8 +343,6 @@ function parseHTML(innerText?: string, isDirectInput = false) {
|
||||
vertical-align: -2px;
|
||||
background: var(--theme--primary-background);
|
||||
border-radius: var(--theme--border-radius);
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
|
||||
@@ -293,6 +293,31 @@ describe('test o2m relation', () => {
|
||||
$index: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('Initial data should be cleared when itemId changes to new item', async () => {
|
||||
// Mount component with existing itemId
|
||||
const wrapper = mount(TestComponent, {
|
||||
props: { relation: relationO2M, value: [], id: 1 },
|
||||
});
|
||||
|
||||
// Wait for initial data to load
|
||||
await flushPromises();
|
||||
|
||||
// Verify initial data is loaded for existing item
|
||||
expect(wrapper.vm.fetchedItems).toEqual(workerData);
|
||||
|
||||
// Change itemId to '+' (new item) - simulates "save and create new"
|
||||
await wrapper.setProps({ id: '+' });
|
||||
|
||||
// Wait for the change to settle
|
||||
await flushPromises();
|
||||
|
||||
// For a new item, fetchedItems should be empty
|
||||
expect(wrapper.vm.fetchedItems).toEqual([]);
|
||||
|
||||
// The component should not be in loading state
|
||||
expect(wrapper.vm.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const relationM2A: RelationM2A = {
|
||||
|
||||
@@ -383,6 +383,8 @@ export function useRelationMultiple(
|
||||
loading.value = true;
|
||||
|
||||
if (itemId.value !== '+') {
|
||||
const currentItemId = itemId.value;
|
||||
|
||||
const filter: Filter = { _and: [{ [reverseJunctionField]: itemId.value } as Filter] };
|
||||
|
||||
if (previewQuery.value.filter) {
|
||||
@@ -400,6 +402,14 @@ export function useRelationMultiple(
|
||||
},
|
||||
});
|
||||
|
||||
// if itemId changed during the request, we wan't to avoid updating items with incorrect data.
|
||||
// This can happen if the user navigates to a different item while the request is in progress.
|
||||
// The assumption here is that there is another request that started after this one started
|
||||
// and this one is no longer relevant.
|
||||
if (itemId.value !== currentItemId) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchedItems.value = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -44,6 +44,14 @@ describe('setLanguage', () => {
|
||||
|
||||
expect(vi.mocked(setLanguage).mock.calls[0]?.[0]).not.toBeNull();
|
||||
});
|
||||
|
||||
test('should be called with user language', async () => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
await hydrate();
|
||||
|
||||
expect(vi.mocked(setLanguage)).toBeCalledWith(userStore.language);
|
||||
});
|
||||
});
|
||||
|
||||
describe('basemap', () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useUserStore } from '@/stores/user';
|
||||
import { getBasemapSources } from '@/utils/geometry/basemap';
|
||||
import { useAppStore } from '@directus/stores';
|
||||
import { onDehydrateExtensions, onHydrateExtensions } from './extensions';
|
||||
import { setLanguage } from './lang/set-language';
|
||||
|
||||
type GenericStore = {
|
||||
$id: string;
|
||||
@@ -76,6 +77,8 @@ export async function hydrate(): Promise<void> {
|
||||
await onHydrateExtensions();
|
||||
}
|
||||
|
||||
await setLanguage(userStore.language);
|
||||
|
||||
appStore.basemap = getBasemapSources()[0].name;
|
||||
} catch (error: any) {
|
||||
appStore.error = error;
|
||||
|
||||
@@ -241,8 +241,6 @@ const newTranslationDefaults = computed(() => {
|
||||
border-radius: var(--theme--border-radius);
|
||||
transition: var(--fast) var(--transition);
|
||||
transition-property: background-color, color;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
font-family: var(--theme--fonts--monospace--font-family);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ function cancelAndClose() {
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<canvas :id="canvasID" class="qr" />
|
||||
<output class="secret selectable">{{ secret }}</output>
|
||||
<output class="secret">{{ secret }}</output>
|
||||
<v-input ref="inputOTP" v-model="otp" type="text" :placeholder="t('otp')" :nullable="false" />
|
||||
<v-error v-if="error" :error="error" />
|
||||
</v-card-text>
|
||||
|
||||
@@ -47,7 +47,7 @@ const { theme } = useTheme(darkMode, themeLight, themeDark, {}, {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-overrides">
|
||||
<div class="theme-overrides" lang="en-US" dir="ltr">
|
||||
<SystemThemeOverridesGroup root :rules="theme.rules" :path="[]" :value="value ?? {}" :set="set" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -297,7 +297,7 @@ function isInterpolation(value: any) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-code codemirror-custom-styles" :class="{ disabled }">
|
||||
<div class="input-code codemirror-custom-styles" :class="{ disabled }" dir="ltr">
|
||||
<div ref="codemirrorEl"></div>
|
||||
|
||||
<v-button v-if="template" v-tooltip.left="t('fill_template')" small icon secondary @click="fillTemplate">
|
||||
@@ -329,8 +329,6 @@ function isInterpolation(value: any) {
|
||||
color: var(--theme--primary);
|
||||
cursor: pointer;
|
||||
transition: color var(--fast) var(--transition-out);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme--primary-accent);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSettingsStore } from '@/stores/settings';
|
||||
import { percentage } from '@/utils/percentage';
|
||||
import { SettingsStorageAssetPreset } from '@directus/types';
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import { createFocusTrap, type FocusTrap } from 'focus-trap';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { ComponentPublicInstance, computed, onMounted, ref, toRefs, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -298,6 +299,40 @@ function setup(editor: any) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let dialogTrap: FocusTrap | null = null;
|
||||
editor.on('OpenWindow', activateTrap);
|
||||
editor.on('CloseWindow', deactivateTrap);
|
||||
|
||||
function activateTrap() {
|
||||
const toxDialogEl = document.querySelector('.tox-dialog');
|
||||
if (toxDialogEl === null) return;
|
||||
|
||||
// TinyMCE adds tabindex="-1" to all focusable elements in the dialog
|
||||
const notFocusableElements = toxDialogEl.querySelectorAll('[tabindex="-1"]');
|
||||
// To apply a focus trap, we need to make these elements temporarily focusable
|
||||
notFocusableElements.forEach(setFocusable);
|
||||
|
||||
deactivateTrap();
|
||||
dialogTrap = createFocusTrap(toxDialogEl as HTMLElement);
|
||||
dialogTrap.activate();
|
||||
|
||||
notFocusableElements.forEach(setNotFocusable);
|
||||
}
|
||||
|
||||
function setFocusable(el: Element) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
}
|
||||
|
||||
function setNotFocusable(el: Element) {
|
||||
el.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
function deactivateTrap() {
|
||||
if (dialogTrap === null) return;
|
||||
dialogTrap.deactivate();
|
||||
dialogTrap = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setFocus(val: boolean) {
|
||||
|
||||
@@ -407,6 +407,21 @@ function getLinkForItem(item: DisplayItem) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasSatisfiedUniqueConstraint = computed(() => {
|
||||
if (!relationInfo.value) return false;
|
||||
|
||||
const parentCollection = relationInfo.value.relation.related_collection;
|
||||
const relatedCollection = relationInfo.value.relatedCollection.collection;
|
||||
|
||||
// Find all M2O fields in the related collection that point to the parent collection and are unique
|
||||
const m2oFields = fieldsStore.getFieldsForCollection(relatedCollection).filter((field) => {
|
||||
const schema = field.schema;
|
||||
return schema?.foreign_key_table === parentCollection && schema?.is_unique === true;
|
||||
});
|
||||
|
||||
return m2oFields.length > 0 && totalItemCount.value > 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -605,10 +620,18 @@ function getLinkForItem(item: DisplayItem) {
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-button v-if="enableCreate && createAllowed" :disabled="disabled" @click="createItem">
|
||||
<v-button
|
||||
v-if="enableCreate && createAllowed && !hasSatisfiedUniqueConstraint"
|
||||
:disabled="disabled"
|
||||
@click="createItem"
|
||||
>
|
||||
{{ t('create_new') }}
|
||||
</v-button>
|
||||
<v-button v-if="enableSelect && updateAllowed" :disabled="disabled" @click="selectModalActive = true">
|
||||
<v-button
|
||||
v-if="enableSelect && updateAllowed && !hasSatisfiedUniqueConstraint"
|
||||
:disabled="disabled"
|
||||
@click="selectModalActive = true"
|
||||
>
|
||||
{{ t('add_existing') }}
|
||||
</v-button>
|
||||
<div class="spacer" />
|
||||
|
||||
@@ -2543,6 +2543,12 @@ operations:
|
||||
name: Sleep
|
||||
description: Wait a given number of milliseconds
|
||||
milliseconds: Milliseconds
|
||||
throw-error:
|
||||
name: Throw Error
|
||||
description: Throw an error in the reject path
|
||||
code: Error Code
|
||||
status: HTTP Status Code
|
||||
message: Error Message
|
||||
transform:
|
||||
name: Transform Payload
|
||||
description: Alter the Flow's JSON payload
|
||||
|
||||
@@ -29,9 +29,9 @@ published: منتشر شد
|
||||
draft: پیشنویس
|
||||
archived: بایگانیشده
|
||||
marketplace: بازار
|
||||
extensions: افزونه ها
|
||||
skip_link_nav: پرش به منو ناوبری
|
||||
skip_link_module_nav: پرش به منو ماژول ها
|
||||
extensions: افزونهها
|
||||
skip_link_nav: پرش به منوی ناوبری
|
||||
skip_link_module_nav: پرش به منوی ماژولها
|
||||
skip_link_main: پرش به محتوای اصلی
|
||||
skip_link_sidebar: پرش به منو کناری
|
||||
modules: ماژولها
|
||||
@@ -46,6 +46,8 @@ location: موقعیت
|
||||
collection_names_are_case_sensitive: نام مجموعه ها حساس به حروف کوچک و بزرگ هستند
|
||||
condition_rules: استایل شرطی
|
||||
input: ورودی
|
||||
invalid_input: ورودی نامعتبر
|
||||
not_a_number: نا عدد
|
||||
maps: نقشهها
|
||||
switch_user: تغییر کاربر
|
||||
item_creation: ساخت آیتم
|
||||
@@ -75,7 +77,7 @@ extension_operation: عملیات
|
||||
extension_bundle: پکیج ها
|
||||
extension_interfaces: رابطهای کاربری
|
||||
extension_displays: نحوه نمایش
|
||||
extension_layouts: چیدمان ها
|
||||
extension_layouts: چیدمانها
|
||||
extension_modules: ماژولها
|
||||
extension_panels: پنلها
|
||||
extension_themes: پوستهها
|
||||
@@ -89,6 +91,7 @@ extension_reload_required_copy: جهت مشاهده تغییرات پس از ف
|
||||
extension_reload_now: هم اکنون بازنشانی گردد
|
||||
published_relative: منتشر شده در {relativeTime}
|
||||
compatible_with_your_project: سازگار با پروژه شما
|
||||
compatible_with_your_project_copy: سازنده سازگاری افزونه با نسخه {hostVersion} را مشخص کرده است، که با نسخه پروژه شما ({currentVersion}) انطباق دارد
|
||||
last_updated_relative: آخرین به روزرسانی {relativeTime}
|
||||
n_files: '{n} فایل'
|
||||
verified: تایید شده
|
||||
@@ -99,9 +102,11 @@ reload_required: صفحه به بازنشانی مجدد نیاز دارد
|
||||
report_an_issue: گزارش یک مشکل
|
||||
website: وب سایت
|
||||
github: گیتهاب
|
||||
beta: آزمایشی
|
||||
n_downloads: '{n} دانلود'
|
||||
downloads: دانلودها
|
||||
compatibility_not_guaranteed: سازگاری تضمین نشده
|
||||
compatibility_not_guaranteed_copy: سازنده سازگاری افزونه با نسخه {hostVersion} را مشخص کرده است، که با نسخه پروژه شما ({currentVersion}) انطباق ندارد
|
||||
enable: فعال سازی
|
||||
disable: غیرفعال کردن
|
||||
custom: سفارشی
|
||||
@@ -159,9 +164,25 @@ version_key: شماره نسخه
|
||||
version_name: نام نسخه
|
||||
main_version: اصلی
|
||||
switch_version: تغییر نسخه
|
||||
switch_version_copy: مطمئنید که میخواهید به نسخه «{version}» بروید؟ تغییرات ذخیرهنشده از دست میرود.
|
||||
create_version: ایجاد نسخه
|
||||
rename_version: ویرایش نسخه
|
||||
compare_version: مقایسه نسخه
|
||||
save_version: ذخیرهسازی نسخه
|
||||
promote_version: ارتقای نسخه
|
||||
promote_version_disabled: بدون تغییر
|
||||
promote_version_drawer_title: ارتقای {version} به اصلی
|
||||
promote_version_changes: تغییرات
|
||||
promote_version_preview: پيش نمايش
|
||||
outdated_notice: >-
|
||||
نسخه اصلی از زمان ایجاد این نسخه بهروزرسانی شده است. لطفا مقادیر تمام فیلدهای پایین را بازبینی کنید و تایید کنید که وقتی این نسخه را به نسخه اصلی ارتقا میدهید، کدام نسخه باید استفاده شود.
|
||||
promote_notice: >-
|
||||
لطفا مقادیر تمام فیلدهای پایین را بازبینی کنید و تایید کنید که وقتی این نسخه را به نسخه اصلی ارتقا میدهید، کدام نسخه باید استفاده شود.
|
||||
delete_version: حذف نسخه
|
||||
delete_version_copy: >-
|
||||
آیا از حذف نسخه «{version}» مطمئنید؟ این اقدام غیر قابل بازگشت است.
|
||||
delete_on_promote_copy: >-
|
||||
میخواهید نسخه «{version}» را بعد از ارتقا نگه دارید یا حذف کنید؟
|
||||
reset_bookmark: بازنشانی نشانک
|
||||
update_bookmark: به روزرسانی نشانک
|
||||
delete_bookmark: حذف نشانک
|
||||
@@ -175,7 +196,12 @@ logoutReason:
|
||||
SESSION_EXPIRED: نشست به پایان رسیده
|
||||
public_label: در دسترس عموم
|
||||
public_description: اینکه کدام داده مربوط به API، بدون احراز هویت، قابل دسترسی باشد را کنترل میکند.
|
||||
public_role_info: >-
|
||||
نقش عمومی تعیین میکند که کدام دادههای API برای کاربران احراز هویت نشده یا کاربران بدون نقش در دسترس باشد. درخواستها با نقش عمومی نمیتوانند دسترسی برنامه یا مدیر داشته باشند. اگر دسترسی برنامه یا مدیر به سیاستی که به نقش عمومی منتسب شده است داده شود، ندیده گرفته میشوند.
|
||||
admin_description: نقش مدیریتی اولیه با سطح دسترسی بدون محدودیت به برنامه و API
|
||||
admin_policy_description: نقش مدیریتی اولیه با دسترسی نامحدود به برنامه و API.
|
||||
no_description: بدون توضیحات...
|
||||
reached_maximum_number_of_extensions: شما به حداکثر تعداد افزونههای این پروژه رسیدید. ({n}) برای اطلاعات بیشتر با مدیر سیستم تماس بگیرید.
|
||||
not_allowed: مجاز نیست
|
||||
archive: بایگانی
|
||||
archive_confirm: از بایگانی شدن این مورد مطمئنید؟
|
||||
@@ -207,7 +233,13 @@ validationError:
|
||||
nnull: مقدار نمی تواند خالی (نال) باشد
|
||||
required: مقدار الزامی است
|
||||
unique: مقدار باید یکتا باشد
|
||||
email: مقدار باید یک نشانی ایمیل معتبر باشد
|
||||
regex: این مقدار، فرمت صحیحی ندارد
|
||||
starts_with: مقدار باید با {substring} شروع شود
|
||||
istarts_with: مقدار باید با {substring} شروع شود
|
||||
nstarts_with: مقدار نمیتواند با {substring} شروع شود
|
||||
nistarts_with: مقدار نمیتواند با {substring} شروع شود
|
||||
ends_with: مقدار باید با {substring} پایان یابد
|
||||
iends_with: مقدار باید با {substring} پایان یابد
|
||||
nends_with: مقدار نمیتواند با {substring} پایان یابد
|
||||
niends_with: مقدار نمیتواند با {substring} پایان یابد
|
||||
@@ -234,9 +266,14 @@ field_permissions: مجوزهای فیلدها
|
||||
field_validation: اعتبار سنجی فیلدها
|
||||
field_presets: الگوهای فیلدها
|
||||
permissions_for_role: 'مواردی که نقش {role} میتواند {action}'
|
||||
permissions_for_policy: 'فیلدهایی که سیاست {policy} میتواند {action}.'
|
||||
fields_for_role: 'فیلدهایی که نقش {role} میتواند {action}'
|
||||
fields_for_policy: 'فیلدهایی که سیاست {policy} میتواند {action}.'
|
||||
validation_for_role: 'قوانین {action} مربوط به فیلد که نقش {role} باید رعایت کند.'
|
||||
validation_for_policy: 'قوانین {action} فیلد که سیاست {policy} باید رعایت کند.'
|
||||
presets_for_role: 'مقادیر پیش فرض فیلد برای نقش {role}.'
|
||||
presets_for_policy: 'مقادیر پیشفرض فیلد برای سیاست {policy}.'
|
||||
presets_field_warning: 'پیشتنظیمهای رابطهای برای فیلد «{field}» باید به شیوه «مفصل» تنظیم شود.'
|
||||
presentation_and_aliases: نحوه نمایش و نامهای مستعار
|
||||
revision_post_create: شکل ظاهری این آیتم وقتی که ساخته شده به این صورت بوده است.
|
||||
revision_post_update: شکل ظاهری این آیتم پس از بروزرسانی، به این صورت بوده است.
|
||||
@@ -285,6 +322,8 @@ field_in_collection: '{field} ({collection})'
|
||||
reset_page_preferences: بازنشانی تنظیمات صفحه
|
||||
hidden_field: فیلد مخفی
|
||||
hidden_on_detail: مخفی کردن فیلد در بخش جزییات آیتم
|
||||
readonly_field_label: غیرفعالسازی فیلد در پنل
|
||||
required_readonly_field_warning: فعالسازی همزمان «فقط خواندنی» و «الزامی» روی یک فیلد، در صورتی که مقدار خالی باشد، مانع ذخیره کردن آن میشود.
|
||||
key: کلید
|
||||
alias: نام مستعار
|
||||
bigInteger: عدد صحیح بزرگ
|
||||
@@ -309,8 +348,14 @@ uuid: شماره شناسایی یکتا (UUID)
|
||||
hash: هش
|
||||
geometry:
|
||||
All: هندسی (همه)
|
||||
Point: نقطه
|
||||
Polygon: چند ضلعی
|
||||
theme_auto: خودکار (بر اساس سیستم)
|
||||
theme_light: حالت روشن
|
||||
theme_dark: حالت تاریک
|
||||
theme_directus_default: دایرکتوس پیشفرض
|
||||
theme_directus_minimal: دایرکتوس مینیمال
|
||||
theme_directus_colormatch: دایرکتوس همرنگ
|
||||
not_available_for_type: برای این نوع در دسترس نیست
|
||||
create_translations: ایجاد ترجمه
|
||||
auto_refresh: تازه نمودن خودکار
|
||||
@@ -329,6 +374,8 @@ fields_group: گروه فیلدها
|
||||
no_collections_found: مجموعه ای یافت نشد.
|
||||
new_data_alert: 'فیلدهای زیر به مدل داده ای شما اضافه خواهند شد:'
|
||||
search_collection: جستجوی مجموعه...
|
||||
search_role: جستجوی نقش...
|
||||
search_policy: جستجوی سیاستها...
|
||||
search_field: جستجوی فیلد...
|
||||
new_field: 'فیلد جدید'
|
||||
new_collection: 'مجموعه جدید'
|
||||
@@ -369,6 +416,7 @@ soft_length: طول کاراکتر مجاز
|
||||
precision_scale: دقت و مقیاس
|
||||
readonly: فقط خواندنی
|
||||
unique: یکتا
|
||||
index: ایندکس
|
||||
updated_on: به روزرسانی شده در
|
||||
updated_by: به روزرسانی شده توسط
|
||||
primary_key: کلید اصلی
|
||||
@@ -394,6 +442,7 @@ clear_items: پاک کردن موارد
|
||||
reset_to_default: بازنشانی به تنظیمات پیش فرض
|
||||
undo_changes: برگرداندن تغییرات به حالت قبل
|
||||
notifications: اعلانات
|
||||
archive_all: بایگانی کردن همه
|
||||
show_all_activity: نمایش تمام فعالیتها
|
||||
page_not_found: برگه مد نظر یافت نشد
|
||||
page_not_found_body: ظاهراً صفحه ای که بدنبال آن هستید وجود ندارد.
|
||||
@@ -406,12 +455,14 @@ revision_delta_created: ایجاد شد
|
||||
revision_delta_created_externally: به شکل خارجی ایجاد شد
|
||||
revision_delta_updated: 'یک فیلد برورزسانی شد | {count} فیلد بروزرسانی شدند'
|
||||
revision_delta_deleted: حذف شده
|
||||
revision_delta_version_saved: 'ذخیرهسازی نسخه (یک فیلد به روز شده است) | ذخیرهسازی نسخه ({count} فیلد به روز شده است)'
|
||||
revision_delta_reverted: بازگردانده شد
|
||||
revision_delta_update_message: این فیلد به روز شده است اما ارزش آن به دلایل امنیتی پنهان شده است.
|
||||
revision_delta_other: نسخه
|
||||
revision_delta_by: 'در {date} توسط {user}'
|
||||
presentation_text_values_cannot_be_reimported: 'از متن ارائه استفاده می کند، مقادیر را نمی توان دوباره وارد کرد.'
|
||||
download_page_as_csv: 'دانلود صفحه بهصورت CSV'
|
||||
download_page_as_csv_unsupported: این چیدمان از دانلود کردن صفحه فعلی پشتیبانی نمیکند.
|
||||
private_user: کاربر خصوصی
|
||||
creation_preview: پیش نمایش ساخت
|
||||
revision_preview: پیش نمایش ویرایش
|
||||
@@ -476,6 +527,11 @@ hours: ساعت
|
||||
month: ماه
|
||||
year: سال
|
||||
select_all: انتخاب همه
|
||||
permissionsLevel:
|
||||
all: دسترسی {action} کامل
|
||||
partial: دسترسی {action} جزئی
|
||||
custom: دسترسی {action} جزئی
|
||||
none: بدون دسترسی {action}
|
||||
months:
|
||||
january: ژانویه
|
||||
february: فوریه
|
||||
@@ -490,15 +546,22 @@ months:
|
||||
november: نوامبر
|
||||
december: دسامبر
|
||||
drag_mode: حالت کشیدن
|
||||
move_tool: ابزار انتقال
|
||||
crop_tool: ابزار بریدن
|
||||
focal_point_tool: ابزار نقطه کانونی
|
||||
cancel_crop: لغو برش
|
||||
cancel_selection: لغو انتخاب
|
||||
original: مقدار اولیه
|
||||
url: URL
|
||||
import_label: وارد کردن
|
||||
file_details: جزئیات فایل
|
||||
copy_id: کپی ID
|
||||
copy_id_success: ID کپی شد
|
||||
copy_id_fail: ID کپی نشد
|
||||
dimensions: ابعاد
|
||||
size: سایز
|
||||
created: ایجاد شد
|
||||
uploaded: بارگزاری شد
|
||||
modified: دستکاری شده
|
||||
owner: مالک
|
||||
edited_by: ویرایش شده توسط
|
||||
@@ -520,9 +583,15 @@ replace_from_url: جایگزینی فایل از URL
|
||||
no_image_selected: تصویری انتخاب نشده است
|
||||
no_file_selected: هیچ فایلی انتخاب نشده است
|
||||
download_file: دانلود فایل
|
||||
open_file_in_tab: باز کردن فایل در تب جدید
|
||||
start_export: شروع برون بری
|
||||
not_available_for_local_downloads: برای دانلودهای محلی در دسترس نیست
|
||||
exporting_all_items_in_collection: برونبری {total} مورد از {collection}.
|
||||
exporting_limited_items_in_collection: '{count} از {total} مورد از {collection} صادر خواهد شد.'
|
||||
exporting_no_items_to_export: موردی برای صدور نیست. تنظیمات صدور را تغییر دهید و/یا مواردی را به مجموعه اضافه کنید.
|
||||
exporting_download_hint: به محض این که فایل {format} ایجاد شد، روی دستگاه شما دانلود خواهد شد.
|
||||
exporting_library_hint: به محض این که فایل {format} ایجاد شد، در کتابخانه فایل ذخیره خواهد شد.
|
||||
exporting_library_hint_forced: این صدور باید به صورت گروهی انجام شود. به محض تکمیل، فایل {format} در کتابخانه فایل ذخیره خواهد شد.
|
||||
collection_key: کلید اصلی مجموعه
|
||||
name: نام
|
||||
primary_key_field: فیلد کلید اصلی
|
||||
@@ -538,6 +607,8 @@ generated_uuid: UUID تولید شده
|
||||
manual_string: رشته حرفیِ وارد شده به صورت دستی
|
||||
save_and_create_new: ذخیره و ساخت آیتم جدید
|
||||
save_and_stay: ذخیره و ماندن
|
||||
save_and_quit: ذخیره و خروج
|
||||
save_and_return_to_main: ذخیره و بازگشت به اصلی
|
||||
save_as_copy: ذخیره به عنوان کپی
|
||||
add_existing: اضافه کردن مورد موجود
|
||||
creating_items: ساختن موارد
|
||||
@@ -548,9 +619,14 @@ allow_duplicates: اجازه تکرار
|
||||
comments: دیدگاهها
|
||||
no_comments: هنوز دیدگاهی ارسال نشده
|
||||
expand: گسترش
|
||||
expand_all: باز کردن همه
|
||||
expand_none: باز کردن هیچ کدام
|
||||
collapse: جمع کردن
|
||||
collapse_all: بستن همه
|
||||
select_item: انتخاب آیتم
|
||||
no_items: آیتمی وجود ندارد
|
||||
search_items: جستجوی موارد...
|
||||
search_extensions: جستجوی افزونهها...
|
||||
disabled: غیرفعال شده
|
||||
information: اطلاعات
|
||||
report_bug: گزارش باگ
|
||||
@@ -563,12 +639,17 @@ display_not_found: 'نمایش "{display}" یافت نشد.'
|
||||
reset_display: بازنشانی نمایشگر
|
||||
list-m2a: سازنده (M2A)
|
||||
item_count: 'بدون مقدار | يک آیتم | {count} آیتم'
|
||||
filtered_item_count: 'بدون مورد فیلتر شده | یک مورد فیلتر شده | {count} مورد فیلتر شده'
|
||||
no_items_copy: هنوز هیچ آیتمی در این مجموعه وجود ندارد.
|
||||
file_count: 'بدون فایل | یک فایل | {count} فایل'
|
||||
no_files_copy: فایلی برای نمایش موجود نمیباشد.
|
||||
user_count: 'بدون کاربر | یک کاربر | {count} کاربر'
|
||||
no_users_copy: این نقش، هنوز به هیچ کاربری تعلق نگرفته است.
|
||||
webhooks_count: 'هیچ وبهوک | یک وبهوک | {count} وبهوک'
|
||||
webhooks_deprecation_notice: |
|
||||
وبهوکها منسوخ شدهاند و استفاده بیشتر توصیه نمیشود.
|
||||
|
||||
لطفا به جای آن از **جریانها** با [عملیات وبهوک / درخواست URL](https://docs.directus.io/app/flows/operations.html#webhook-request-url) استفاده کنید.
|
||||
no_webhooks_copy: تاکنون هیچ وبهوکی پیکربندی نشده | جهت شروع یکی به پایین اضافه کنید.
|
||||
no_notifications: اعلانی موجود نیست
|
||||
no_notifications_copy: شما همه اعلانها و پیامها را دیده اید!
|
||||
@@ -579,7 +660,7 @@ csv: CSV
|
||||
no_collections: مجموعه ای وجود ندارد
|
||||
create_collection: ساخت مجموعه
|
||||
no_collections_copy_admin: شما هنوز هیچ مجموعه ای ندارید. برای شروع روی دکمه زیر کلیک کنید.
|
||||
no_collections_copy: شما هنوز هیچ مجموعه ای ندارید. لطفا با سرپرست سیستم خود تماس بگیرید.
|
||||
no_collections_copy: شما هنوز هیچ مجموعهای ندارید. لطفا با مدیر کل سیستم خود تماس بگیرید.
|
||||
relationship_not_setup: این رابطه به درستی پیکربندی نشده است
|
||||
no_singleton_relations: Relationships to Singletons are not supported
|
||||
display_template_not_setup: گزینه نمایش قالب اشتباه پیکربندی شده است
|
||||
@@ -593,6 +674,7 @@ copy_to: رونوشت به...
|
||||
no_other_dashboards_copy: شما هنوز داشبورد دیگری ندارید.
|
||||
inactive: غیرفعال
|
||||
users: کاربران
|
||||
roles: نقشها
|
||||
activity: فعالیتها
|
||||
activity_item: گزارش فعالیتها
|
||||
action: فعالیت
|
||||
@@ -630,7 +712,11 @@ bookmark_doesnt_exist_copy: نشانکی که میخواهید باز کنید
|
||||
bookmark_doesnt_exist_cta: بازگشت به مجموعه
|
||||
select_an_item: یک آیتم را انتخاب کنید...
|
||||
edit: ویرایش
|
||||
edit_item: ویرایش مورد
|
||||
enabled: فعال شده
|
||||
partially_enabled: به طور جزئی فعال شده
|
||||
enable_all: فعالسازی همه
|
||||
disable_all: غیرفعالسازی همه
|
||||
disable_tfa: غیرفعالسازی احراز هویت دو عاملی
|
||||
admin_disable_tfa_text: آیا مطمئن هستید که میخواهید رمز دو مرحله ای را برای این کاربر غیرفعال کنید؟
|
||||
tfa_setup: پیکربندی 2FA
|
||||
@@ -644,6 +730,7 @@ errors:
|
||||
COLLECTION_NOT_FOUND: "مجموعه مد نظر، وجود ندارد"
|
||||
CONTAINS_NULL_VALUES: فیلد حاوی مقدار null است
|
||||
CONTENT_TOO_LARGE: آیتم/فایل بیشتر از حد مجاز است
|
||||
FAILED_VALIDATION: اعتبارسنجی ناموفق بود
|
||||
FIELD_NOT_FOUND: فیلد یافت نشد
|
||||
FORBIDDEN: عدم اجازه دسترسی
|
||||
ILLEGAL_ASSET_TRANSFORMATION: منبع تصویر برای پیش نمایش بیش از حد بزرگ است
|
||||
@@ -711,6 +798,9 @@ make_collection_hidden: این مجموعه را پنهان کن
|
||||
make_folder_visible: این پوشه را هویدا کن
|
||||
make_folder_hidden: این پوشه را پنهان کن
|
||||
goto_collection_content: مشاهده محتوا
|
||||
count_of_total_items: '{count} از {total} مورد'
|
||||
start_end_of_count_items: '{start}-{end} از {count} مورد'
|
||||
start_end_of_count_filtered_items: '{start}-{end} از {count} مورد فیلتر شده'
|
||||
delete_collection_are_you_sure: >-
|
||||
آیا مطمئن هستید که می خواهید مجموعه "{collection}" را حذف کنید؟ با این کار مجموعه و همه موارد موجود در آن حذف می شود. این عمل دائمی است.
|
||||
delete_folder_are_you_sure: آیا مطمئن هستید که می خواهید پوشه "{folder}" را حذف کنید؟ پوشه ها و مجموعه های تو در تو به بالاترین سطح منتقل می شوند.
|
||||
@@ -758,8 +848,12 @@ operators:
|
||||
icontains: حاوی (غیر حساس)
|
||||
starts_with: با این مقدار شروع می شود
|
||||
nstarts_with: با این مقدار شروع نمی شود
|
||||
istarts_with: شروع میشود با (غیرحساس)
|
||||
nistarts_with: شروع نمیشود با (غیرحساس)
|
||||
ends_with: با این مقدار پایان می یابد
|
||||
nends_with: با این مقدار پایان نمی یابد
|
||||
iends_with: تمام میشود با (غیرحساس)
|
||||
niends_with: تمام نمیشود با (غیرحساس)
|
||||
between: بین این مقادیر است
|
||||
nbetween: بین این مقادیر نیست
|
||||
empty: خالی است
|
||||
@@ -773,6 +867,7 @@ operators:
|
||||
regex: با RegExp مطابقت دارد
|
||||
custom_validation_message: پیام اعتبارسنجی سفارشی
|
||||
loading: در حال بارگذاری...
|
||||
extension_readme_missing: این افزونه، فایل README ندارد.
|
||||
drop_to_upload: برای آپلود، در اینجا، رها کنید
|
||||
item: آیتم
|
||||
items: آیتمها
|
||||
@@ -785,16 +880,20 @@ upload_pending: آپلود متوقف شده
|
||||
drag_file_here: فایلها را بکشید و در اینجا رها کنید
|
||||
click_to_browse: برای انتخاب فایل، کلید کنید
|
||||
interface_options: گزینه های رابط کاربری
|
||||
display_options: تنظیمات نحوه نمایش
|
||||
layout_options: تنظیمات چیدمان
|
||||
rows: ردیف ها
|
||||
columns: ستون ها
|
||||
collection_setup: راه اندازی مجموعه
|
||||
optional_system_fields: فیلدهای اختیاری
|
||||
value_unique: مقدار باید یکتا باشد
|
||||
value_index: فیلد ایندکس شده است
|
||||
all_activity: تمام فعالیت ها
|
||||
create_item: ساخت آیتم
|
||||
display_template: نمایش الگو
|
||||
language_display_template: قالب نمایش زبان
|
||||
translations_display_template: قالب نمایش ترجمه ها
|
||||
n_items_selected: 'موردی انتخاب نشده | یک مورد انتخاب شده | {n} مورد انتخاب شده'
|
||||
per_page: هر صفحه
|
||||
all_files: تمامی فایلها
|
||||
my_files: فایلهای من
|
||||
@@ -844,6 +943,7 @@ run_flow_on_current: اجرای جریان در مجموعه فعلی
|
||||
run_flow_on_current_edited_confirm: این مورد ممکن است توسط این جریان بهروزرسانی شود، آیا از اجرای آن اطمینان دارید؟
|
||||
run_flow_on_selected: ابتدا یک یا چند مورد را انتخاب کنید | اجرای جریان روی 1 مورد انتخاب شده | اجرای {n} مورد روی جریان
|
||||
run_flow: اجرای جریان
|
||||
trigger_flow_success: جریان «{flow}» با موفقیت اجرا شد
|
||||
field_name_placeholder: ورود نام فیلد...
|
||||
field_key_placeholder: ورود کلید فیلد...
|
||||
trigger: راهانداز
|
||||
@@ -871,15 +971,20 @@ flow_tracking_activity: فقط پیگرد فعالیتها
|
||||
flow_tracking_null: هیچ چیز را پیگیری نکن
|
||||
start_flow: شروع جریان
|
||||
stop_flow: توقف جریان
|
||||
create_operation: ساخت اپراتور
|
||||
edit_operation: ویرایش اپراتور
|
||||
create_operation: ایجاد عملگر
|
||||
edit_operation: ویرایش عملگر
|
||||
code: کد
|
||||
operation_options: تنظیمات اپراتور
|
||||
operation_name: نام اپراتور...
|
||||
operation_options: تنظیمات عملیات
|
||||
operation_name: نام عملیات...
|
||||
operation_key: شناسه مرجع...
|
||||
operation_key_unique_error: کلیدهای عملیاتی باید در یک جریان منحصر به فرد باشند
|
||||
operation_handle_resolve: 'تصمیم: برای افزودن کلیک کنید یا برای مسیریابی مجدد بکشید'
|
||||
operation_handle_reject: 'رد کردن: برای افزودن کلیک کنید یا برای مسیریابی مجدد بکشید'
|
||||
visual_editor: ویرایشگر بصری
|
||||
no_url: URLی ارائه نشده است
|
||||
no_url_copy: لطفا یک URL در تنظیمات اضافه کنید.
|
||||
invalid_url: URL اشتباه
|
||||
invalid_url_copy: URL مشخص شده با هیچ یک از URL های مشخص شده در تنظیمات منطبق نیست.
|
||||
insights: بینش های داده ای
|
||||
dashboard: پیشخوان
|
||||
panel: پنل
|
||||
@@ -905,6 +1010,7 @@ no_data_in_flow: هیچ داده ای در این جریان تولید یا ب
|
||||
accountability: مسئوليت
|
||||
payload: ظرفیت یا Payload
|
||||
details: جزئیات
|
||||
logs_unread_count: '{count} خوانده نشده'
|
||||
full_screen: تمام صفحه
|
||||
full_text_search: جستجوی تمام متن
|
||||
edit_panels: ادیت پنلها
|
||||
@@ -933,6 +1039,7 @@ tooltip: راهنما
|
||||
tooltip_placeholder: یک مقدار برای راهنما وارد کنید...
|
||||
unlimited: بدون محدودیت
|
||||
open_link_in: باز کردن پیوند در
|
||||
new_tab: زبانه جدید
|
||||
save_image: ذخیره تصویر
|
||||
save_media: ذخیره رسانه
|
||||
wysiwyg_options:
|
||||
@@ -968,6 +1075,7 @@ wysiwyg_options:
|
||||
h4: سرتیتر ۴
|
||||
h5: سرتیتر ۵
|
||||
h6: سرتیتر ۶
|
||||
pre: از پیش قالببندیشده
|
||||
fontselect: انتخاب فونت
|
||||
fontsizeselect: انتخاب سایز فونت
|
||||
indent: تورفتگی
|
||||
@@ -982,11 +1090,19 @@ wysiwyg_options:
|
||||
source_code: ویرایش کد سورس
|
||||
fullscreen: تمام صفحه
|
||||
directionality: جهت گیری
|
||||
lazy_loading: بارگزاری تنبل تصاویر
|
||||
lazy_loading_label: فعالسازی بارگزاری تنبل
|
||||
dropdown: لیست کشویی
|
||||
choices: انتخاب ها
|
||||
choices_option_configured_incorrectly: انتخابها به درستی پیکربندی نشدهاند
|
||||
deselect: لغو انتخاب
|
||||
deselect_all: لغو همه
|
||||
theme: پوسته
|
||||
select_a_theme: یک پوسته انتخاب کنید
|
||||
default_sync_with_project: پیشفرض (همگام با پروژه)
|
||||
appearance_auto: خودکار (همگام با سیستم)
|
||||
appearance_light: روشن
|
||||
appearance_dark: تیره
|
||||
other: دیگری...
|
||||
adding_user: اضافه کردن کاربر
|
||||
unknown_user: کاربر ناشناس
|
||||
@@ -998,12 +1114,17 @@ editing_unit: 'در حال ویرایش {unit}'
|
||||
editing_in_batch: 'در حال ویرایش جمعی {count} آیتم'
|
||||
no_options_available: هیچ گزینه ای وجود ندارد
|
||||
settings_data_model: مدل داده
|
||||
settings_roles: نقشهای کاربر
|
||||
settings_permissions: سیاستهای دسترسی
|
||||
settings_project: تنظیمات
|
||||
settings_appearance: ظاهر
|
||||
settings_webhooks: هوک های تحت وب
|
||||
settings_flows: اتوماسیون
|
||||
settings_system_logs: لاگهای سیستم
|
||||
settings_presets: نشانکها
|
||||
settings_translations: ترجمهها
|
||||
one_or_more_options_are_missing: یک یا چند مورد از تنظیمات، وجود ندارد
|
||||
configure_field_key_to_continue: برای ادامه، کلید فیلد را تنظیم کنید
|
||||
scope: محدوده در دسترس
|
||||
actions: فعالیت ها
|
||||
select: انتخاب...
|
||||
@@ -1037,24 +1158,36 @@ page_help_files_collection: >-
|
||||
**مجموعه فایلها** - تمامی محتوای آپلود شده برای این پروژه را لیست میکند. تنظیمات مربوط به چیدمان، فیلترها و مرتب سازی را بر اساس نیاز خود ست کنید و حتی نشانکهایی از تنظیمات متفاوت را برای دسترسی سریعتر به آنها ذخیره کنید.
|
||||
page_help_files_item: >-
|
||||
**جزئیات فایل** - فرمی برای مدیریت متادیتای فایل، ویرایش محتوای اصلی و بروزرسانی تنظیمات دسترسی.
|
||||
page_help_settings_project: "**تنظیمات** — تنظیمات سراسری پروژه شما."
|
||||
page_help_settings_appearance: "**ظاهر** - ظاهر پروژه شما و تنظیمات قالببندی."
|
||||
page_help_settings_extensions: '**افزونهها** — افزونه نصبشده در پروژه شما.'
|
||||
page_help_settings_datamodel_collections: >-
|
||||
**مدل داده: مجموعه ها** - تمامی مجموعه های موجود را لیست می کند. این شامل مجموعه های پیدا و نهان و مجموعه های سیستمی و همچنین جداول پایگاه داده مدیریت نشده ای است که میتوانند اضافه شوند.
|
||||
page_help_settings_datamodel_fields: >-
|
||||
**مدل داده: مجموعه** - فرمی برای مدیریت مجموعه و فیلدهای آن.
|
||||
page_help_settings_system_logs: '**لاگهای سیستم** — دسترسی به جریان زنده لاگهای سیستم.'
|
||||
page_help_settings_policies_collection: '**مرور سیاستها** — همه سیاستهای داخل پروژه را فهرست میکند.'
|
||||
page_help_settings_policies_item: "**جزئیات سیاست** — مدیریت مجوزهای یک سیاست و سایر تنظیمات."
|
||||
page_help_settings_roles_collection: '**مرور نقشها** - تمام نقشهای کاربر در پروژه را لیست میکند.'
|
||||
page_help_settings_roles_item: "**جزئیات نقش** - مدیریت اجازه های یک نقش و سایر تنظیمات مربوط به آن."
|
||||
page_help_settings_presets_collection: >-
|
||||
**مرور الگوها** - لیستی از تمامی الگوهای موجود در پروژه شامل: کاربر، نقش و نشانک های بالاترین سطح بعلاوه نماهای پیش فرض.
|
||||
page_help_settings_presets_item: >-
|
||||
**جزئیات از پیش تعیین شده** - فرمی برای مدیریت بوکمارک ها و از پیش تنظیم های مجموعه پیش فرض.
|
||||
**جزئیات از پیش تعیین شده** - فرمی برای مدیریت نشانکها و از پیش تنظیمهای مجموعه پیش فرض.
|
||||
page_help_settings_webhooks_collection: '**مرور Webhooks** - فهرستی از تمام وبک هوک های داخل پروژه.'
|
||||
page_help_settings_webhooks_item: '** جزئیات وب هوک ** - فرمی برای ایجاد و مدیریت وب هوک های پروژه.'
|
||||
page_help_settings_flows_collection: '**مرور جریانها** - همه جریانهای داخل پروژه را فهرست می کند.'
|
||||
page_help_settings_flows_item: '**جزئیات جریان** - محیط کاری برای مدیریت یک یا چند عملیات.'
|
||||
page_help_settings_translations_collection: '**مرور ترجمههای سفارشی** — تمام ترجمههای سفارشی داخل پروژه را لیست میکند.'
|
||||
page_help_settings_translations_item: '**جزئیات ترجمههای سفارشی** — فرمی برای دیدن و مدیریت این مورد.'
|
||||
page_help_users_collection: '**دایرکتوری کاربر** - لیست تمامی کاربران سیستم در این پروژه.'
|
||||
page_help_users_item: >-
|
||||
**جزئیات کاربر** — اطلاعات حساب خود را مدیریت کنید یا جزئیات سایر کاربران را مشاهده کنید.
|
||||
page_help_insights_overview: '**اطلاعات** - لیست داشبوردهایی که به آنها دسترسی دارید.'
|
||||
page_help_insights_dashboard: '**داشبورد** - فضای کاری برای مدیریت یک یا چند پنل'
|
||||
page_help_marketplace_account: '**حساب** — ناشر یا نگهدارنده یک افزونه منتشر شده.'
|
||||
page_help_marketplace_registry: '**رجیستری** — افزونههای فعال در بازار دایرکتوس.'
|
||||
page_help_marketplace_extension: '**افزونه** — افزونه در بازار دایرکتوس.'
|
||||
activity_feed: گزارش فعالیت ها
|
||||
add_new: اضافه کردن
|
||||
create_new: ساخت آیتم جدید
|
||||
@@ -1065,7 +1198,17 @@ batch_delete_confirm: >-
|
||||
هیچ آیتمی انتخاب نشده است | آیا مطمئنید که میخواهید این آیتم را حذف کنید؟ این عمل غیرقابل بازگشت است. | آیا مطمئنید که میخواهید این {count} آیتم را پاک کنید؟ این عمل غیر قابل بازگشت است.
|
||||
cancel: انصراف
|
||||
no_upscale: تصاویر را بالا نبرید
|
||||
no_extensions: بدون افزونه
|
||||
no_extensions_copy: هنوز هیچ افزونهای نصب نشده است.
|
||||
install_extension: نصب افزونه
|
||||
uninstall_locked: نمیتوانید افزونهای را که از طریق بازار نصب نشده است پاک کنید
|
||||
uninstall: حذف نصب
|
||||
installed: نصب شده
|
||||
reinstall: نصب مجدد
|
||||
open_in_marketplace: باز کردن در بازار
|
||||
source_registry: بازار
|
||||
source_local: محلی
|
||||
source_module: ماژولهای نود
|
||||
collection: مجموعه
|
||||
collections: مجموعهها
|
||||
content: محتوا
|
||||
@@ -1073,8 +1216,10 @@ singleton: تک کالکشن یا Singleton
|
||||
singleton_label: به عنوان یک شی واحد رفتار کند
|
||||
system_fields_locked: فیلدهای سیستم قفل هستند و قابل ویرایش نیستند
|
||||
directus_collection:
|
||||
directus_access: پیوستهای سیاست
|
||||
directus_activity: گزارش های مسئولیت پذیری برای همه رویدادها
|
||||
directus_collections: پیکربندی مجموعه اضافی و ابرداده
|
||||
directus_comments: نظرات برای موارد
|
||||
directus_dashboards: داشبوردهای درون ماژول Insights
|
||||
directus_fields: پیکربندی فیلد اضافی و ابرداده
|
||||
directus_files: فراداده برای همه Assets های فایل مدیریت شده
|
||||
@@ -1084,7 +1229,9 @@ directus_collection:
|
||||
directus_notifications: اعلانهای ارسالی به کاربر
|
||||
directus_operations: عملیاتی که در جریانها اجرا می شوند
|
||||
directus_panels: پنلهای مجزا در داشبوردها
|
||||
directus_presets: تنظیمات پیشفرض برای مجموعه و بوکمارکها
|
||||
directus_permissions: مجوزهای دسترسی برای هر سیاست
|
||||
directus_policies: سیاستهای کنترل دسترسی
|
||||
directus_presets: تنظیمات پیشفرض برای مجموعه و نشانکها
|
||||
directus_relations: پیکربندی رابطه و داده متا
|
||||
directus_revisions: عکس های فوری داده برای همه فعالیت ها
|
||||
directus_roles: گروههای اجازه برای کاربران سیستم
|
||||
@@ -1094,6 +1241,8 @@ directus_collection:
|
||||
directus_users: کاربران سیستمی برای پلتفرم
|
||||
directus_webhooks: پیکربندی برای درخواست های HTTP مبتنی بر رویداد
|
||||
directus_translations: ترجمه های سفارشی
|
||||
directus_versions: نسخههای محتوا برای موارد
|
||||
directus_extensions: تنظیمات افزونهها
|
||||
fields:
|
||||
directus_activity:
|
||||
item: کلید اصلی آیتم
|
||||
@@ -1120,6 +1269,7 @@ fields:
|
||||
unarchive_value: مقدار بایگانی نشده
|
||||
sort_field: فیلد مرتب سازی
|
||||
accountability: مشاهده Log ها و فعالیت ها
|
||||
versioning: نسخهبندی
|
||||
archive_field: فیلد بایگانی
|
||||
item_duplication_fields: فیلدهای تکراری
|
||||
directus_files:
|
||||
@@ -1146,6 +1296,8 @@ fields:
|
||||
height: ارتفاع
|
||||
charset: مجموعه کاراکتر
|
||||
duration: مدت زمان
|
||||
focal_point_x: مختصات افقی
|
||||
focal_point_y: مختصات عمودی
|
||||
directus_users:
|
||||
first_name: نام
|
||||
last_name: نام خانوادگی
|
||||
@@ -1158,11 +1310,13 @@ fields:
|
||||
tags: برچسب ها
|
||||
user_preferences: تنظیمات مربوط به کاربر
|
||||
language: زبان
|
||||
text_direction: جهت متن
|
||||
tfa_secret: احراز هویت دو عاملی
|
||||
admin_options: تنظیمات ادمین
|
||||
status: وضعیت
|
||||
status_draft: پیشنویس
|
||||
status_invited: دعوت شده
|
||||
status_unverified: تایید نشده
|
||||
status_active: فعال
|
||||
status_suspended: تعلیق شده
|
||||
status_archived: بایگانی شده
|
||||
@@ -1173,12 +1327,18 @@ fields:
|
||||
last_page: صفحه آخر
|
||||
last_access: آخرین دسترسی
|
||||
email_notifications: اعلان های ایمیلی
|
||||
appearance: ظاهر
|
||||
theme_light: پوسته روشن
|
||||
theme_dark: پوسته تیره
|
||||
theme_light_overrides: سفارشیسازی پوسته روشن
|
||||
theme_dark_overrides: سفارشیسازی پوسته تیره
|
||||
directus_settings:
|
||||
jpg: JPEG
|
||||
png: PNG
|
||||
webP: WebP
|
||||
tiff: Tiff
|
||||
avif: AVIF
|
||||
reporting: گزارشگیری
|
||||
mapping: نقشه برداری
|
||||
basemaps: Basemap
|
||||
basemaps_raster: Raster
|
||||
@@ -1196,9 +1356,14 @@ fields:
|
||||
project_color: رنگ پروژه
|
||||
project_logo: نماد پروژه
|
||||
default_language: زبان پیشفرض
|
||||
branding: برندسازی
|
||||
public_foreground: رنگ پیش زمینه عمومی
|
||||
public_background: رنگ پس زمینه عمومی
|
||||
public_note: یادداشت عمومی
|
||||
theming: پیشفرضهای پوسته
|
||||
default_appearance: ظاهر پیشفرض
|
||||
default_theme_light: پوسته روشن پیشفرض
|
||||
default_theme_dark: پوسته تیره پیشفرض
|
||||
auth_password_policy: سیاست کلمهی عبور جدید
|
||||
auth_login_attempts: حداکثر تعداد ورود ناموفق
|
||||
files_and_thumbnails: فایلها و ذخیره سازی
|
||||
@@ -1213,6 +1378,17 @@ fields:
|
||||
transformations_presets: تبدیلها را به تنظیمات پیشفرض زیر محدود کنید
|
||||
image_editor: ویرایشگر تصویر
|
||||
custom_aspect_ratios: نسبت تصویر سفارشی
|
||||
theme_light_overrides: سفارشیسازی پوسته روشن
|
||||
theme_dark_overrides: سفارشیسازی پوسته تیره
|
||||
public_registration: ثبت نام کاربر
|
||||
public_registration_note: به کاربران اجازه میدهد از طریق [نشانی ثبت نام](/admin/register) ثبت نام کنند.
|
||||
public_registration_role: نقش کاربر
|
||||
public_registration_role_note: این نقش به کاربرانی که با استفاده از این روش ثبت نام کنند تعلق میگیرد و تاثیری روی کاربرانی که قبلا ثبت نام کردهاند ندارد.
|
||||
public_registration_email_filter: فیلتر نشانی ایمیل
|
||||
public_registration_email_filter_note: فقط نشانیهای ایمیل که با این فیلتر منطبق باشند میتوانند ثبت نام کنند.
|
||||
public_registration_verify_email: تایید ایمیل
|
||||
public_registration_verify_email_note: ایمیلی برای کاربر در حال ثبت نام میفرستد که از آنها میخواهد روی یک لینک تایید کلیک کنند تا به آنها اجازه ثبت نام داده شود.
|
||||
visual_editor_urls: URLهای ویرایشگر بصری
|
||||
directus_shares:
|
||||
name: نام
|
||||
role: نقش
|
||||
@@ -1234,9 +1410,15 @@ fields:
|
||||
icon: آیکن نقش
|
||||
description: توضیحات
|
||||
users: کاربران دارای این نقش
|
||||
parent: نقش والد
|
||||
children: نقشهای فرزند
|
||||
directus_policies:
|
||||
name: نام خط مشی
|
||||
description: توضیحات
|
||||
app_access: دسترسی به برنامه
|
||||
admin_access: دسترسی مدیر
|
||||
ip_access: محدودیت IP
|
||||
enforce_tfa: اجبار به احراز هویت دو عاملی
|
||||
directus_webhooks:
|
||||
name: نام
|
||||
method: شیوه یا متود
|
||||
@@ -1253,6 +1435,8 @@ field_options:
|
||||
directus_settings:
|
||||
project_name_placeholder: پروژه من...
|
||||
project_color_note: نماد برگه ورود
|
||||
project_logo_note: White 40x40 SVG/PNG
|
||||
project_favicon_note: 32×32 ICO
|
||||
public_note_placeholder: یک پیام کوتاه و عمومی که از قالب بندی علامت گذاری پشتیبانی می کند...
|
||||
security_divider_title: امنیت
|
||||
auth_password_policy:
|
||||
@@ -1307,13 +1491,22 @@ field_options:
|
||||
accountability_divider: مسئوليت
|
||||
duplication_divider: تکراری
|
||||
preview_divider: پيش نمايش
|
||||
content_versioning_divider: نسخهبندی محتوا
|
||||
enable_versioning: فعالسازی نسخهبندی
|
||||
directus_files:
|
||||
title: یک عنوان منحصر به فرد
|
||||
description: توضیحات اختیاری...
|
||||
location: موقعیت اختیاری...
|
||||
storage_divider: نحوه نام گذاری فایل
|
||||
focal_point_divider: نقطه کانونی
|
||||
filename_disk: نام برروی دیسک...
|
||||
filename_download: نام فایل به هنگام دانلود...
|
||||
directus_policies:
|
||||
name: نامی یکتا برای این سیاست...
|
||||
description: توضیحاتی برای این سیاست...
|
||||
ip_access: نشانی IP ها، بازههای IP و قطعات CIDR مجاز را اضافه کنید. برای اجازه دادن به همه، خالی بگذارید...
|
||||
enforce_tfa: اجبار به احراز هویت دو عاملی
|
||||
assigned_to: منتسب به
|
||||
directus_roles:
|
||||
name: نامی یکتا برای این نقش...
|
||||
description: توضیحات مربوط به این نقش...
|
||||
@@ -1323,6 +1516,8 @@ field_options:
|
||||
name_placeholder: عنوانی را وارد نمایید...
|
||||
link_name: لینک
|
||||
link_placeholder: لینک نسبی یا کامل...
|
||||
parent_note: نقش والد اختیاری که این نقش، دسترسیها را از آن به ارث میبرد
|
||||
children_note: نقشهای فرزند تو در تو که از دسترسیهای این نقش ارثبری میکنند
|
||||
collections_name: مجموعهها
|
||||
collections_addLabel: مجموعه جدید...
|
||||
directus_users:
|
||||
@@ -1371,8 +1566,10 @@ live_preview:
|
||||
new_window: باز کردن در پنجره جدید
|
||||
close_window: حالت توو در توو
|
||||
toggle_3d: حالت سه بعدی
|
||||
iframe_title: پیش نمایش زنده وبسایت
|
||||
shares: اشتراکگذاری
|
||||
unlimited_usage: Unlimited usage
|
||||
uses_left: هیچ استفادهای نمانده | یک استفاده مانده | {n} استفاده مانده
|
||||
no_shares: هیچ اشتراک گذاری برای نمایش نیست
|
||||
new_share: اشتراک گذاری جدید
|
||||
expired: منقضی شده
|
||||
@@ -1381,6 +1578,7 @@ share: اشتراکگذاری
|
||||
share_item: اشتراک گذاری مورد
|
||||
shared_with_you: موردی با شما به اشتراک گذاشته شد
|
||||
shared_enter_passcode: برای ادامه رمز خود را وارد نمائید
|
||||
shared_leave_blank_for_passwordless_access: برای دسترسی بدون رمز عبور، خالی بگذارید
|
||||
shared_leave_blank_for_unlimited: (خالی به معنی بدون محدودیت.)
|
||||
shared_times_remaining: This link can only be used {n} times
|
||||
shared_last_remaining: This link can only be used once
|
||||
@@ -1403,8 +1601,11 @@ referential_action_set_default: Set {field} to its default value
|
||||
choose_action: انتخاب فعالیت
|
||||
continue_label: ادامه
|
||||
continue_as: >-
|
||||
{name} is currently authenticated. If you recognize this account, press continue.
|
||||
{name} در حال حاضر احراز هویت شده است. اگر این حساب را میشناسید، روی ادامه بزنید.
|
||||
editing_role: 'نقشِ {role}'
|
||||
editing_policy: 'سیاست {policy}'
|
||||
no_permissions: بدون مجوز
|
||||
permission_add_collection: ایجاد مجموعه
|
||||
creating_webhook: ساخت وب هوک
|
||||
default_label: پیشفرض
|
||||
delete_label: حذف
|
||||
@@ -1450,6 +1651,7 @@ authenticated: احراز هویت شده
|
||||
options: گزینهها
|
||||
otp: رمز عبور یکبار مصرف
|
||||
password: رمز عبور
|
||||
confirm_password: تایید رمز عبور
|
||||
permissions: دسترسي ها
|
||||
relationship: رابطه ها
|
||||
reset: بازنشانی
|
||||
@@ -1457,6 +1659,7 @@ reset_password: بازنشانی رمز عبور
|
||||
revisions: تجدید نظرها
|
||||
no_revisions: هنوز هیچ تجدیدنظری وجود ندارد
|
||||
no_logs: هنوز گزارشی نیست
|
||||
show_failed_only: فقط نمایش ناموفقها
|
||||
revert: بازگشت
|
||||
save: ذخیره
|
||||
schema: اسکیما
|
||||
@@ -1464,7 +1667,16 @@ search: جستوجو
|
||||
select_existing: انتخاب مورد موجود
|
||||
select_field_type: انتخاب نوع فیلد
|
||||
select_interface: رابط شبکه را انتخاب کنید
|
||||
select_display: یک روش نمایش را انتخاب کنید
|
||||
settings: تنظیمات
|
||||
register: ثبت نام
|
||||
registration_successful_headline: موفق!
|
||||
registration_successful_note: اکنون میتوانید به صفحه ورود بروید.
|
||||
registration_successful_check_email_note: |
|
||||
لطفا توجه کنید که قبل از این که بتوانید ثبت نام کنید، نیاز است نشانی ایمیل خود را با کلیک روی لینک فعالسازی ارسال شده برای شما تایید کنید.
|
||||
dont_have_an_account: حساب ندارید؟
|
||||
already_have_an_account: قبلا حساب داشتید؟
|
||||
sign_up_now: حالا ثبت نام کنید
|
||||
sign_in: ورود
|
||||
sign_out: خروج از سیستم
|
||||
sign_out_confirm: آیا اطمینان دارید که میخواهید خارج شوید؟
|
||||
@@ -1493,7 +1705,12 @@ navigate_to_item: بازگشت به آیتم
|
||||
undo_removed_item: لغو حذف آیتم
|
||||
remove_item: حذف آیتم
|
||||
delete_item: پاککردن آیتم
|
||||
pause: توقف
|
||||
resume: ادامه
|
||||
interfaces:
|
||||
list-m2a:
|
||||
prefix_note: به طور پیشفرض از نام مجموعه مورد مرتبط استفاده میکند.
|
||||
sorting_disabled: مرتبسازی غیرفعال شده است؛ چون تعداد موارد بیشتر از آن است که در یک صفحه بگنجد.
|
||||
filter:
|
||||
name: فیلتر
|
||||
description: یک فیلتر را پیکربندی کنید.
|
||||
@@ -1551,11 +1768,11 @@ interfaces:
|
||||
placeholder: کد را اینجا وارد کنید...
|
||||
system-collection:
|
||||
collection: کالکشن یا مجموعه
|
||||
description: بین کالکشن های موجود انتخاب کنید
|
||||
description: بین مجموعههای موجود انتخاب کنید
|
||||
include_system_collections: شامل کالکشن های سیستمی
|
||||
system-collections:
|
||||
collections: کالکشن ها یا مجموعه ها
|
||||
description: بین کالکشن های موجود انتخاب کنید
|
||||
description: بین مجموعههای موجود انتخاب کنید
|
||||
include_system_collections: شامل کالکشن های سیستمی
|
||||
select-color:
|
||||
color: رنگ
|
||||
@@ -1622,15 +1839,21 @@ interfaces:
|
||||
description: Select or upload an image
|
||||
crop: تناسب با برش تصویر
|
||||
crop_label: برش تصویر در صورت نیاز
|
||||
letterbox_label: اضافه کردن حاشیه به پیشنمایش تصویر
|
||||
system-interface:
|
||||
interface: رابط کاربری
|
||||
description: انتخاب یک رابط موجود
|
||||
description: از رابطهای موجود انتخاب کنید
|
||||
placeholder: Select an interface...
|
||||
system-interface-options:
|
||||
interface-options: تنظیمات رابط
|
||||
description: A modal for selecting options of an interface
|
||||
system-display:
|
||||
display: نحوه نمایش
|
||||
description: از نحوه نمایشهای موجود انتخاب کنید
|
||||
placeholder: یک روش نمایش را انتخاب کنید...
|
||||
system-display-options:
|
||||
display-options: تنظیمات نحوه نمایش
|
||||
description: یک پنجره برای انتخاب تنظیمات یک روش نمایش
|
||||
list-m2m:
|
||||
many-to-many: چندتا به چندتا
|
||||
description: انتخاب چندین آیتم تقاطعی مرتبط
|
||||
@@ -1651,6 +1874,7 @@ interfaces:
|
||||
editorFont: Editor Font
|
||||
previewFont: Preview Font
|
||||
default_view: Default View
|
||||
default_view_editor: ویرایشگر
|
||||
default_view_preview: پيش نمايش
|
||||
customSyntax: بلوکهای سفارشی
|
||||
customSyntax_label: Add custom syntax types
|
||||
@@ -1685,6 +1909,7 @@ interfaces:
|
||||
description: انتخاب چندین آیتم مرتبط
|
||||
no_collection: گزینه مورد نظر یافت نشد.
|
||||
system-folder:
|
||||
folder: پوشه اصلی
|
||||
description: یک پوشه انتخاب کنید
|
||||
field_hint: Default folder for uploaded files without a folder specified. Does not affect existing files.
|
||||
root_name: پوشه اصلی مجموعه فایلها
|
||||
@@ -1702,6 +1927,7 @@ interfaces:
|
||||
field_note_placeholder: Enter field note...
|
||||
incompatible_data: The current data is not compatible with the repeater interface and will be overridden when adding items to the repeater
|
||||
interface_group: تنظیمات رابط
|
||||
display_group: تنظیمات نحوه نمایش
|
||||
slider:
|
||||
slider: اسلایدر
|
||||
description: یک عدد را با استفاده از نوار متحرک انتخاب کنید
|
||||
@@ -1778,8 +2004,10 @@ interfaces:
|
||||
folder_note: Folder for uploaded files. Does not affect existing files.
|
||||
imageToken: Static Access Token
|
||||
imageToken_label: Static access token is appended to the assets' URL
|
||||
media_preview_iframe_title: پیشنمایش رسانه
|
||||
input-block-editor:
|
||||
input-block-editor: ویرایشگر بلوک
|
||||
description: ویرایشگر قطعهای برای محتوای غنی، دادههای تمیز را توسط Editor.js در قالب JSON خروجی میدهد
|
||||
tools: نوار ابزار
|
||||
tools_options:
|
||||
header: سربرگ
|
||||
@@ -1829,6 +2057,9 @@ interfaces:
|
||||
system-raw-editor:
|
||||
system-raw-editor: ویرایشگر خام
|
||||
description: Allow entering of raw or mustache templating values
|
||||
input-password:
|
||||
input-password: ورود رمز عبور
|
||||
description: ورودی پسورد با دکمهای برای مخفی کردن مقدار
|
||||
displays:
|
||||
translations:
|
||||
translations: ترجمهها
|
||||
@@ -2020,6 +2251,7 @@ layouts:
|
||||
no_group: بدون گروه
|
||||
horizontal: افقی
|
||||
vertical: عمودی
|
||||
axis: محور
|
||||
x_axis: محور افقی
|
||||
y_axis: محور عمودی
|
||||
y_axis_function: تابع Y-Axis
|
||||
@@ -2040,12 +2272,17 @@ show_labels: نمایش برچسب
|
||||
right: راست
|
||||
left: چپ
|
||||
start: آغاز
|
||||
end: پایان
|
||||
donut: بخشبندی
|
||||
continuous: ادامهدار
|
||||
gap: شکاف
|
||||
panels:
|
||||
metric:
|
||||
name: معیار
|
||||
description: یک مقدار را بر اساس یک پرس و جو نشان دهید
|
||||
field: فیلد
|
||||
metric_list:
|
||||
name: فهرست شاخصها
|
||||
time_series:
|
||||
name: سری زمانی
|
||||
description: نمودار خطی را بر اساس مقادیر در طول زمان ارائه دهید
|
||||
@@ -2053,6 +2290,7 @@ panels:
|
||||
value_field: مقدار فیلد
|
||||
fill_type: نوع انباشت
|
||||
curve_type: نوع منحنی
|
||||
missing_data: داده گمشده
|
||||
label:
|
||||
name: برچسب
|
||||
description: نمایش چند نوشته
|
||||
@@ -2074,6 +2312,7 @@ panels:
|
||||
smooth: نرم
|
||||
straight: صاف
|
||||
step_line: خط مرحله ای
|
||||
show_legend: نمایش علائم
|
||||
meter:
|
||||
name: متر
|
||||
description: شمارنده برای ردیابی مقادیر
|
||||
@@ -2097,6 +2336,7 @@ triggers:
|
||||
name: هوک رویداد
|
||||
webhook:
|
||||
name: وبهوک
|
||||
error_on_reject: خطا هنگام رد درخواست
|
||||
description: روی درخواست های ورودی HTTP اجرا میشود
|
||||
method: شیوه یا متود
|
||||
async: ناهمگام
|
||||
@@ -2111,7 +2351,9 @@ triggers:
|
||||
manual:
|
||||
name: دستی
|
||||
description: در اجراهای دستی روی مجموعههای انتخاب شده اجرا میشود
|
||||
collection_and_item: صفحات مجموعه و مورد
|
||||
collection_only: فقط برگه مجموعه
|
||||
item_only: فقط صفحه مورد
|
||||
collection_page: برگه مجموعه
|
||||
require_selection: نیاز به انتخاب دارد
|
||||
a_flow_uuid: یک جریان را جهت راهاندازی انتخاب کنید...
|
||||
@@ -2122,7 +2364,7 @@ operations:
|
||||
name: شرط
|
||||
description: مسیریابی یک جریان بر اساس منطق If / Else
|
||||
exec:
|
||||
name: خواندن داده
|
||||
name: اجرای اسکریپت
|
||||
description: خواندن داده از پایگاه داده
|
||||
modules: 'میتوان از **ماژولهای Node** مقابل بهکارگیری کرد:'
|
||||
item-create:
|
||||
@@ -2150,9 +2392,14 @@ operations:
|
||||
payload: ظرفیت یا Payload
|
||||
query: کوئری
|
||||
json-web-token:
|
||||
operation: عملیات
|
||||
secret: سکرت
|
||||
secret_placeholder: سکرت را وارد کنید...
|
||||
payload: ظرفیت یا Payload
|
||||
token: توکن
|
||||
token_placeholder: eyJhbGciOi......
|
||||
options: گزینهها
|
||||
options_placeholder: 'برای مشاهده گزینههای موجود به https://www.npmjs.com/package/jsonwebtoken#usage مراجعه کنید'
|
||||
log:
|
||||
name: ذخیره رخداد در کنسول
|
||||
description: خروجی چیزی به کنسول
|
||||
@@ -2162,15 +2409,23 @@ operations:
|
||||
name: فرستادن رایانامه
|
||||
description: فرستادن رایانامه به یک یا چند نفر
|
||||
to: به
|
||||
to_placeholder: نشانیهای ایمیل را اضافه کنید و اینتر بزنید...
|
||||
body: متن
|
||||
template: پوسته
|
||||
data: داده Data
|
||||
cc: CC
|
||||
cc_placeholder: نشانیهای ایمیل CC را اضافه کنید و اینتر بزنید...
|
||||
bcc: BCC
|
||||
bcc_placeholder: نشانیهای ایمیل BCC را اضافه کنید و اینتر بزنید...
|
||||
reply_to: پاسخ به
|
||||
reply_to_placeholder: نشانیهای ایمیل «پاسخ به» را اضافه کنید و اینتر بزنید...
|
||||
notification:
|
||||
name: فرستادن اعلان
|
||||
description: فرستادن اعلان درون برنامهای برای یک یا چند کاربر
|
||||
recipient: کاربر
|
||||
recipient_placeholder: یک UUID وارد کنید...
|
||||
recipient_note: UUID کاربر را وارد و اینتر بزنید...
|
||||
item_note: کلید مورد یا URL نسبی
|
||||
message: پیام
|
||||
subject: موضوع
|
||||
request:
|
||||
@@ -2204,14 +2459,47 @@ operations:
|
||||
batch: دسته
|
||||
batch_size: اندازه دسته
|
||||
cache: Cache
|
||||
unit: واحد
|
||||
standard: استاندارد
|
||||
scientific: علمی
|
||||
engineering: مهندسی
|
||||
compact: جمع و جور
|
||||
currency: واحد پول
|
||||
style: استایل
|
||||
fonts:
|
||||
thin: نازک
|
||||
extra_light: خیلی نازک
|
||||
light: نازک
|
||||
normal: عادی
|
||||
medium: میانه
|
||||
semi_bold: نیمه ضخیم
|
||||
bold: توپُر
|
||||
extra_bold: خیلی ضخیم
|
||||
black: سیاه
|
||||
italic: ایتالیک
|
||||
oblique: کج
|
||||
small: کوچک
|
||||
large: بزرگ
|
||||
auto: Auto
|
||||
center: وسط
|
||||
justify: همتراز کردن
|
||||
text_align: چیدمان متن
|
||||
font_weight: ضخامت قلم
|
||||
font_style: سبک قلم
|
||||
font_size: اندازه قلم
|
||||
minimum_fraction_digits: حداقل اعشار
|
||||
maximum_fraction_digits: حداکثر اعشار
|
||||
show_percentage: نمایش درصد
|
||||
aggregation: تجمیع
|
||||
date_precision: دقت داده
|
||||
export_dashboard: صدور داشبورد
|
||||
search_dashboard: جستجوی داشبورد...
|
||||
search_flow: جستجوی جریان...
|
||||
percent: درصد
|
||||
show_password: نمایش رمز عبور
|
||||
hide_password: مخفی کردن رمز عبور
|
||||
bsl_banner:
|
||||
welcome_to_directus: به دایرکتوس خوش آمدید!
|
||||
text_direction_auto: خودکار (مطابق زبان)
|
||||
text_direction_ltr: چپ به راست (LTR)
|
||||
text_direction_rtl: راست به چپ (RTL)
|
||||
|
||||
@@ -46,6 +46,8 @@ location: 위치
|
||||
collection_names_are_case_sensitive: 컬렉션 이름은 대소문자를 구분합니다
|
||||
condition_rules: 조건식
|
||||
input: 입력
|
||||
invalid_input: 잘못된 입력
|
||||
not_a_number: 숫자가 아닙니다
|
||||
maps: 지도
|
||||
switch_user: 사용자 전환
|
||||
item_creation: 추가
|
||||
@@ -1335,6 +1337,7 @@ fields:
|
||||
tags: 태그(Tags)
|
||||
user_preferences: 개인 설정
|
||||
language: 언어
|
||||
text_direction: 텍스트 방향
|
||||
tfa_secret: 이중 인증
|
||||
admin_options: 관리 옵션
|
||||
status: 상태
|
||||
@@ -1590,6 +1593,7 @@ live_preview:
|
||||
new_window: 새 창에서 열기
|
||||
close_window: 분할 화면에서 열기
|
||||
toggle_3d: 토글 3D
|
||||
iframe_title: 라이브 웹사이트 미리보기
|
||||
shares: 공유
|
||||
unlimited_usage: 무제한 사용
|
||||
uses_left: 남은 용도 없음 | 1회 사용 남음 | {n} 왼쪽 사용
|
||||
@@ -2032,6 +2036,7 @@ interfaces:
|
||||
folder_note: 업로드된 파일의 폴더입니다. 기존 파일에는 영향을 주지 않습니다.
|
||||
imageToken: 정적 액세스 토큰
|
||||
imageToken_label: 정적 액세스 토큰이 자산의 URL에 추가됩니다.
|
||||
media_preview_iframe_title: 미디어 미리보기
|
||||
input-block-editor:
|
||||
input-block-editor: 블록 편집기
|
||||
description: Editor.js를 사용하여 리치 미디어 스토리를 위한 블록 스타일 편집기, JSON 형식의 깨끗한 데이터를 출력합니다.
|
||||
@@ -2554,3 +2559,6 @@ bsl_banner:
|
||||
accept_terms: 승인
|
||||
get_license: 라이선스 받기
|
||||
accepted: 수락됨
|
||||
text_direction_auto: 자동(일치 언어)
|
||||
text_direction_ltr: 왼쪽에서 오른쪽으로(LTR)
|
||||
text_direction_rtl: 오른쪽에서 왼쪽(RTL)
|
||||
|
||||
@@ -213,8 +213,6 @@ function useDeleteBookmark() {
|
||||
--v-icon-color: var(--theme--foreground-subdued);
|
||||
|
||||
opacity: 0;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition: opacity var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
@@ -223,8 +221,6 @@ function useDeleteBookmark() {
|
||||
&:focus-visible .ctx-toggle,
|
||||
&:hover .ctx-toggle {
|
||||
opacity: 1;
|
||||
-webkit-user-select: auto;
|
||||
user-select: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -436,6 +436,10 @@ const refreshInterval = computed({
|
||||
inset-block-start: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
html[dir='rtl'] & {
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
|
||||
&.header-offset {
|
||||
inset-block-start: calc(50% - 12px);
|
||||
}
|
||||
|
||||
@@ -90,15 +90,15 @@ const steps = computed(() => {
|
||||
|
||||
<div class="inset">
|
||||
<v-detail v-if="triggerData.options" :label="t('options')">
|
||||
<pre class="json selectable">{{ triggerData.options }}</pre>
|
||||
<pre class="json">{{ triggerData.options }}</pre>
|
||||
</v-detail>
|
||||
|
||||
<v-detail v-if="triggerData.trigger" :label="t('payload')">
|
||||
<pre class="json selectable">{{ triggerData.trigger }}</pre>
|
||||
<pre class="json">{{ triggerData.trigger }}</pre>
|
||||
</v-detail>
|
||||
|
||||
<v-detail v-if="triggerData.accountability" :label="t('accountability')">
|
||||
<pre class="json selectable">{{ triggerData.accountability }}</pre>
|
||||
<pre class="json">{{ triggerData.accountability }}</pre>
|
||||
</v-detail>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,11 +114,11 @@ const steps = computed(() => {
|
||||
|
||||
<div class="inset">
|
||||
<v-detail v-if="step.options" :label="t('options')">
|
||||
<pre class="json selectable">{{ step.options }}</pre>
|
||||
<pre class="json">{{ step.options }}</pre>
|
||||
</v-detail>
|
||||
|
||||
<v-detail v-if="step.data !== null" :label="t('payload')">
|
||||
<pre class="json selectable">{{ step.data }}</pre>
|
||||
<pre class="json">{{ step.data }}</pre>
|
||||
</v-detail>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -172,7 +172,7 @@ function saveOperation() {
|
||||
<v-icon name="vpn_key" />
|
||||
</template>
|
||||
</v-input>
|
||||
<small v-if="!isOperationKeyUnique" class="error selectable">{{ t('operation_key_unique_error') }}</small>
|
||||
<small v-if="!isOperationKeyUnique" class="error">{{ t('operation_key_unique_error') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -413,10 +413,10 @@ function pointerLeave() {
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
padding-inline-start: 60px;
|
||||
transform: translate(-1px, -50%);
|
||||
transform: translate(-1px, calc(-50% - 2.5px));
|
||||
|
||||
html[dir='rtl'] & {
|
||||
transform: translate(1px, -50%);
|
||||
transform: translate(1px, calc(-50% - 2.5px));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const { isCopySupported, copyToClipboard } = useClipboard();
|
||||
<div v-tooltip="panel.key" class="name">
|
||||
{{ panel.id === '$trigger' ? t(`triggers.${panel.type}.name`) : panel.name }}
|
||||
</div>
|
||||
<dl class="options-overview selectable">
|
||||
<dl class="options-overview">
|
||||
<div
|
||||
v-for="{ label, text, copyable } of translate(currentOperation?.overview(panel.options ?? {}, { flow }))"
|
||||
:key="label"
|
||||
|
||||
@@ -17,11 +17,6 @@ const { t } = useI18n();
|
||||
@use '@/styles/mixins';
|
||||
|
||||
.readme {
|
||||
:deep(*) {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
:deep() {
|
||||
@include mixins.markdown;
|
||||
}
|
||||
|
||||
103
app/src/operations/throw-error/index.ts
Normal file
103
app/src/operations/throw-error/index.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ErrorCode, InternalServerError } from '@directus/errors';
|
||||
import { defineOperationApp } from '@directus/extensions';
|
||||
|
||||
const FALLBACK_ERROR = new InternalServerError();
|
||||
|
||||
export default defineOperationApp({
|
||||
id: 'throw-error',
|
||||
icon: 'error',
|
||||
name: '$t:operations.throw-error.name',
|
||||
description: '$t:operations.throw-error.description',
|
||||
overview: ({ code, status, message }) => [
|
||||
{
|
||||
label: '$t:operations.throw-error.code',
|
||||
text: code ?? FALLBACK_ERROR.code,
|
||||
},
|
||||
{
|
||||
label: '$t:operations.throw-error.status',
|
||||
text: status ?? FALLBACK_ERROR.status.toString(),
|
||||
},
|
||||
{
|
||||
label: '$t:operations.throw-error.message',
|
||||
text: message ?? FALLBACK_ERROR.message,
|
||||
},
|
||||
],
|
||||
options: () => [
|
||||
{
|
||||
field: 'code',
|
||||
name: '$t:operations.throw-error.code',
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'full',
|
||||
interface: 'select-dropdown',
|
||||
options: {
|
||||
choices: Object.values(ErrorCode).map((code) => ({
|
||||
text: code,
|
||||
value: code,
|
||||
})),
|
||||
allowOther: true,
|
||||
placeholder: FALLBACK_ERROR.code,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: '$t:operations.throw-error.status',
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'full',
|
||||
interface: 'select-dropdown',
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
text: '400 (Bad Request)',
|
||||
value: '400',
|
||||
},
|
||||
{
|
||||
text: '401 (Unauthorized)',
|
||||
value: '401',
|
||||
},
|
||||
{
|
||||
text: '403 (Forbidden)',
|
||||
value: '403',
|
||||
},
|
||||
{
|
||||
text: '404 (Not Found)',
|
||||
value: '404',
|
||||
},
|
||||
{
|
||||
text: '405 (Method Not Allowed)',
|
||||
value: '405',
|
||||
},
|
||||
{
|
||||
text: '422 (Unprocessable Entity)',
|
||||
value: '422',
|
||||
},
|
||||
{
|
||||
text: '429 (Too Many Requests)',
|
||||
value: '429',
|
||||
},
|
||||
{
|
||||
text: '500 (Internal Server Error)',
|
||||
value: '500',
|
||||
},
|
||||
],
|
||||
allowOther: true,
|
||||
placeholder: FALLBACK_ERROR.status.toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
name: '$t:operations.throw-error.message',
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'full',
|
||||
interface: 'input',
|
||||
options: {
|
||||
placeholder: FALLBACK_ERROR.message,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -101,7 +101,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<div
|
||||
ref="labelContainer"
|
||||
class="label type-title selectable"
|
||||
class="label type-title"
|
||||
:class="[font, { 'has-header': showHeader }]"
|
||||
:style="{ color: color }"
|
||||
>
|
||||
|
||||
@@ -60,7 +60,6 @@ async function saveEdits(item: Record<string, any>) {
|
||||
<v-list-item
|
||||
v-for="row in data"
|
||||
:key="row[primaryKeyField]"
|
||||
class="selectable"
|
||||
:clickable="linkToItem === true"
|
||||
@click="startEditing(row)"
|
||||
>
|
||||
|
||||
@@ -204,7 +204,7 @@ const color = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="labelContainer" class="metric type-title selectable" :class="[font, { 'has-header': showHeader }]">
|
||||
<div ref="labelContainer" class="metric type-title" :class="[font, { 'has-header': showHeader }]">
|
||||
<p
|
||||
ref="labelText"
|
||||
class="metric-text"
|
||||
|
||||
@@ -76,7 +76,7 @@ useHead({
|
||||
</div>
|
||||
<div>
|
||||
<canvas :id="canvasID" class="qr" />
|
||||
<output class="secret selectable">{{ secret }}</output>
|
||||
<output class="secret">{{ secret }}</output>
|
||||
<v-input ref="inputOTP" v-model="otp" type="text" :placeholder="t('otp')" :nullable="false" />
|
||||
<v-error v-if="error" :error="error" />
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
tab-size: 2;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
|
||||
&::-moz-focus-inner,
|
||||
@@ -68,19 +66,6 @@ body,
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
[contenteditable],
|
||||
.selectable {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
|
||||
* {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
direction: ltr;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ function useEdits() {
|
||||
@cancel="cancelEditing"
|
||||
/>
|
||||
|
||||
<div v-else v-md="{ value: comment.display, target: '_blank' }" class="content selectable" />
|
||||
<div v-else v-md="{ value: comment.display, target: '_blank' }" class="content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -144,8 +144,6 @@ function useEdits() {
|
||||
line-height: 1;
|
||||
background: var(--theme--primary-background);
|
||||
border-radius: var(--theme--border-radius);
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ const done = async () => {
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p class="title selectable">{{ title }}</p>
|
||||
<p v-if="text" class="text selectable">{{ text }}</p>
|
||||
<p class="title">{{ title }}</p>
|
||||
<p v-if="text" class="text">{{ text }}</p>
|
||||
</div>
|
||||
|
||||
<v-icon
|
||||
|
||||
@@ -425,15 +425,8 @@ function clearFilters() {
|
||||
.message {
|
||||
inline-size: 100%;
|
||||
margin-block-start: 8px;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
cursor: auto;
|
||||
|
||||
:deep(*) {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
:deep() {
|
||||
@include mixins.markdown;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ const items = computed(() => allItems.filter((item) => item.key !== section));
|
||||
&.center {
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
html[dir='rtl'] & {
|
||||
transform: translate(50%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ flag_management:
|
||||
|
||||
comment:
|
||||
layout: 'header, diff, flags'
|
||||
require_changes: 'coverage_drop AND uncovered_patch'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "11.10.0",
|
||||
"version": "11.10.2",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
@@ -35,7 +35,8 @@ $15/month.
|
||||
|
||||
[The Directus Documentation](https://docs.directus.io) is a great place to start, or explore these other channels:
|
||||
|
||||
- [Discord](https://directus.chat) (Questions, Live Discussions)
|
||||
- [Community](https://community.directus.io) (Questions, Discussions)
|
||||
- [Discord](https://directus.chat) (Live Chat)
|
||||
- [GitHub Issues](https://github.com/directus/directus/issues) (Report Bugs)
|
||||
- [GitHub Discussions](https://github.com/directus/directus/discussions) (Feature Requests)
|
||||
- [Twitter](https://twitter.com/directus) (Latest News)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/composables",
|
||||
"version": "11.2.1",
|
||||
"version": "11.2.2",
|
||||
"description": "Shared Vue composables for Directus use",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/constants",
|
||||
"version": "13.0.1",
|
||||
"version": "13.0.2",
|
||||
"description": "Shared constants for Directus",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-directus-extension",
|
||||
"version": "11.0.16",
|
||||
"version": "11.0.17",
|
||||
"description": "A small util that will scaffold a Directus extension",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-directus-project",
|
||||
"version": "12.0.1",
|
||||
"version": "12.0.2",
|
||||
"description": "A small installer util that will create a directory, add boilerplate folders, and install Directus through npm",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
2
packages/env/package.json
vendored
2
packages/env/package.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/env",
|
||||
"version": "5.1.1",
|
||||
"version": "5.1.2",
|
||||
"description": "Utilities around using global env configuration",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/errors",
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.3",
|
||||
"description": "Create consistent error objects around the codebase",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/extensions-registry",
|
||||
"version": "3.0.8",
|
||||
"version": "3.0.9",
|
||||
"description": "Abstraction for exploring Directus extensions on a package registry",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/extensions-sdk",
|
||||
"version": "15.0.0",
|
||||
"version": "16.0.0",
|
||||
"description": "A toolkit to develop extensions to extend Directus",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/extensions",
|
||||
"version": "3.0.8",
|
||||
"version": "3.0.9",
|
||||
"description": "Utilities and types for Directus extensions",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/memory",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"description": "Memory / Redis abstraction for Directus",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/pressure",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"description": "Pressure based rate limiter",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/release-notes-generator",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"description": "Directus tailored release notes generator for changesets",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/schema-builder",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"description": "Directus SchemaBuilder for mocking/constructing a database schema based on code.",
|
||||
"keywords": [
|
||||
"sql",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/schema",
|
||||
"version": "13.0.1",
|
||||
"version": "13.0.2",
|
||||
"description": "Utility for extracting information about existing DB schema",
|
||||
"keywords": [
|
||||
"sql",
|
||||
|
||||
@@ -276,7 +276,8 @@ export default class MySQL implements SchemaInspector {
|
||||
.andOn('rc.CONSTRAINT_SCHEMA', '=', 'fk.CONSTRAINT_SCHEMA');
|
||||
})
|
||||
.leftJoin('INFORMATION_SCHEMA.STATISTICS as stats', function () {
|
||||
this.on('stats.TABLE_NAME', '=', 'c.TABLE_NAME')
|
||||
this.on('stats.TABLE_SCHEMA', '=', 'c.TABLE_SCHEMA')
|
||||
.andOn('stats.TABLE_NAME', '=', 'c.TABLE_NAME')
|
||||
.andOn('stats.COLUMN_NAME', '=', 'c.COLUMN_NAME')
|
||||
.andOnVal('stats.NON_UNIQUE', 1)
|
||||
.andOnVal('stats.SEQ_IN_INDEX', 1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/specs",
|
||||
"version": "11.1.0",
|
||||
"version": "11.1.1",
|
||||
"description": "OpenAPI Specification of the Directus API",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/storage-driver-azure",
|
||||
"version": "12.0.7",
|
||||
"version": "12.0.8",
|
||||
"description": "Azure file storage abstraction for `@directus/storage`",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/storage-driver-cloudinary",
|
||||
"version": "12.0.7",
|
||||
"version": "12.0.8",
|
||||
"description": "Cloudinary file storage abstraction for `@directus/storage`",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/storage-driver-gcs",
|
||||
"version": "12.0.7",
|
||||
"version": "12.0.8",
|
||||
"description": "GCS file storage abstraction for `@directus/storage`",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/storage-driver-local",
|
||||
"version": "12.0.0",
|
||||
"version": "12.0.1",
|
||||
"description": "Local file storage abstraction for `@directus/storage`",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/storage-driver-s3",
|
||||
"version": "12.0.7",
|
||||
"version": "12.0.8",
|
||||
"description": "S3 file storage abstraction for `@directus/storage`",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/storage-driver-supabase",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"description": "Supabase file storage abstraction for `@directus/storage`",
|
||||
"homepage": "https://directus.io",
|
||||
"repository": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user