Merge branch 'next' into develop

This commit is contained in:
Zeke Zhang
2026-08-15 09:18:11 +08:00
28 changed files with 481 additions and 20 deletions
@@ -167,6 +167,21 @@ describe('gateway', () => {
expect((req as any).originalUrl).toBe('/api/__app/demo/.well-known/oauth-authorization-server');
});
it('should proxy local storage requests to the selected sub app', async () => {
const req = {
url: '/storage/uploads/logo.png?__appName=demo',
headers: {},
} as any;
const res = {} as any;
const supervisor = AppSupervisor.getInstance();
const proxyWeb = vi.spyOn(supervisor, 'proxyWeb').mockResolvedValue(true);
await gateway.requestHandler(req, res);
expect(proxyWeb).toHaveBeenCalledWith('demo', req, res);
});
it('should add same middleware into app selector once', async () => {
const fn = async (ctx, next) => {
ctx.resolvedAppName = 'test';
@@ -246,6 +246,10 @@ export const CardUpload = (props) => {
);
};
type SelectExistingRecordHandler = (event?: unknown) => void;
const defaultSelectExistingRecordHandlers = new WeakSet<SelectExistingRecordHandler>();
@largeField()
export class UploadFieldModel extends FieldModel {
selectedRows = observable.ref([]);
@@ -257,11 +261,13 @@ export class UploadFieldModel extends FieldModel {
onInit(options: any): void {
super.onInit(options);
this.onSelectExitRecordClick = (e) => {
const onSelectExitRecordClick: SelectExistingRecordHandler = (e) => {
this.dispatchEvent('openView', {
event: e,
});
};
defaultSelectExistingRecordHandlers.add(onSelectExitRecordClick);
this.onSelectExitRecordClick = onSelectExitRecordClick;
}
set onSelectExitRecordClick(fn) {
this.setProps({ onSelectExitRecordClick: fn });
@@ -276,7 +282,23 @@ export class UploadFieldModel extends FieldModel {
const fileCollectionReference = fileCollection
? { dataSourceKey: fileCollection.dataSourceKey, collectionName: fileCollection.name }
: undefined;
return <CardUpload {...this.props} fileCollection={fileCollectionReference} />;
const configuredHandler = this.props.onSelectExitRecordClick as SelectExistingRecordHandler | undefined;
const onSelectExitRecordClick =
configuredHandler && defaultSelectExistingRecordHandlers.has(configuredHandler)
? (e?: unknown) => {
this.dispatchEvent('openView', {
event: e,
});
}
: configuredHandler;
return (
<CardUpload
{...this.props}
fileCollection={fileCollectionReference}
onSelectExitRecordClick={onSelectExitRecordClick}
/>
);
}
}
@@ -573,7 +595,7 @@ UploadFieldModel.registerFlow({
},
},
},
content: () => <RecordPickerContent model={ctx.model} />,
content: () => <RecordPickerContent model={ctx.model} toOne={toOne} />,
styles: {
content: {
padding: 0,
@@ -0,0 +1,236 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import type { ReactElement } from 'react';
import { FlowEngine } from '@nocobase/flow-engine';
import { describe, expect, it, vi } from 'vitest';
import { UploadFieldModel } from '../UploadFieldModel';
type FileRecord = {
id: number;
filename: string;
};
type PickerValue = FileRecord | FileRecord[] | undefined;
type PickerViewOptions = {
inputArgs: {
rowSelectionProps: {
type: 'radio' | 'checkbox';
onChange: (selectedRowKeys: unknown, selectedRows: FileRecord[]) => void;
};
};
content: () => ReactElement<{ toOne?: boolean }>;
};
type OpenViewContext = {
inputArgs: {
mode?: string;
size?: string;
};
collectionField: {
type: string;
target: string;
};
collection: {
dataSourceKey: string;
filterTargetKey: keyof FileRecord;
};
model: {
uid: string;
props: {
sourceFieldModelUid?: string;
value: FileRecord[];
};
parent?: {
use?: string;
};
selectedRows: {
value: PickerValue;
};
change: () => void;
_closeView?: () => void;
flowEngine: {
context: {
themeToken: {
colorBgLayout: string;
};
};
};
};
viewer: {
open: (options: PickerViewOptions) => void;
};
isMobileLayout: boolean;
layoutContentElement: null;
};
type OpenViewHandler = (ctx: OpenViewContext, params: { mode: string; size: string }) => void;
function getOpenViewHandler(): OpenViewHandler {
const flow = UploadFieldModel.globalFlowRegistry.getFlow('selectExitRecordSettings');
const handler = flow?.getStep('openView')?.serialize().handler;
if (!handler) {
throw new Error('selectExitRecordSettings.openView handler is not registered');
}
return handler as unknown as OpenViewHandler;
}
function createContext(fieldType: string, value: FileRecord[] = []) {
const open = vi.fn<(options: PickerViewOptions) => void>();
const closeView = vi.fn();
const change = vi.fn();
const selectedRows = { value: undefined as PickerValue };
const context: OpenViewContext = {
inputArgs: {},
collectionField: {
type: fieldType,
target: 'attachments',
},
collection: {
dataSourceKey: 'main',
filterTargetKey: 'id',
},
model: {
uid: 'upload-field',
props: {
value,
},
selectedRows,
change,
_closeView: closeView,
flowEngine: {
context: {
themeToken: {
colorBgLayout: '#fff',
},
},
},
},
viewer: {
open,
},
isMobileLayout: false,
layoutContentElement: null,
};
return { change, closeView, context, open, selectedRows };
}
function getOpenedView(open: ReturnType<typeof vi.fn<(options: PickerViewOptions) => void>>) {
const view = open.mock.calls[0]?.[0];
if (!view) {
throw new Error('Record picker view was not opened');
}
return view;
}
describe('UploadFieldModel existing-record picker', () => {
it('dispatches the picker event from the current field fork', () => {
const onChange = vi.fn();
const model = new UploadFieldModel({
uid: 'upload-field-dispatch',
use: 'UploadFieldModel',
flowEngine: new FlowEngine(),
props: {},
});
model.onInit({});
const fork = model.createFork({ onChange }, 'subtable-row');
const dispatchEvent = vi.fn<typeof fork.dispatchEvent>().mockResolvedValue([]);
fork.dispatchEvent = dispatchEvent;
vi.spyOn(model, 'dispatchEvent').mockResolvedValue([]);
const event = { type: 'click' };
const field = (
fork as unknown as {
renderOriginal: () => ReactElement<{
onSelectExitRecordClick: (event: { type: string }) => void;
}>;
}
).renderOriginal();
field.props.onSelectExitRecordClick(event);
expect(dispatchEvent).toHaveBeenCalledWith('openView', {
event,
});
});
it('preserves a custom existing-record picker handler on a fork', () => {
const customHandler = vi.fn();
const model = new UploadFieldModel({
uid: 'upload-field-custom-handler',
use: 'UploadFieldModel',
flowEngine: new FlowEngine(),
props: {},
});
model.onInit({});
model.onSelectExitRecordClick = customHandler;
const fork = model.createFork({ onChange: vi.fn() }, 'subtable-row');
const dispatchEvent = vi.fn<typeof fork.dispatchEvent>().mockResolvedValue([]);
fork.dispatchEvent = dispatchEvent;
const event = { type: 'click' };
const field = (
fork as unknown as {
renderOriginal: () => ReactElement<{
onSelectExitRecordClick: (event: { type: string }) => void;
}>;
}
).renderOriginal();
field.props.onSelectExitRecordClick(event);
expect(customHandler).toHaveBeenCalledWith(event);
expect(dispatchEvent).not.toHaveBeenCalled();
});
it('configures a to-one picker without a separate submit action', () => {
const { context, open } = createContext('belongsTo');
getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
const view = getOpenedView(open);
expect(view.inputArgs.rowSelectionProps.type).toBe('radio');
expect(view.content().props.toOne).toBe(true);
});
it('commits through the current model and closes a to-one picker', () => {
const { change, closeView, context, open, selectedRows } = createContext('belongsTo');
const selectedRecord = { id: 1, filename: 'report.pdf' };
getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
getOpenedView(open).inputArgs.rowSelectionProps.onChange(undefined, [selectedRecord]);
expect(selectedRows.value).toBe(selectedRecord);
expect(change).toHaveBeenCalledOnce();
expect(closeView).toHaveBeenCalledOnce();
});
it('keeps a to-many picker pending until its submit action is used', () => {
const existingRecord = { id: 1, filename: 'existing.pdf' };
const addedRecord = { id: 2, filename: 'added.pdf' };
const { change, closeView, context, open, selectedRows } = createContext('belongsToMany', [existingRecord]);
getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
const view = getOpenedView(open);
expect(view.inputArgs.rowSelectionProps.type).toBe('checkbox');
expect(view.content().props.toOne ?? false).toBe(false);
view.inputArgs.rowSelectionProps.onChange(undefined, [existingRecord, addedRecord]);
expect(selectedRows.value).toEqual([existingRecord, addedRecord]);
expect(closeView).not.toHaveBeenCalled();
expect(change).not.toHaveBeenCalled();
});
});
@@ -869,6 +869,22 @@ describe('file manager > server', () => {
expect(response.headers.location).toBe(storageUrl);
});
it('preserves the sub-application when redirecting to local storage', async () => {
await app.destroy();
app = await getApp({ name: 'sub-app' });
agent = app.agent();
db = app.db;
plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
});
const response = await agent.get(body.data.url);
expect(response.status).toBe(302);
expect(response.headers.location).toBe(`${await plugin.getFileURL(body.data)}?__appName=sub-app`);
});
it('keeps permanent URLs without extname compatible', async () => {
const admin = await db.getRepository('users').findOne();
const loggedAgent = await app.agent().login(admin);
@@ -1133,7 +1149,7 @@ describe('file manager > server', () => {
const response = await loggedAgent.get(body.data.url);
expect(response.status).toBe(302);
expect(response.headers.location).toBe(await plugin.getFileURL(body.data));
expect(response.headers.location).toBe(`${await plugin.getFileURL(body.data)}?__appName=subapp`);
const wrongAppResponse = await loggedAgent.get(`/files/main/main/attachments/${body.data.id}`);
expect(wrongAppResponse.status).toBe(404);
@@ -1476,7 +1492,7 @@ describe('file manager > server', () => {
.get(file.get('url') as string)
.set('Cookie', cookieHeader);
expect(response.status).toBe(302);
expect(response.headers.location).toBe(await plugin.getFileURL(file));
expect(response.headers.location).toBe(`${await plugin.getFileURL(file)}?__appName=subapp`);
const fileTemplateRecord = await plugin.createFileRecord({
collectionName: 'files',
@@ -1491,7 +1507,9 @@ describe('file manager > server', () => {
.get(fileTemplateRecord.get('url') as string)
.set('Cookie', cookieHeader);
expect(fileTemplateResponse.status).toBe(302);
expect(fileTemplateResponse.headers.location).toBe(await plugin.getFileURL(fileTemplateRecord));
expect(fileTemplateResponse.headers.location).toBe(
`${await plugin.getFileURL(fileTemplateRecord)}?__appName=subapp`,
);
const dynamicCollectionName = 't_8jbl7u3wx4j';
await createFileTemplateCollection(db, dynamicCollectionName);
@@ -1508,7 +1526,9 @@ describe('file manager > server', () => {
.get(dynamicFileTemplateRecord.get('url') as string)
.set('Cookie', cookieHeader);
expect(dynamicFileTemplateResponse.status).toBe(302);
expect(dynamicFileTemplateResponse.headers.location).toBe(await plugin.getFileURL(dynamicFileTemplateRecord));
expect(dynamicFileTemplateResponse.headers.location).toBe(
`${await plugin.getFileURL(dynamicFileTemplateRecord)}?__appName=subapp`,
);
} finally {
app.options.name = originalName;
restoreEnv('APP_PUBLIC_PATH', originalPath);
@@ -27,6 +27,15 @@ type StorageFileURLResolver = {
}) => Promise<string | undefined>;
};
function preserveSubAppInLocalURL(url: string, appName?: string) {
if (!appName || appName === 'main' || !url.startsWith('/') || url.startsWith('//')) {
return url;
}
const [urlWithoutHash, hash] = url.split('#', 2);
const separator = urlWithoutHash.includes('?') ? '&' : '?';
return `${urlWithoutHash}${separator}__appName=${encodeURIComponent(appName)}${hash ? `#${hash}` : ''}`;
}
export async function getFile(ctx: Context, next: Next) {
const collection = ctx.dataSource.collectionManager.getCollection(ctx.action.resourceName);
if (!collection || (collection.name !== 'attachments' && collection.options?.template !== 'file')) {
@@ -125,13 +134,14 @@ export async function getFile(ctx: Context, next: Next) {
const download = !temporaryAccess && ctx.method === 'GET' && ctx.query.download === '1';
const preview = download ? false : temporaryAccess ? false : Boolean(ctx.state.fileAccess?.preview);
const dataSource = ctx.dataSource as DataSource & StorageFileURLResolver;
const finalUrl =
const storageUrl =
(await dataSource.resolveStorageFileURL?.({
collectionName: collection.name,
file: getFilePlainObject(file as AttachmentModel) as Record<string, unknown>,
preview,
download,
})) || (await plugin.getFileURL(file, preview, { download }));
const finalUrl = preserveSubAppInLocalURL(storageUrl, ctx.state.fileAccess?.appName);
ctx.status = 302;
ctx.redirect(finalUrl);
await next();
@@ -7,9 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useApp } from '@nocobase/client-v2';
import { Button, Result } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { CanvasContent } from './canvas/CanvasContent';
import { FlowContext } from './canvas/contexts';
import { linkNodes } from './canvas/nodeTree';
@@ -43,7 +43,7 @@ function attachJobs(nodes: any[], jobs: any[] = []) {
export function ExecutionCanvas({ record, resource, refresh }: { record: any; resource: any; refresh: () => void }) {
const { t } = useWorkflowTranslation();
const navigate = useNavigate();
const app = useApp();
const [viewJob, setViewJob] = useState<any>(null);
const { jobs = [], workflow, ...execution } = record ?? {};
@@ -66,17 +66,17 @@ export function ExecutionCanvas({ record, resource, refresh }: { record: any; re
}
}, [jobs, viewJob?.id]);
const onBack = () => {
navigate(-1);
};
if (!workflow) {
return (
<Result
status="404"
title={t('Not found')}
subTitle={t('Workflow of execution is not existed')}
extra={<Button onClick={onBack}>{t('Go back')}</Button>}
extra={
<Button href={app.getHref(app.pluginSettingsManager.getRoutePath('workflow'))}>
{t('Back to Workflow List')}
</Button>
}
/>
);
}
@@ -0,0 +1,48 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { render, screen } from '@testing-library/react';
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
vi.mock('@nocobase/client-v2', () => ({
useApp: () => ({
getHref: (path: string) => `/v${path}`,
pluginSettingsManager: { getRoutePath: () => '/admin/settings/workflow' },
}),
}));
vi.mock('../locale', () => ({
useWorkflowTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock('../canvas/CanvasContent', () => ({
CanvasContent: () => null,
}));
vi.mock('../components/ExecutionViewHeader', () => ({
ExecutionViewHeader: () => null,
}));
vi.mock('../components/JobResultModal', () => ({
JobResultModal: () => null,
}));
import { ExecutionCanvas } from '../ExecutionCanvas';
describe('ExecutionCanvas', () => {
it('links to the workflow list when the execution workflow does not exist', () => {
render(<ExecutionCanvas record={{ id: 1, jobs: [] }} resource={{}} refresh={vi.fn()} />);
expect(screen.getByRole('link', { name: 'Back to Workflow List' })).toHaveAttribute(
'href',
'/v/admin/settings/workflow',
);
});
});
@@ -10,14 +10,14 @@
import { useFlowContext } from '@nocobase/flow-engine';
import { useRequest } from 'ahooks';
import { Spin, theme } from 'antd';
import React from 'react';
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import ExecutionCanvas from '../ExecutionCanvas';
import { normalizeRecordResponse } from '../components/workflowCanvas';
import { useWorkflowTranslation } from '../locale';
export default function ExecutionViewPage() {
useWorkflowTranslation();
const { t } = useWorkflowTranslation();
const ctx = useFlowContext();
const { token } = theme.useToken();
const params = useParams<{ id?: string }>();
@@ -41,6 +41,15 @@ export default function ExecutionViewPage() {
const record = data?.id != null && String(data.id) === String(executionId) ? data : null;
useEffect(() => {
if (!record) {
return;
}
const workflowTitle = record.workflow?.title;
document.title = `${workflowTitle ? `${workflowTitle} - ` : ''}${t('Execution history')} - NocoBase`;
}, [record, t]);
if (!executionId) {
return null;
}
@@ -7,9 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useApp } from '@nocobase/client-v2';
import { useFlowContext as useFlowEngineContext } from '@nocobase/flow-engine';
import { useRequest } from 'ahooks';
import { Spin, theme } from 'antd';
import { Button, Result, Spin, theme } from 'antd';
import React, { useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { WorkflowCanvasHeader } from '../components/WorkflowCanvasHeader';
@@ -25,13 +26,14 @@ import { useT } from '../locale';
export default function WorkflowCanvasPage() {
const ctx = useFlowEngineContext();
const app = useApp();
const t = useT();
const { token } = theme.useToken();
const params = useParams<{ id?: string }>();
const workflowId = params.id;
const resource = ctx.api.resource('workflows');
const { data, refresh } = useRequest(
const { data, loading, refresh } = useRequest(
async () => {
if (!workflowId) {
return null;
@@ -86,7 +88,21 @@ export default function WorkflowCanvasPage() {
}
if (!record) {
return <Spin />;
if (loading) {
return <Spin />;
}
return (
<Result
status="404"
title={t('Workflow does not exist')}
extra={
<Button type="primary" href={app.getHref(app.pluginSettingsManager.getRoutePath('workflow'))}>
{t('Back to Workflow List')}
</Button>
}
/>
);
}
return (
@@ -52,6 +52,7 @@ describe('ExecutionViewPage', () => {
beforeEach(() => {
vi.clearAllMocks();
holder.executionCanvasProps = null;
document.title = '';
});
it('loads execution canvas data and renders ExecutionCanvas instead of the empty placeholder', async () => {
@@ -94,4 +95,30 @@ describe('ExecutionViewPage', () => {
expect(holder.executionCanvasProps?.record?.id).toBe(369411612409856);
expect(screen.queryByText('Workflow canvas editor is being migrated to the new UI.')).toBeNull();
});
it('updates the browser title after the execution record is loaded', async () => {
document.title = 'Loading...';
holder.ctx = {
api: {
resource: () => ({
get: vi.fn().mockResolvedValue({
data: {
data: {
id: 369411612409856,
jobs: [],
workflow: { title: 'Approval workflow', nodes: [] },
},
},
}),
}),
},
};
renderWithApp(<ExecutionViewPage />);
await screen.findByTestId('execution-canvas');
await waitFor(() => {
expect(document.title).toBe('Approval workflow - Execution history - NocoBase');
});
});
});
@@ -46,6 +46,17 @@ vi.mock('@nocobase/flow-engine', async (importOriginal) => {
};
});
vi.mock('@nocobase/client-v2', async (importOriginal) => {
const actual = await importOriginal<typeof import('@nocobase/client-v2')>();
return {
...actual,
useApp: () => ({
getHref: (path: string) => `/v${path}`,
pluginSettingsManager: { getRoutePath: () => '/admin/settings/workflow' },
}),
};
});
vi.mock('../../components/WorkflowCanvasHeader', () => ({
WorkflowCanvasHeader: ({ record }: { record: { title?: string } }) => (
<div data-testid="workflow-header">{record.title}</div>
@@ -103,4 +114,17 @@ describe('WorkflowCanvasPage', () => {
expect(document.title).toBe('Workflow: 审批 - NocoBase');
});
});
it('shows a not-found result with a link back to the workflow list', async () => {
holder.getWorkflow.mockResolvedValue({ data: { data: null } });
render(<WorkflowCanvasPage />);
expect(await screen.findByText('Workflow does not exist')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Back to Workflow List' })).toHaveAttribute(
'href',
'/v/admin/settings/workflow',
);
expect(holder.listRevisions).not.toHaveBeenCalled();
});
});
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Wird im Hintergrund als Aufgabe in der Warteschlange ausgeführt.",
"Workflow": "Workflow",
"Back to Workflow List": "Zurück zur Workflow-Liste",
"Workflow does not exist": "Workflow ist nicht vorhanden",
"Workflow not executed": "Workflow nicht ausgeführt",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow ausgeführt, der Ergebnisstatus ist <1>{{statusText}}</1><2>Ausführung anzeigen</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Der Workflow wurde nicht ausgelöst, weil die aktuelle Anfrage die Auslöseanforderungen nicht erfüllt.",
@@ -283,6 +283,7 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Workflow",
"Back to Workflow List": "Back to Workflow List",
"This workflow has configuration issues and may not work properly.": "This workflow has configuration issues and may not work properly.",
"Workflow title": "Workflow title",
"Workflow canvas editor is being migrated to the new UI.": "Workflow canvas editor is being migrated to the new UI.",
@@ -290,6 +291,7 @@
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Workflow was not triggered because the current request did not meet the trigger requirements.",
"Workflow of execution is not existed": "Workflow of execution is not existed",
"Workflow does not exist": "Workflow does not exist",
"Workflow tasks": "Workflow tasks",
"Workflow todos": "Workflow todos",
"Workflow will be triggered before deleting succeeded (only supports pre-action event in local mode).": "Workflow will be triggered before deleting succeeded (only supports pre-action event in local mode).",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Flujo de trabajo",
"Back to Workflow List": "Volver a la lista de flujos de trabajo",
"Workflow does not exist": "El flujo de trabajo no existe",
"Workflow not executed": "Flujo de trabajo no ejecutado",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "El flujo de trabajo no se activó porque la solicitud actual no cumplió los requisitos del desencadenador.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Workflow",
"Back to Workflow List": "Retour à la liste des workflows",
"Workflow does not exist": "Le workflow nexiste pas",
"Workflow not executed": "Workflow non exécuté",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Le workflow na pas été déclenché, car la requête actuelle ne répond pas aux exigences du déclencheur.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "A háttérben sorba állított feladatként kerül végrehajtásra.",
"Workflow": "Munkafolyamat",
"Back to Workflow List": "Vissza a munkafolyamatok listájához",
"Workflow does not exist": "A munkafolyamat nem létezik",
"Workflow not executed": "A munkafolyamat nem lett végrehajtva",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Munkafolyamat végrehajtva, az eredmény állapota: <1>{{statusText}}</1><2>Végrehajtás megtekintése</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "A munkafolyamat nem indult el, mert az aktuális kérés nem felelt meg az indítási feltételeknek.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Akan dieksekusi di latar belakang sebagai tugas antrean.",
"Workflow": "Alur kerja",
"Back to Workflow List": "Kembali ke daftar alur kerja",
"Workflow does not exist": "Alur kerja tidak ada",
"Workflow not executed": "Alur kerja tidak dieksekusi",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Alur kerja dieksekusi, status hasilnya adalah <1>{{statusText}}</1><2>Lihat eksekusi</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Alur kerja tidak dipicu karena permintaan saat ini tidak memenuhi persyaratan pemicu.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Verrà eseguito in background come attività in coda.",
"Workflow": "Workflow",
"Back to Workflow List": "Torna all'elenco dei workflow",
"Workflow does not exist": "Il workflow non esiste",
"Workflow not executed": "Workflow non eseguito",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow eseguito, lo stato del risultato è <1>{{statusText}}</1><2>Visualizza l'esecuzione</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Il workflow non è stato attivato perché la richiesta corrente non soddisfa i requisiti del trigger.",
@@ -256,6 +256,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "キュータスクとしてバックグラウンドで実行されます。",
"Workflow": "ワークフロー",
"Back to Workflow List": "ワークフロー一覧に戻る",
"Workflow does not exist": "ワークフローが存在しません",
"Workflow not executed": "ワークフローは実行されませんでした",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "ワークフローが実行されました。結果ステータス: <1>{{statusText}}</1><2>実行詳細を表示</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "現在のリクエストがトリガー要件を満たしていないため、ワークフローはトリガーされませんでした。",
@@ -272,6 +272,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "백그라운드에서 대기 작업으로 실행됩니다.",
"Workflow": "워크플로우",
"Back to Workflow List": "워크플로 목록으로 돌아가기",
"Workflow does not exist": "워크플로가 존재하지 않습니다",
"Workflow not executed": "워크플로우가 실행되지 않았습니다",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "워크플로우가 실행되었습니다. 결과 상태는 <1>{{statusText}}</1><2>실행 보기</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "현재 요청이 트리거 요구 사항을 충족하지 않아 워크플로우가 트리거되지 않았습니다.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Workflow",
"Back to Workflow List": "Terug naar de workflowlijst",
"Workflow does not exist": "Workflow bestaat niet",
"Workflow not executed": "Workflow niet uitgevoerd",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "De workflow is niet geactiveerd omdat de huidige aanvraag niet aan de triggervereisten voldoet.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Fluxo de trabalho",
"Back to Workflow List": "Voltar para a lista de fluxos de trabalho",
"Workflow does not exist": "O fluxo de trabalho não existe",
"Workflow not executed": "Fluxo de trabalho não executado",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "O workflow não foi acionado porque a solicitação atual não atende aos requisitos do gatilho.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Будет выполнен в фоновом режиме как задача в очереди.",
"Workflow": "Рабочий процесс",
"Back to Workflow List": "Вернуться к списку рабочих процессов",
"Workflow does not exist": "Рабочий процесс не существует",
"Workflow not executed": "Рабочий процесс не выполнен",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Рабочий процесс выполнен, статус результата: <1>{{statusText}}</1><2>Посмотреть выполнение</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Рабочий процесс не был запущен, так как текущий запрос не соответствует требованиям триггера.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "İş Akışı",
"Back to Workflow List": "İş akışı listesine dön",
"Workflow does not exist": "İş akışı mevcut değil",
"Workflow not executed": "İş akışı yürütülmedi",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Geçerli istek tetikleme gereksinimlerini karşılamadığı için iş akışı tetiklenmedi.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Workflow",
"Back to Workflow List": "Повернутися до списку робочих процесів",
"Workflow does not exist": "Робочий процес не існує",
"Workflow not executed": "Робочий процес не виконано",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Робочий процес не було запущено, оскільки поточний запит не відповідає вимогам тригера.",
@@ -254,6 +254,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Workflow",
"Back to Workflow List": "Quay lại danh sách quy trình làm việc",
"Workflow does not exist": "Quy trình làm việc không tồn tại",
"Workflow not executed": "Quy trình làm việc chưa được thực thi",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "Quy trình làm việc không được kích hoạt vì yêu cầu hiện tại không đáp ứng các điều kiện kích hoạt.",
@@ -291,6 +291,7 @@
"When no condition matches": "未满足任何条件时",
"Will be executed in the background as a queued task.": "将作为队列任务在后台执行。",
"Workflow": "工作流",
"Back to Workflow List": "返回工作流列表",
"This workflow has configuration issues and may not work properly.": "该工作流配置存在问题,可能无法正常使用",
"Workflow title": "工作流名称",
"Workflow canvas editor is being migrated to the new UI.": "工作流画布编辑器正在迁移到新版界面。",
@@ -298,6 +299,7 @@
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "工作流已执行,结果状态为 <1>{{statusText}}</1><2>查看执行详情</2>",
"Workflow was not triggered because the current request did not meet the trigger requirements.": "当前请求未满足触发要求,工作流未触发。",
"Workflow of execution is not existed": "执行计划对应的工作流不存在",
"Workflow does not exist": "工作流不存在",
"Workflow tasks": "流程待办",
"Workflow todos": "流程待办",
"Workflow will be triggered before deleting succeeded (only supports pre-action event in local mode).": "删除成功之前触发工作流(支持操作前事件)。",
@@ -256,6 +256,8 @@
"When no condition matches": "When no condition matches",
"Will be executed in the background as a queued task.": "Will be executed in the background as a queued task.",
"Workflow": "Workflow",
"Back to Workflow List": "返回工作流程列表",
"Workflow does not exist": "工作流程不存在",
"Workflow title": "工作流名稱",
"Workflow not executed": "工作流未執行",
"Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>": "Workflow executed, the result status is <1>{{statusText}}</1><2>View the execution</2>",