Merge remote-tracking branch 'origin/main' into content-versioning-fix

This commit is contained in:
Nitwel
2025-08-21 11:27:47 +02:00
112 changed files with 2453 additions and 449 deletions

View File

@@ -1,5 +0,0 @@
---
"@directus/api": patch
---
Updated dependency tar-fs

View File

@@ -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

View File

@@ -1,5 +0,0 @@
---
'create-directus-project': patch
---
Upgrade dependencies

View File

@@ -1,5 +0,0 @@
---
'@directus/api': patch
---
Fixed admin users email not trimmed on project initialization

View File

@@ -1,5 +0,0 @@
---
'@directus/extensions-registry': patch
---
Upgraded dependencies

View File

@@ -0,0 +1,5 @@
---
"@directus/api": patch
---
Updated nodemailer to us AWS SESv2

View File

@@ -0,0 +1,5 @@
---
'@directus/app': patch
---
Hide "Create new" and "Add existing" buttons on o2m fields with unique constraint

View File

@@ -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

View File

@@ -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.
:::

View File

@@ -0,0 +1,5 @@
---
'@directus/app': patch
---
Added currentItem id check to prevent in-flight api call from returning stale data

View File

@@ -0,0 +1,5 @@
---
'@directus/app': patch
---
Added RTL support for popper context menu

View File

@@ -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

View File

@@ -1,5 +0,0 @@
---
'@directus/extensions-sdk': patch
---
Upgraded dependencies

View File

@@ -1,5 +0,0 @@
---
'@directus/errors': patch
---
Moved dependency

View File

@@ -1,6 +0,0 @@
---
'@directus/api': patch
'@directus/app': patch
---
Updated dependency form-data

View File

@@ -1,5 +0,0 @@
---
'@directus/api': minor
---
Added the ability to override the email `from` property

View File

@@ -1,5 +0,0 @@
---
'@directus/app': patch
---
Standardized batch mode for raw group fields

View File

@@ -1,5 +0,0 @@
---
'@directus/env': patch
---
Upgrade dependencies

View File

@@ -0,0 +1,5 @@
---
'@directus/sdk': patch
---
Fixed auth being cleared before `login`/`refresh` request succeeds

View File

@@ -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:

View File

@@ -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

View File

@@ -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:*",

View File

@@ -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) => {

View File

@@ -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
View 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();
});
});

View File

@@ -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;

View 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);
}
});
});

View 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();
},
});

View File

@@ -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": {

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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) {

View 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);
});
});

View File

@@ -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" />

View File

@@ -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">

View File

@@ -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>

View File

@@ -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)));

View File

@@ -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)');
});

View File

@@ -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;

View 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');
});
});
});

View File

@@ -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 {

View 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
});
});

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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) {

View File

@@ -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', () => {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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" />

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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"

View File

@@ -17,11 +17,6 @@ const { t } = useI18n();
@use '@/styles/mixins';
.readme {
:deep(*) {
-webkit-user-select: text;
user-select: text;
}
:deep() {
@include mixins.markdown;
}

View 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,
},
},
},
],
});

View File

@@ -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 }"
>

View File

@@ -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)"
>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -3,8 +3,6 @@
font-size: 0;
line-height: 0;
direction: ltr;
-webkit-user-select: none;
user-select: none;
touch-action: none;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}

View File

@@ -10,3 +10,4 @@ flag_management:
comment:
layout: 'header, diff, flags'
require_changes: 'coverage_drop AND uncovered_patch'

View File

@@ -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",

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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