pgkiln 0.83.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/154_notifications.sql +186 -0
- package/docs/apex-feature-parity.md +3 -2
- package/docs/guide/03-builder.md +16 -5
- package/docs/guide/04-pages-and-regions.md +42 -0
- package/docs/guide/06-processing.md +22 -1
- package/docs/guide/07-dynamic-actions.md +1 -0
- package/docs/guide/09-reference.md +15 -0
- package/docs/guide/11-from-apex.md +1 -0
- package/docs/guide/12-development.md +13 -1
- package/package.json +1 -1
- package/public/app.css +29 -0
- package/public/app.js +78 -0
- package/public/sw.js +5 -1
- package/src/app.ts +11 -0
- package/src/builder/ai-agents.ts +86 -85
- package/src/builder/ai-builder.ts +30 -30
- package/src/builder/ai.ts +93 -92
- package/src/builder/api.ts +47 -47
- package/src/builder/app-groups.ts +33 -32
- package/src/builder/appfromfile.ts +57 -58
- package/src/builder/appsheets.ts +33 -32
- package/src/builder/appwizard.ts +65 -65
- package/src/builder/arrange.ts +23 -22
- package/src/builder/assistant.ts +39 -38
- package/src/builder/automations.ts +32 -31
- package/src/builder/availability.ts +24 -24
- package/src/builder/backups.ts +30 -31
- package/src/builder/blueprints.ts +62 -61
- package/src/builder/code-editor.ts +1 -1
- package/src/builder/components.ts +10 -6
- package/src/builder/create-page.ts +52 -50
- package/src/builder/cross-app.ts +37 -35
- package/src/builder/dataload.ts +2 -2
- package/src/builder/datasets.ts +107 -106
- package/src/builder/dependencies.ts +20 -19
- package/src/builder/designer.ts +36 -36
- package/src/builder/diagnostics.ts +58 -57
- package/src/builder/features.ts +39 -35
- package/src/builder/feedback.ts +23 -20
- package/src/builder/find-workspace.ts +28 -24
- package/src/builder/form-designer.ts +36 -35
- package/src/builder/gallery.ts +25 -29
- package/src/builder/globalization.ts +50 -49
- package/src/builder/i18n/de.ts +3461 -0
- package/src/builder/i18n/en.ts +3461 -0
- package/src/builder/i18n/es.ts +3461 -0
- package/src/builder/i18n/fr.ts +3461 -0
- package/src/builder/i18n/it.ts +3461 -0
- package/src/builder/i18n/ja.ts +3461 -0
- package/src/builder/i18n/ko.ts +3461 -0
- package/src/builder/i18n/nl.ts +3461 -0
- package/src/builder/i18n/pt.ts +3461 -0
- package/src/builder/i18n/zh-cn.ts +3461 -0
- package/src/builder/i18n/zh-tw.ts +3461 -0
- package/src/builder/i18n.ts +17 -3
- package/src/builder/json-sources.ts +63 -62
- package/src/builder/layouts.ts +12 -11
- package/src/builder/locks.ts +29 -28
- package/src/builder/login-page.ts +26 -25
- package/src/builder/mail.ts +148 -147
- package/src/builder/newapp.ts +15 -12
- package/src/builder/notification-settings.ts +27 -0
- package/src/builder/plugins.ts +47 -44
- package/src/builder/process-jobs.ts +11 -10
- package/src/builder/pwa.ts +22 -21
- package/src/builder/remote-databases.ts +55 -56
- package/src/builder/report-settings.ts +1 -1
- package/src/builder/reporter.ts +43 -42
- package/src/builder/rest-catalogs.ts +87 -86
- package/src/builder/rest.ts +9 -8
- package/src/builder/routes.ts +30 -27
- package/src/builder/search.ts +14 -13
- package/src/builder/static-files.ts +31 -32
- package/src/builder/subscriptions.ts +47 -45
- package/src/builder/supporting.ts +23 -18
- package/src/builder/team.ts +179 -179
- package/src/builder/telemetry.ts +59 -62
- package/src/builder/templates.ts +66 -65
- package/src/builder/themeroller.ts +60 -63
- package/src/builder/ui-defaults.ts +21 -20
- package/src/builder/ui.ts +2 -1
- package/src/builder/upgrade.ts +25 -24
- package/src/builder/utilities.ts +48 -47
- package/src/builder/vector-providers.ts +67 -66
- package/src/builder/websources.ts +43 -42
- package/src/builder/wizards.ts +95 -89
- package/src/builder/workflow-migrate.ts +30 -31
- package/src/builder/workflows.ts +40 -39
- package/src/builder/workingcopies.ts +41 -49
- package/src/builder/workspace-backups.ts +21 -20
- package/src/builder/workspace-mail.ts +17 -16
- package/src/builder/workspace-resources.ts +104 -112
- package/src/builder/workspace-transfer.ts +34 -26
- package/src/cli/replace.ts +2 -0
- package/src/i18n/ar.ts +9 -0
- package/src/i18n/cs.ts +9 -0
- package/src/i18n/da.ts +9 -0
- package/src/i18n/de.ts +9 -0
- package/src/i18n/el.ts +9 -0
- package/src/i18n/es.ts +9 -0
- package/src/i18n/fi.ts +9 -0
- package/src/i18n/fr.ts +9 -0
- package/src/i18n/he.ts +9 -0
- package/src/i18n/it.ts +9 -0
- package/src/i18n/ja.ts +9 -0
- package/src/i18n/ko.ts +9 -0
- package/src/i18n/nb.ts +9 -0
- package/src/i18n/pl.ts +9 -0
- package/src/i18n/pt.ts +9 -0
- package/src/i18n/ru.ts +9 -0
- package/src/i18n/sv.ts +9 -0
- package/src/i18n/tr.ts +9 -0
- package/src/i18n/uk.ts +9 -0
- package/src/i18n/zh.ts +9 -0
- package/src/i18n.ts +18 -0
- package/src/metadata.ts +9 -3
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/feedback.ts +1 -1
- package/src/runtime/lists.ts +43 -11
- package/src/runtime/notifications.ts +237 -0
- package/src/runtime/processes.ts +45 -1
- package/src/runtime/regions.ts +11 -15
- package/src/runtime/render.ts +12 -3
- package/src/runtime/routes.ts +12 -4
- package/src/runtime/signin.ts +2 -1
- package/src/session.ts +13 -4
package/src/builder/wizards.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ICONS } from '../icons.ts';
|
|
|
5
5
|
import { appOr404 } from './forms.ts';
|
|
6
6
|
import { addDashboard } from './appsheets.ts';
|
|
7
7
|
import { appHeader, back, BASE, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
|
|
8
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
8
9
|
|
|
9
10
|
// Create page wizards (APEX: Create Page). Step 1, on the app's Pages tab,
|
|
10
11
|
// picks the page type and a table or view (a GET form); step 2 (GET
|
|
@@ -46,12 +47,15 @@ export const MORE_KINDS: [string, string, string][] = [
|
|
|
46
47
|
];
|
|
47
48
|
/** All the page types step 2 knows: [key, label, description]. */
|
|
48
49
|
export const PAGE_KINDS: [string, string, string][] = [...WIZARD_KINDS, ...MORE_KINDS];
|
|
50
|
+
/** A page type's label and description in the builder language (the lists above stay English for the AI path). */
|
|
51
|
+
export const kindLabel = (k: string, fallback: string) => tx.dyn(`wiz.kind.${k}`, fallback);
|
|
52
|
+
export const kindHelp = (k: string, fallback: string) => tx.dyn(`wiz.kind_help.${k}`, fallback);
|
|
49
53
|
const KINDS = new Set(PAGE_KINDS.map((k) => k[0]));
|
|
50
54
|
/** Page types that need no table (search takes one optionally, for a new search configuration). */
|
|
51
55
|
export const NO_TABLE = new Set(['search', 'tasks', 'workflows', 'dashboard']);
|
|
52
|
-
const TASK_CONTEXTS: [string, string][]
|
|
56
|
+
const TASK_CONTEXTS = (): [string, string][] => [['my', tx('wiz.my_tasks_to_act_on')], ['initiated', tx('wiz.tasks_i_requested')], ['admin', tx('wiz.tasks_i_administer')]];
|
|
53
57
|
const CHART_KINDS = ['bar', 'column', 'line', 'area', 'donut', 'pie', 'funnel'];
|
|
54
|
-
const FUNCTIONS: [string, string][]
|
|
58
|
+
const FUNCTIONS = (): [string, string][] => [['count', tx('wiz.count')], ['sum', tx('wiz.sum')], ['avg', tx('wiz.average')], ['min', tx('wiz.minimum')], ['max', tx('wiz.maximum')]];
|
|
55
59
|
|
|
56
60
|
interface Col {
|
|
57
61
|
column_name: string;
|
|
@@ -121,7 +125,7 @@ export function wizardOptions(kind: string, b: Record<string, unknown>) {
|
|
|
121
125
|
break;
|
|
122
126
|
case 'chart':
|
|
123
127
|
o.chart = CHART_KINDS.includes(str('chart')) ? str('chart') : 'bar';
|
|
124
|
-
o.function = FUNCTIONS.some(([f]) => f === str('function')) ? str('function') : 'count';
|
|
128
|
+
o.function = FUNCTIONS().some(([f]) => f === str('function')) ? str('function') : 'count';
|
|
125
129
|
o.label_column = str('label_column') || null;
|
|
126
130
|
o.value_column = str('value_column') || null;
|
|
127
131
|
break;
|
|
@@ -176,7 +180,7 @@ export function wizardOptions(kind: string, b: Record<string, unknown>) {
|
|
|
176
180
|
o.form_page = formPage();
|
|
177
181
|
break;
|
|
178
182
|
case 'tasks':
|
|
179
|
-
o.context = TASK_CONTEXTS.some(([c]) => c === str('context')) ? str('context') : 'my';
|
|
183
|
+
o.context = TASK_CONTEXTS().some(([c]) => c === str('context')) ? str('context') : 'my';
|
|
180
184
|
o.completed = b.completed === 'true';
|
|
181
185
|
break;
|
|
182
186
|
case 'workflows':
|
|
@@ -213,7 +217,7 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
213
217
|
)
|
|
214
218
|
: null;
|
|
215
219
|
if (!rel && !(NO_TABLE.has(kind) && !table)) {
|
|
216
|
-
flash(s, KINDS.has(kind) ? '
|
|
220
|
+
flash(s, KINDS.has(kind) ? tx('wiz.choose_a_table_or_view') : tx('wiz.choose_a_page_type'), 'error');
|
|
217
221
|
// (137) back to the type's table step on the Create page grid
|
|
218
222
|
return back(reply, s, KINDS.has(kind) ? `${BASE}/apps/${a.id}/create-page?type=${encodeURIComponent(kind)}` : `${BASE}/apps/${a.id}/create-page`);
|
|
219
223
|
}
|
|
@@ -233,7 +237,7 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
233
237
|
)
|
|
234
238
|
: null;
|
|
235
239
|
|
|
236
|
-
const none: [string, string] = ['', '
|
|
240
|
+
const none: [string, string] = ['', tx('wiz.none')];
|
|
237
241
|
const colOpts = (pred: (c: Col) => boolean, withNone = true): [string, string][] => [
|
|
238
242
|
...(withNone ? [none] : []),
|
|
239
243
|
...cols.filter(pred).map((c): [string, string] => [c.column_name, `${c.column_name} (${c.type_sql}${c.fk_table ? `, shows ${c.fk_table}.${c.fk_display ?? '?'}` : ''})`]),
|
|
@@ -245,7 +249,7 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
245
249
|
html`<fieldset class="prop-group"><legend>${legend}</legend><div class="qb-pick">${cols.filter(pred).map(
|
|
246
250
|
(c) => html`<label class="check"><input type="checkbox" name="${name}" value="${c.column_name}"${chosen.includes(c.column_name) ? raw(' checked') : ''}> ${c.column_name} <span class="muted small">${c.type_sql}${note(c)}</span></label>`,
|
|
247
251
|
)}</div></fieldset>`;
|
|
248
|
-
const formPage = (help: string) => input('form_page', '
|
|
252
|
+
const formPage = (help: string) => input('form_page', tx('wiz.form_page'), '', { type: 'number', placeholder: tx('wiz.form_page_ph', { n: nextPage + 1 }), help });
|
|
249
253
|
const needsPk = ['report_form', 'grid', 'form', 'master_detail'].includes(kind);
|
|
250
254
|
const allCols = (c: Col) => c.kind !== 'binary';
|
|
251
255
|
const dates = (c: Col) => c.kind === 'date' || c.kind === 'timestamp';
|
|
@@ -253,146 +257,146 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
253
257
|
let fields: Raw | string = '';
|
|
254
258
|
switch (kind) {
|
|
255
259
|
case 'report_form':
|
|
256
|
-
fields = input('form_page', '
|
|
260
|
+
fields = input('form_page', tx('wiz.form_page'), nextPage + 1, { type: 'number', required: true, help: tx('wiz.the_modal_form_page_opened') });
|
|
257
261
|
break;
|
|
258
262
|
case 'form':
|
|
259
263
|
fields = html`<div class="form-grid">
|
|
260
|
-
${select('mode', '
|
|
261
|
-
${input('return_page', '
|
|
264
|
+
${select('mode', tx('wiz.page_mode'), defaults.mode, [['normal', tx('wiz.normal_page')], ['modal', tx('wiz.modal_dialog')]])}
|
|
265
|
+
${input('return_page', tx('wiz.after_saving_or_cancelling_go'), defaults.return_page ?? '', { type: 'number', placeholder: a.home_page ? tx('wiz.return_home', { n: a.home_page }) : tx('wiz.return_this'), help: tx('wiz.cancel_create_apply_changes_and') })}
|
|
262
266
|
</div>
|
|
263
|
-
${colChecks('columns', '
|
|
267
|
+
${colChecks('columns', tx('wiz.col_columns'), defaults.columns ?? [], (c) => !c.is_pk && !c.generated && c.kind !== 'binary', (c) => (c.not_null && !c.has_default ? tx('wiz.note_required') : c.fk_table ? tx('wiz.note_select_list', { table: c.fk_table }) : ''))}`;
|
|
264
268
|
break;
|
|
265
269
|
case 'cards':
|
|
266
270
|
fields = html`<div class="form-grid">
|
|
267
|
-
${select('title', '
|
|
268
|
-
${select('subtitle', '
|
|
269
|
-
${select('body', '
|
|
270
|
-
${select('badge', '
|
|
271
|
+
${select('title', tx('wiz.title'), defaults.title, colOpts(notBinary, false))}
|
|
272
|
+
${select('subtitle', tx('wiz.subtitle'), defaults.subtitle ?? '', colOpts(notBinary))}
|
|
273
|
+
${select('body', tx('wiz.body'), defaults.body ?? '', colOpts(notBinary))}
|
|
274
|
+
${select('badge', tx('wiz.badge'), defaults.badge ?? '', colOpts(notBinary))}
|
|
271
275
|
${pk ? formPage('A modal form page for a card (a link on each card and a Create button).') : ''}
|
|
272
276
|
</div>`;
|
|
273
277
|
break;
|
|
274
278
|
case 'calendar':
|
|
275
279
|
fields = html`<div class="form-grid">
|
|
276
|
-
${select('start', '
|
|
277
|
-
${select('end', '
|
|
278
|
-
${select('title', '
|
|
280
|
+
${select('start', tx('wiz.start_date'), defaults.start ?? '', colOpts((c) => c.kind === 'date' || c.kind === 'timestamp', false))}
|
|
281
|
+
${select('end', tx('wiz.end_date'), defaults.end ?? '', colOpts((c) => c.kind === 'date' || c.kind === 'timestamp'))}
|
|
282
|
+
${select('title', tx('wiz.event_title'), defaults.title ?? '', colOpts(notBinary))}
|
|
279
283
|
${pk ? formPage('A modal form page: each event links to it, and a + on every day creates an event there.') : ''}
|
|
280
284
|
</div>
|
|
281
|
-
${pk ? check('drag', '
|
|
285
|
+
${pk ? check('drag', tx('wiz.drag_and_drop'), !!defaults.drag, tx('wiz.users_who_see_the_calendar')) : ''}`;
|
|
282
286
|
break;
|
|
283
287
|
case 'chart':
|
|
284
288
|
fields = html`<div class="form-grid">
|
|
285
|
-
${select('chart', '
|
|
286
|
-
${select('label_column', '
|
|
287
|
-
${select('function', '
|
|
288
|
-
${select('value_column', '
|
|
289
|
+
${select('chart', tx('wiz.chart_type'), defaults.chart, CHART_KINDS)}
|
|
290
|
+
${select('label_column', tx('wiz.label_one_bar_slice_or'), defaults.label_column ?? '', colOpts(notBinary, false))}
|
|
291
|
+
${select('function', tx('wiz.value'), defaults.function, FUNCTIONS())}
|
|
292
|
+
${select('value_column', tx('wiz.of_column'), defaults.value_column ?? '', colOpts((c) => c.kind === 'number'), tx('wiz.count_empty_counts_the_rows'))}
|
|
289
293
|
</div>`;
|
|
290
294
|
break;
|
|
291
295
|
case 'map':
|
|
292
296
|
fields = html`<div class="form-grid">
|
|
293
|
-
${select('location', '
|
|
294
|
-
${select('lat', '
|
|
295
|
-
${select('lng', '
|
|
296
|
-
${select('title', '
|
|
297
|
-
${select('body', '
|
|
298
|
-
${pk ? formPage('
|
|
297
|
+
${select('location', tx('wiz.position'), defaults.location ?? '', colOpts((c) => c.kind === 'geometry' || c.kind === 'point' || c.kind === 'text'), 'A PostGIS geometry, a point, or text "latitude,longitude" (a location item). Empty: the latitude and longitude columns.')}
|
|
298
|
+
${select('lat', tx('wiz.latitude'), defaults.lat ?? '', colOpts((c) => c.kind === 'number'))}
|
|
299
|
+
${select('lng', tx('wiz.longitude'), defaults.lng ?? '', colOpts((c) => c.kind === 'number'))}
|
|
300
|
+
${select('title', tx('wiz.popup_title'), defaults.title ?? '', colOpts(notBinary))}
|
|
301
|
+
${select('body', tx('wiz.popup_text'), defaults.body ?? '', colOpts(notBinary))}
|
|
302
|
+
${pk ? formPage(tx('wiz.form_map')) : ''}
|
|
299
303
|
</div>
|
|
300
|
-
${check('report', '
|
|
304
|
+
${check('report', tx('wiz.a_list_below_the_map'), defaults.report !== false, tx('wiz.an_interactive_report_of_the'))}`;
|
|
301
305
|
break;
|
|
302
306
|
case 'facets':
|
|
303
307
|
fields = html`
|
|
304
|
-
${colChecks('columns', '
|
|
305
|
-
${colChecks('facets', '
|
|
306
|
-
${check('search', '
|
|
307
|
-
${pk ? html`<div class="form-grid">${formPage('
|
|
308
|
+
${colChecks('columns', tx('wiz.col_report_columns'), defaults.columns ?? [], notBinary, (c) => (c.is_pk ? tx('wiz.note_key') : c.fk_table && c.fk_display ? tx('wiz.note_with', { col: `${c.fk_table}.${c.fk_display}` }) : ''))}
|
|
309
|
+
${colChecks('facets', tx('wiz.col_facets'), defaults.facets ?? [], (c) => notBinary(c) && c.kind !== 'point' && c.kind !== 'other', (c) => (c.fk_table ? tx('wiz.note_values_of', { col: `${c.fk_table}.${c.fk_display ?? c.column_name}` }) : ['number', 'date', 'timestamp'].includes(c.kind) ? tx('wiz.note_range') : tx('wiz.note_counts')))}
|
|
310
|
+
${check('search', tx('wiz.a_search_field_above_the'), defaults.search !== false)}
|
|
311
|
+
${pk ? html`<div class="form-grid">${formPage(tx('wiz.form_rows'))}</div>` : ''}`;
|
|
308
312
|
break;
|
|
309
313
|
// (137) the page types of migration 137
|
|
310
314
|
case 'report':
|
|
311
|
-
fields = pk ? html`<div class="form-grid">${formPage('
|
|
315
|
+
fields = pk ? html`<div class="form-grid">${formPage(tx('wiz.form_rows'))}</div>` : '';
|
|
312
316
|
break;
|
|
313
317
|
case 'smart_filters':
|
|
314
318
|
fields = html`
|
|
315
|
-
${colChecks('facets', '
|
|
316
|
-
${pk ? html`<div class="form-grid">${formPage('
|
|
319
|
+
${colChecks('facets', tx('wiz.col_filters'), defaults.facets ?? [], (c) => notBinary(c) && c.kind !== 'point' && c.kind !== 'other', (c) => (c.fk_table ? tx('wiz.note_values_of', { col: `${c.fk_table}.${c.fk_display ?? c.column_name}` }) : ['number', 'date', 'timestamp'].includes(c.kind) ? tx('wiz.note_ranges') : tx('wiz.note_values')))}
|
|
320
|
+
${pk ? html`<div class="form-grid">${formPage(tx('wiz.form_rows'))}</div>` : ''}`;
|
|
317
321
|
break;
|
|
318
322
|
case 'tree':
|
|
319
323
|
fields = html`<div class="form-grid">
|
|
320
|
-
${select('id_column', '
|
|
321
|
-
${select('parent_column', '
|
|
322
|
-
${select('label_column', '
|
|
323
|
-
${input('expanded', '
|
|
324
|
+
${select('id_column', tx('wiz.node_id'), defaults.id_column ?? '', colOpts(allCols, false), tx('wiz.what_identifies_a_node_usually'))}
|
|
325
|
+
${select('parent_column', tx('wiz.parent'), defaults.parent_column ?? '', colOpts(allCols), tx('wiz.the_column_with_the_parent'))}
|
|
326
|
+
${select('label_column', tx('wiz.label'), defaults.label_column ?? '', colOpts(notBinary, false))}
|
|
327
|
+
${input('expanded', tx('wiz.levels_open_at_first'), defaults.expanded ?? 1, { type: 'number' })}
|
|
324
328
|
${pk ? formPage('A modal form page, opened by clicking a node.') : ''}
|
|
325
329
|
</div>`;
|
|
326
330
|
break;
|
|
327
331
|
case 'timeline':
|
|
328
332
|
fields = html`<div class="form-grid">
|
|
329
|
-
${select('title', '
|
|
330
|
-
${select('when', '
|
|
331
|
-
${select('who', '
|
|
332
|
-
${select('body', '
|
|
333
|
+
${select('title', tx('wiz.title'), defaults.title ?? '', colOpts(notBinary, false))}
|
|
334
|
+
${select('when', tx('wiz.when'), defaults.when ?? '', colOpts(dates), tx('wiz.the_events_come_newest_first'))}
|
|
335
|
+
${select('who', tx('wiz.who'), defaults.who ?? '', colOpts(notBinary))}
|
|
336
|
+
${select('body', tx('wiz.text'), defaults.body ?? '', colOpts(notBinary))}
|
|
333
337
|
${pk ? formPage('A modal form page, linked from each title.') : ''}
|
|
334
338
|
</div>`;
|
|
335
339
|
break;
|
|
336
340
|
case 'media_list':
|
|
337
341
|
case 'content_row':
|
|
338
342
|
fields = html`<div class="form-grid">
|
|
339
|
-
${select('title', '
|
|
340
|
-
${select('description', '
|
|
341
|
-
${kind === 'content_row' ? select('misc', '
|
|
342
|
-
${select('badge', '
|
|
343
|
+
${select('title', tx('wiz.title'), defaults.title ?? '', colOpts(notBinary, false), tx('wiz.its_first_two_letters_are'))}
|
|
344
|
+
${select('description', tx('wiz.description'), defaults.description ?? '', colOpts(notBinary))}
|
|
345
|
+
${kind === 'content_row' ? select('misc', tx('wiz.secondary_text_on_the_right'), defaults.misc ?? '', colOpts(notBinary)) : ''}
|
|
346
|
+
${select('badge', tx('wiz.badge'), defaults.badge ?? '', colOpts(notBinary))}
|
|
343
347
|
${pk ? formPage('A modal form page, linked from each title.') : ''}
|
|
344
348
|
</div>`;
|
|
345
349
|
break;
|
|
346
350
|
case 'comments':
|
|
347
351
|
fields = html`<div class="form-grid">
|
|
348
|
-
${select('user', '
|
|
349
|
-
${select('date', '
|
|
350
|
-
${select('comment', '
|
|
352
|
+
${select('user', tx('wiz.who_wrote_it'), defaults.user ?? '', colOpts(notBinary))}
|
|
353
|
+
${select('date', tx('wiz.when'), defaults.date ?? '', colOpts(dates), tx('wiz.the_newest_first'))}
|
|
354
|
+
${select('comment', tx('wiz.comment'), defaults.comment ?? '', colOpts(notBinary, false))}
|
|
351
355
|
</div>`;
|
|
352
356
|
break;
|
|
353
357
|
case 'data_load': {
|
|
354
358
|
const defs = (await owner.query<{ name: string; table_name: string }>('select name, table_name from meta.data_load_def where app_id = $1 order by name', [a.id])).rows;
|
|
355
359
|
fields = html`<div class="form-grid">
|
|
356
|
-
${input('definition', '
|
|
357
|
-
${select('mode', '
|
|
360
|
+
${input('definition', tx('wiz.data_load_definition'), defaults.definition ?? '', { required: true, help: defs.length ? tx('wiz.def_help_has', { table: rel?.t ?? tx('wiz.the_table'), list: defs.map((d) => d.name).join(', ') }) : tx('wiz.def_help', { table: rel?.t ?? tx('wiz.the_table') }) })}
|
|
361
|
+
${select('mode', tx('wiz.mode'), defaults.mode ?? 'append', [['append', tx('wiz.append_add_the_rows')], ['merge', tx('wiz.merge_update_rows_with_the')], ['replace', tx('wiz.replace_delete_all_rows_first')]])}
|
|
358
362
|
</div>
|
|
359
|
-
${check('skip_errors', '
|
|
363
|
+
${check('skip_errors', tx('wiz.skip_rows_with_errors'), !!defaults.skip_errors, tx('wiz.load_the_good_rows_and'))}`;
|
|
360
364
|
break;
|
|
361
365
|
}
|
|
362
366
|
case 'wizard':
|
|
363
367
|
fields = html`<div class="form-grid">
|
|
364
|
-
${input('steps', '
|
|
365
|
-
${input('return_page', '
|
|
368
|
+
${input('steps', tx('wiz.steps'), defaults.steps ?? 2, { type: 'number', help: tx('wiz.one_page_per_step_numbered') })}
|
|
369
|
+
${input('return_page', tx('wiz.after_finish_or_cancel_go'), defaults.return_page ?? '', { type: 'number', placeholder: a.home_page ? tx('wiz.return_home', { n: a.home_page }) : tx('wiz.return_this') })}
|
|
366
370
|
</div>
|
|
367
|
-
${colChecks('columns', '
|
|
371
|
+
${colChecks('columns', tx('wiz.col_columns'), defaults.columns ?? [], (c) => !c.is_pk && !c.generated && c.kind !== 'binary' && c.kind !== 'geometry', (c) => (c.not_null && !c.has_default ? tx('wiz.note_required') : c.fk_table ? tx('wiz.note_select_list', { table: c.fk_table }) : ''))}`;
|
|
368
372
|
break;
|
|
369
373
|
case 'search': {
|
|
370
374
|
const configs = (await owner.query<{ name: string; label: string; source: string }>('select name, label, source from meta.search_config where app_id = $1 order by seq, name', [a.id])).rows;
|
|
371
375
|
const tables = await wizardTables(role);
|
|
372
376
|
fields = html`
|
|
373
377
|
${configs.length
|
|
374
|
-
? html`<fieldset class="prop-group"><legend
|
|
375
|
-
: html`<p class="muted"
|
|
378
|
+
? html`<fieldset class="prop-group"><legend>${tx('wiz.search_configurations')}</legend><div class="qb-pick">${configs.map((c) => html`<label class="check"><input type="checkbox" name="configs" value="${c.name}" checked> ${c.label} <span class="muted small">${c.name}, ${c.source.slice(0, 60)}</span></label>`)}</div></fieldset>`
|
|
379
|
+
: html`<p class="muted">${tx.rich('wiz.no_search_configs', { link: html`<a href="${BASE}/apps/${a.id}/shared?type=search_config">${tx('wiz.shared_components_search_configurations')}</a>` })}</p>`}
|
|
376
380
|
<div class="form-grid">
|
|
377
|
-
${select('table', '
|
|
378
|
-
${formPage('
|
|
381
|
+
${select('table', tx('wiz.and_a_new_configuration_on'), rel?.t ?? '', [['', tx('wiz.none')], ...tables.filter((t) => t.access).map((t): [string, string] => [t.t, t.t])], tx('wiz.searches_the_table_s_text'))}
|
|
382
|
+
${formPage(tx('wiz.form_search'))}
|
|
379
383
|
</div>`;
|
|
380
384
|
break;
|
|
381
385
|
}
|
|
382
386
|
case 'tasks':
|
|
383
|
-
fields = html`<div class="form-grid">${select('context', '
|
|
384
|
-
${check('completed', '
|
|
385
|
-
<p class="muted small"
|
|
387
|
+
fields = html`<div class="form-grid">${select('context', tx('wiz.show'), defaults.context ?? 'my', TASK_CONTEXTS())}</div>
|
|
388
|
+
${check('completed', tx('wiz.also_completed_tasks'), !!defaults.completed)}
|
|
389
|
+
<p class="muted small">${tx('wiz.tasks_come_from_task_definitions')}</p>`;
|
|
386
390
|
break;
|
|
387
391
|
case 'workflows':
|
|
388
|
-
fields = html`<div class="form-grid">${select('context', '
|
|
392
|
+
fields = html`<div class="form-grid">${select('context', tx('wiz.show'), defaults.context ?? 'initiator', [['initiator', tx('wiz.the_workflows_the_user_started')], ['admin', tx('wiz.the_workflows_the_user_administers')]])}</div>`;
|
|
389
393
|
break;
|
|
390
394
|
case 'dashboard': {
|
|
391
395
|
const tables = (await wizardTables(role)).filter((t) => t.access);
|
|
392
396
|
fields = tables.length
|
|
393
|
-
? html`<fieldset class="prop-group"><legend
|
|
394
|
-
<p class="muted small"
|
|
395
|
-
: html`<p class="alert alert-error" role="status"
|
|
397
|
+
? html`<fieldset class="prop-group"><legend>${tx('wiz.tables')}</legend><div class="qb-pick">${tables.map((t, n) => html`<label class="check"><input type="checkbox" name="tables" value="${t.t}"${n < 12 ? raw(' checked') : ''}> ${t.t}</label>`)}</div></fieldset>
|
|
398
|
+
<p class="muted small">${tx('wiz.a_chart_per_table_at')}</p>`
|
|
399
|
+
: html`<p class="alert alert-error" role="status">${tx('wiz.the_application_s_database_role')}</p>`;
|
|
396
400
|
break;
|
|
397
401
|
}
|
|
398
402
|
case 'master_detail': {
|
|
@@ -407,33 +411,33 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
407
411
|
)
|
|
408
412
|
).rows;
|
|
409
413
|
fields = details.length
|
|
410
|
-
? select('detail', '
|
|
411
|
-
: html`<p class="alert alert-error" role="status"
|
|
414
|
+
? select('detail', tx('wiz.detail_table'), defaults.detail ? `${defaults.detail}|${defaults.detail_column}` : '', details.map((d): [string, string] => [`${d.t}|${d.col}`, `${d.t} (${d.col} → ${t})`]), tx('wiz.tables_with_a_foreign_key'))
|
|
415
|
+
: html`<p class="alert alert-error" role="status">${tx('wiz.no_fk', { table: t })}</p>`;
|
|
412
416
|
break;
|
|
413
417
|
}
|
|
414
418
|
}
|
|
415
419
|
|
|
416
420
|
const kindDef = PAGE_KINDS.find((k) => k[0] === kind)!;
|
|
417
|
-
const kindName = kindDef[1];
|
|
421
|
+
const kindName = kindLabel(kindDef[0], kindDef[1]);
|
|
418
422
|
const main = html`${appHeader(a, 'pages', s)}
|
|
419
|
-
${region(rel && kind !== 'search' ?
|
|
420
|
-
<p class="muted u-mt0">${kindDef[2]} ${rel ? '
|
|
421
|
-
${rel && needsPk && !pk ? html`<p class="alert alert-error" role="status">${
|
|
423
|
+
${region(rel && kind !== 'search' ? tx('wiz.kind_on', { kind: kindName, table: rel.t }) : kindName, html`
|
|
424
|
+
<p class="muted u-mt0">${kindHelp(kindDef[0], kindDef[2])} ${rel ? tx('wiz.the_proposals_come_from_the') : ''} <a href="${BASE}/apps/${a.id}/create-page">${rel ? tx('wiz.back_choose_table') : tx('wiz.back_choose')}</a>.</p>
|
|
425
|
+
${rel && needsPk && !pk ? html`<p class="alert alert-error" role="status">${tx('wiz.no_pk', { table: rel.t })}</p>` : ''}
|
|
422
426
|
${rel && role && access && !(access.s && (!(needsPk || kind === 'wizard' || kind === 'data_load') || (access.i && (kind === 'wizard' || (access.u && access.d)))))
|
|
423
|
-
? html`<p class="alert alert-error" role="status"
|
|
427
|
+
? html`<p class="alert alert-error" role="status">${tx.rich(access.s ? 'wiz.role_read_only' : 'wiz.role_no_access', { role: html`<code>${role}</code>`, table: rel.t, privs: needsPk ? 'select, insert, update, delete' : kind === 'wizard' ? 'select, insert' : kind === 'data_load' ? tx('wiz.privs_merge') : 'select' })}</p>`
|
|
424
428
|
: ''}
|
|
425
429
|
<form method="post" action="${BASE}/apps/${a.id}/wizard">${csrf(s)}
|
|
426
430
|
<input type="hidden" name="kind" value="${kind}">${rel && kind !== 'search' ? html`<input type="hidden" name="table" value="${rel.t}">` : ''}
|
|
427
431
|
<div class="form-grid">
|
|
428
|
-
${input('report_page', '
|
|
429
|
-
${input('label', '
|
|
430
|
-
${select('icon', '
|
|
432
|
+
${input('report_page', tx('wiz.page_number'), nextPage, { type: 'number', required: true })}
|
|
433
|
+
${input('label', tx('wiz.name'), defaults.label ?? '', { help: tx('wiz.the_page_title_and_the') })}
|
|
434
|
+
${select('icon', tx('wiz.menu_icon'), defaults.icon ?? 'table', [...ICONS])}
|
|
431
435
|
</div>
|
|
432
|
-
${['report_form', 'grid', 'dashboard'].includes(kind) ? '' : check('nav', '
|
|
436
|
+
${['report_form', 'grid', 'dashboard'].includes(kind) ? '' : check('nav', tx('wiz.add_a_navigation_entry'), !!defaults.nav)}
|
|
433
437
|
${fields}
|
|
434
|
-
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}"
|
|
438
|
+
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}">${tx('wiz.cancel')}</a><button class="btn btn-hot">${tx('wiz.create_page')}</button></div>
|
|
435
439
|
</form>`)}`;
|
|
436
|
-
return send(reply, s, shell(s,
|
|
440
|
+
return send(reply, s, shell(s, tx('wiz.page_title', { app: a.name }), [[tx('wiz.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('wiz.create_page'), `${BASE}/apps/${a.id}/create-page`], [kindName]], main));
|
|
437
441
|
});
|
|
438
442
|
|
|
439
443
|
app.post(`${BASE}/apps/:id/wizard`, async (req: Req, reply) => {
|
|
@@ -445,22 +449,22 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
445
449
|
const retry = `${BASE}/apps/${req.params.id}/wizard?kind=${encodeURIComponent(kind)}${table && kind !== 'search' ? `&table=${encodeURIComponent(table)}` : ''}`;
|
|
446
450
|
try {
|
|
447
451
|
const page = pageNo(b.report_page);
|
|
448
|
-
if (page === null) throw new Error('
|
|
452
|
+
if (page === null) throw new Error(tx('wiz.enter_a_page_number'));
|
|
449
453
|
const options = wizardOptions(kind, b);
|
|
450
|
-
if (!table && !NO_TABLE.has(kind)) throw new Error('
|
|
454
|
+
if (!table && !NO_TABLE.has(kind)) throw new Error(tx('wiz.choose_a_table_or_view'));
|
|
451
455
|
const id = await owner.tx(async (c) => {
|
|
452
456
|
const a = await c.query('select alias, db_role from meta.app where id = $1', [req.params.id]);
|
|
453
|
-
if (!a.rowCount) throw new Error('
|
|
457
|
+
if (!a.rowCount) throw new Error(tx('wiz.this_application_does_not_exist'));
|
|
454
458
|
// report and form and grid keep their own icons and need no step 2 (older forms post here directly)
|
|
455
|
-
if (kind === 'report_form' && options.form_page === null) throw new Error('
|
|
459
|
+
if (kind === 'report_form' && options.form_page === null) throw new Error(tx('wiz.enter_the_form_page_number'));
|
|
456
460
|
if (['report_form', 'grid'].includes(kind) && !options.icon) options.icon = kind === 'grid' ? 'grid' : 'table';
|
|
457
461
|
if (kind === 'dashboard') {
|
|
458
462
|
// (137) only tables the application's role can read
|
|
459
463
|
const readable = new Set((await wizardTables(a.rows[0].db_role)).filter((t) => t.access).map((t) => t.t));
|
|
460
464
|
const tables = (options.tables as string[]).filter((t) => readable.has(t));
|
|
461
|
-
if (!tables.length) throw new Error('
|
|
465
|
+
if (!tables.length) throw new Error(tx('wiz.choose_at_least_one_table'));
|
|
462
466
|
const made = await addDashboard(c, a.rows[0].alias, tables, page);
|
|
463
|
-
if (!made) throw new Error('
|
|
467
|
+
if (!made) throw new Error(tx('wiz.none_of_these_tables_has'));
|
|
464
468
|
const pid = (await c.query('select id from meta.page where app_id = $1 and page_no = $2', [req.params.id, page])).rows[0].id as number;
|
|
465
469
|
if (options.label) {
|
|
466
470
|
await c.query('update meta.page set name = $2, title = $2 where id = $1', [pid, options.label]);
|
|
@@ -472,7 +476,9 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
472
476
|
return (await c.query('select meta.generate_page($1, $2, $3::regclass, $4, $5::jsonb) as id', [a.rows[0].alias, kind, table || null, page, JSON.stringify(options)])).rows[0].id as number;
|
|
473
477
|
});
|
|
474
478
|
const pages = (await owner.query('select page_no from meta.page where app_id = $1 and id >= $2 order by page_no', [req.params.id, id])).rows.map((r) => r.page_no);
|
|
475
|
-
flash(s,
|
|
479
|
+
flash(s, table
|
|
480
|
+
? (pages.length > 1 ? tx('wiz.created_pages_for', { list: pages.join(', '), table }) : tx('wiz.created_page_for', { n: page, table }))
|
|
481
|
+
: (pages.length > 1 ? tx('wiz.created_pages', { list: pages.join(', ') }) : tx('wiz.created_page', { n: page })));
|
|
476
482
|
return back(reply, s, `${BASE}/pages/${id}`);
|
|
477
483
|
} catch (e) {
|
|
478
484
|
flash(s, (e as Error).message, 'error');
|
|
@@ -5,6 +5,7 @@ import { clientIp, logActivity } from '../session.ts';
|
|
|
5
5
|
import { workflowDiagram, type Step } from '../workflow.ts';
|
|
6
6
|
import { appOr404 } from './forms.ts';
|
|
7
7
|
import { appHeader, back, BASE, csrf, developer, flash, region, send, shell, type Req } from './ui.ts';
|
|
8
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
8
9
|
|
|
9
10
|
// (123) Workflows in the builder: migrating running instances to a newer
|
|
10
11
|
// version (APEX 26.2: in-flight workflow migration) and the diagram as a file.
|
|
@@ -80,7 +81,7 @@ export async function workflowMigrateRoutes(app: FastifyInstance) {
|
|
|
80
81
|
const pick = v ? versions.find((x) => x.version === v) : versions[versions.length - 1];
|
|
81
82
|
if (!pick) return reply.code(404).send('No such version');
|
|
82
83
|
const svg = workflowDiagram(pick.steps, { id: 'export', standalone: { title: `${d.name} version ${pick.version}` } }).toString();
|
|
83
|
-
if (!svg) return reply.code(404).send('
|
|
84
|
+
if (!svg) return reply.code(404).send(tx('wfm.no_steps'));
|
|
84
85
|
const file = `${d.name.toLowerCase()}-v${pick.version}.svg`.replace(/[^a-z0-9._-]/gi, '_');
|
|
85
86
|
return reply
|
|
86
87
|
.header('content-type', 'image/svg+xml; charset=utf-8')
|
|
@@ -126,7 +127,7 @@ export async function workflowMigrateRoutes(app: FastifyInstance) {
|
|
|
126
127
|
let preview: { workflow_id: string; branch: string | null; from_version: string | null; from_step: string | null; to_step: string | null; problem: string | null }[] | null = null;
|
|
127
128
|
let previewError: string | null = null;
|
|
128
129
|
if (q.preview && to) {
|
|
129
|
-
if (!chosen.length) previewError = '
|
|
130
|
+
if (!chosen.length) previewError = tx('wfm.choose_instances');
|
|
130
131
|
else {
|
|
131
132
|
try {
|
|
132
133
|
preview = (await owner.query('select workflow_id::text, branch, from_version, from_step, to_step, problem from meta.migrate_workflows($1, $2, $3::bigint[], $4, false, $5)',
|
|
@@ -141,52 +142,50 @@ export async function workflowMigrateRoutes(app: FastifyInstance) {
|
|
|
141
142
|
const self = `${url(d)}/migrate`;
|
|
142
143
|
const hidden = html`<input type="hidden" name="to" value="${to ?? ''}">${chosen.map((w) => html`<input type="hidden" name="wf" value="${w.id}">`)}${Object.entries(mapping).map(([k, v]) => html`<input type="hidden" name="${`m.${k}`}" value="${v}">`)}`;
|
|
143
144
|
const body = !running.length
|
|
144
|
-
? region('
|
|
145
|
+
? region(tx('wfm.title'), html`<p class="muted u-mt0">${tx('wfm.none_running', { name: d.name })}</p><p><a class="btn" href="${BASE}/apps/${a.id}/shared?c=workflow_definition-${d.id}">${tx('wfm.back_to', { name: d.name })}</a></p>`)
|
|
145
146
|
: !targets.length
|
|
146
|
-
? region('
|
|
147
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/shared?c=workflow_definition-${d.id}"
|
|
148
|
-
: html`${region('
|
|
147
|
+
? region(tx('wfm.title'), html`<p class="muted u-mt0">${tx('wfm.all_current', { name: d.name, version: d.version })}</p>
|
|
148
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/shared?c=workflow_definition-${d.id}">${tx('wfm.back_to', { name: d.name })}</a></p>`)
|
|
149
|
+
: html`${region(tx('wfm.title'), html`<form method="get" action="${self}">
|
|
149
150
|
<input type="hidden" name="preview" value="1">
|
|
150
|
-
<p class="muted u-mt0">
|
|
151
|
-
keeping its variables and the task it waits for. An instance inside parallel branches moves as a whole: its main path at its join and each open
|
|
152
|
-
branch at its own step, each mapped (a join stays a join). Every position must have a mapping, or nothing moves.</p>
|
|
151
|
+
<p class="muted u-mt0">${tx('wfm.intro')}</p>
|
|
153
152
|
<div class="form-grid">
|
|
154
|
-
<div class="field"><label class="label" for="wf-to"
|
|
155
|
-
<select id="wf-to" name="to">${targets.map((v) => html`<option value="${v}"${v === to ? raw(' selected') : ''}>${v}${v === d.version ? '
|
|
156
|
-
<small class="help"
|
|
153
|
+
<div class="field"><label class="label" for="wf-to">${tx('wfm.to_version')}</label>
|
|
154
|
+
<select id="wf-to" name="to">${targets.map((v) => html`<option value="${v}"${v === to ? raw(' selected') : ''}>${v} (${tx(v === d.version ? 'wfv.state.active' : 'wfv.state.inactive')})</option>`)}</select>
|
|
155
|
+
<small class="help">${tx('wfm.to_help')}</small></div>
|
|
157
156
|
</div>
|
|
158
|
-
<fieldset class="prop-group u-mt1"><legend
|
|
157
|
+
<fieldset class="prop-group u-mt1"><legend>${tx('wfm.older')}</legend>
|
|
159
158
|
${older.length
|
|
160
|
-
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th
|
|
161
|
-
${older.map((w) => html`<tr><td data-label="
|
|
162
|
-
<td class="num" data-label="
|
|
163
|
-
<td data-label="
|
|
159
|
+
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('wfm.migrate')}</th><th class="num">${tx('wfv.id')}</th><th>${tx('wfv.title')}</th><th>${tx('wfv.version')}</th><th>${tx('wfv.state')}</th><th>${tx('wfv.at')}</th></tr></thead><tbody>
|
|
160
|
+
${older.map((w) => html`<tr><td data-label="${tx('wfm.migrate')}"><input type="checkbox" name="wf" value="${w.id}" id="${`wf-${w.id}`}"${picked.includes(w.id) ? raw(' checked') : ''} aria-label="${tx('wfm.migrate_instance', { id: w.id })}"></td>
|
|
161
|
+
<td class="num" data-label="${tx('wfv.id')}"><label for="${`wf-${w.id}`}">${w.id}</label></td><td data-label="${tx('wfv.title')}">${w.title}</td><td data-label="${tx('wfv.version')}">${w.version}</td>
|
|
162
|
+
<td data-label="${tx('wfv.state')}">${w.state}</td><td data-label="${tx('wfv.at')}">${w.current_step ?? '—'}${w.branch_steps.length ? html`<br><small class="muted">${tx('wfm.branches_at', { steps: w.branch_steps.join(', ') })}</small>` : ''}</td></tr>`)}
|
|
164
163
|
</tbody></table></div>`
|
|
165
|
-
: html`<p class="muted"
|
|
164
|
+
: html`<p class="muted">${tx('wfm.none_older', { version: to ?? '' })}</p>`}
|
|
166
165
|
</fieldset>
|
|
167
166
|
${current.length
|
|
168
|
-
? html`<fieldset class="prop-group u-mt1"><legend
|
|
167
|
+
? html`<fieldset class="prop-group u-mt1"><legend>${tx('wfm.map', { version: to ?? '' })}</legend>
|
|
169
168
|
<div class="wf-migrate-map">${current.map((st) => html`<label>${st}
|
|
170
|
-
<select name="${`m.${st}`}"><option value=""
|
|
169
|
+
<select name="${`m.${st}`}"><option value="">${tx('wfm.no_mapping')}</option>${targetSteps.map((x) => html`<option value="${x.name}"${mapping[st] === x.name ? raw(' selected') : ''}>${x.name} (${x.type})</option>`)}</select></label>`)}</div>
|
|
171
170
|
</fieldset>`
|
|
172
171
|
: ''}
|
|
173
|
-
<div class="buttons u-mt1"><button class="btn"
|
|
172
|
+
<div class="buttons u-mt1"><button class="btn">${tx('wfm.preview')}</button> <a class="btn" href="${BASE}/apps/${a.id}/shared?c=workflow_definition-${d.id}">${tx('common.cancel')}</a></div>
|
|
174
173
|
</form>`)}
|
|
175
174
|
${previewError ? html`<div class="alert alert-error u-mt1" role="alert">${previewError}</div>` : ''}
|
|
176
175
|
${preview
|
|
177
|
-
? region('
|
|
178
|
-
${preview.map((r) => html`<tr><td class="num" data-label="
|
|
179
|
-
<td data-label="
|
|
176
|
+
? region(tx('wfm.preview'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th class="num">${tx('wfv.id')}</th><th>${tx('wfm.path')}</th><th>${tx('wfm.from')}</th><th>${tx('wfm.to')}</th><th>${tx('wfm.result')}</th></tr></thead><tbody>
|
|
177
|
+
${preview.map((r) => html`<tr><td class="num" data-label="${tx('wfv.id')}">${r.workflow_id}</td><td data-label="${tx('wfm.path')}">${r.branch ? tx('wfm.branch', { name: r.branch }) : tx('wfm.main')}</td><td data-label="${tx('wfm.from')}">${r.from_version ? `${r.from_version}: ${r.from_step ?? '—'}` : '—'}</td>
|
|
178
|
+
<td data-label="${tx('wfm.to')}">${r.to_step ? `${to}: ${r.to_step}` : '—'}</td><td data-label="${tx('wfm.result')}">${r.problem ? html`<span class="wf-problem">${r.problem}</span>` : html`<span class="tag tag-info">${tx('wfm.ready')}</span>`}</td></tr>`)}
|
|
180
179
|
</tbody></table></div>
|
|
181
180
|
${ready
|
|
182
181
|
? html`<form method="post" action="${self}" class="u-mt1">${csrf(s)}${hidden}
|
|
183
|
-
<button class="btn btn-hot" data-confirm="${
|
|
184
|
-
: html`<p class="wf-problem"
|
|
182
|
+
<button class="btn btn-hot" data-confirm="${tx('wfm.confirm', { n: instances, version: to ?? '' })}">${tx('wfm.do', { n: instances })}</button></form>`
|
|
183
|
+
: html`<p class="wf-problem">${tx('wfm.not_ready')}</p>`}`)
|
|
185
184
|
: ''}
|
|
186
|
-
${target ? region(
|
|
185
|
+
${target ? region(tx('wfm.version', { version: to ?? '' }), html`<div class="wf-wrap">${workflowDiagram(target.steps, { id: 'target' })}</div>`) : ''}`;
|
|
187
186
|
const main = html`${appHeader(a, 'shared', s)}<div class="ide-body">${body}</div>`;
|
|
188
|
-
return send(reply, s, shell(s,
|
|
189
|
-
[d.name, `${BASE}/apps/${a.id}/shared?c=workflow_definition-${d.id}`], ['
|
|
187
|
+
return send(reply, s, shell(s, tx('wfm.page_title', { name: d.name }), [[tx('rail.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('app.tab.shared'), `${BASE}/apps/${a.id}/shared`],
|
|
188
|
+
[d.name, `${BASE}/apps/${a.id}/shared?c=workflow_definition-${d.id}`], [tx('wfm.crumb')]], main));
|
|
190
189
|
});
|
|
191
190
|
|
|
192
191
|
app.post(`${BASE}/apps/:id/shared/workflow_definition/:cid/migrate`, async (req: Req, reply) => {
|
|
@@ -200,12 +199,12 @@ export async function workflowMigrateRoutes(app: FastifyInstance) {
|
|
|
200
199
|
const mapping = mappingOf(b);
|
|
201
200
|
const self = `${url(d)}/migrate`;
|
|
202
201
|
try {
|
|
203
|
-
if (!to) throw new Error('
|
|
202
|
+
if (!to) throw new Error(tx('wfm.choose_version'));
|
|
204
203
|
const rows = await owner.tx(async (c) =>
|
|
205
204
|
(await c.query('select workflow_id::text, from_step, to_step from meta.migrate_workflows($1, $2, $3::bigint[], $4, true, $5)', [d.id, to, ids, mapping, s.username])).rows);
|
|
206
205
|
const moved = new Set(rows.map((r) => r.workflow_id)).size;
|
|
207
206
|
logActivity({ appId: d.app_id, username: s.username, event: 'workflow', ip: clientIp(req), detail: `${d.name}: ${moved} instance(s) migrated to version ${to}` });
|
|
208
|
-
flash(s,
|
|
207
|
+
flash(s, tx('wfm.done', { n: moved, name: d.name, version: to }));
|
|
209
208
|
return back(reply, s, `${BASE}/apps/${d.app_id}/shared?c=workflow_definition-${d.id}`);
|
|
210
209
|
} catch (e) {
|
|
211
210
|
flash(s, (e as Error).message, 'error');
|