pgkiln 0.84.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/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +14 -5
- package/docs/guide/12-development.md +6 -1
- package/package.json +1 -1
- package/src/app.ts +9 -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/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 +3453 -0
- package/src/builder/i18n/en.ts +3453 -0
- package/src/builder/i18n/es.ts +3453 -0
- package/src/builder/i18n/fr.ts +3453 -0
- package/src/builder/i18n/it.ts +3453 -0
- package/src/builder/i18n/ja.ts +3453 -0
- package/src/builder/i18n/ko.ts +3453 -0
- package/src/builder/i18n/nl.ts +3453 -0
- package/src/builder/i18n/pt.ts +3453 -0
- package/src/builder/i18n/zh-cn.ts +3453 -0
- package/src/builder/i18n/zh-tw.ts +3453 -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/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 +27 -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/builder/workflows.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Session } from '../session.ts';
|
|
|
5
5
|
import { invokeStepReferences, outcomeMap, stepProblems, stepWarnings, workflowDiagram, type Step } from '../workflow.ts';
|
|
6
6
|
import type { ComponentSpec } from './components.ts';
|
|
7
7
|
import { back, BASE, csrf, developer, flash, type Req } from './ui.ts';
|
|
8
|
+
import { scopedTx as tx, type BuilderKey } from './i18n.ts';
|
|
8
9
|
|
|
9
10
|
// Shared Components → Workflows: versions (APEX: development, active,
|
|
10
11
|
// inactive), the diagram of the steps, problems with them, and the instances
|
|
@@ -53,7 +54,7 @@ export async function workflowBeforeSave(values: Record<string, unknown>, cid: s
|
|
|
53
54
|
return false;
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
|
-
if (!cid && hasDraft(values.steps)) throw new Error('
|
|
57
|
+
if (!cid && hasDraft(values.steps)) throw new Error(tx('wfv.draft_new'));
|
|
57
58
|
if (!cid || values.steps === undefined) return;
|
|
58
59
|
const d = await owner.one('select version, dev_version, steps = $2::jsonb as same from meta.workflow_definition where id = $1', [cid, values.steps]);
|
|
59
60
|
if (!d) return;
|
|
@@ -61,8 +62,8 @@ export async function workflowBeforeSave(values: Record<string, unknown>, cid: s
|
|
|
61
62
|
values.dev_steps = values.steps;
|
|
62
63
|
delete values.steps;
|
|
63
64
|
} else if (d.same) delete values.steps;
|
|
64
|
-
else if (hasDraft(values.steps)) throw new Error(
|
|
65
|
-
else throw new Error(
|
|
65
|
+
else if (hasDraft(values.steps)) throw new Error(tx('wfv.draft_active', { version: d.version }));
|
|
66
|
+
else throw new Error(tx('wfv.active_fixed', { version: d.version }));
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export async function workflowExtras(appId: number, row: Definition, s: Session, query: Record<string, string | undefined> = {}) {
|
|
@@ -103,7 +104,7 @@ export async function workflowExtras(appId: number, row: Definition, s: Session,
|
|
|
103
104
|
const action = `${base}/versions`;
|
|
104
105
|
const usage = (v: string) => {
|
|
105
106
|
const u = perVersion.get(v);
|
|
106
|
-
return u ? `${u.n}${u.running ? `
|
|
107
|
+
return u ? `${u.n}${u.running ? ` ${tx('wfv.running', { n: u.running })}` : ''}` : '0';
|
|
107
108
|
};
|
|
108
109
|
const inactive = Array.isArray(row.inactive_versions) ? row.inactive_versions : [];
|
|
109
110
|
const nextLabel = String(Math.max(0, ...[row.version, ...inactive.map((v) => v.version)].filter((v) => /^\d{1,9}$/.test(v)).map(Number)) + 1);
|
|
@@ -112,58 +113,58 @@ export async function workflowExtras(appId: number, row: Definition, s: Session,
|
|
|
112
113
|
{ version: row.version, state: 'active', at: row.activated_at, steps: row.steps },
|
|
113
114
|
...[...inactive].reverse().map((v) => ({ version: v.version, state: 'inactive', at: v.activated_at, steps: v.steps })),
|
|
114
115
|
];
|
|
115
|
-
return html`<fieldset class="prop-group u-mt125"><legend
|
|
116
|
-
<p class="muted u-mt0"
|
|
117
|
-
<div class="table-wrap"><table class="report report-reflow wf-versions"><thead><tr><th
|
|
116
|
+
return html`<fieldset class="prop-group u-mt125"><legend>${tx('wfv.versions')}</legend>
|
|
117
|
+
<p class="muted u-mt0">${tx.rich('wfv.intro', { active: html`<b>${tx('wfv.state.active')}</b>`, development: html`<b>${tx('wfv.state.development')}</b>` })}</p>
|
|
118
|
+
<div class="table-wrap"><table class="report report-reflow wf-versions"><thead><tr><th>${tx('wfv.version')}</th><th>${tx('wfv.state')}</th><th>${tx('wfv.activated')}</th><th class="num">${tx('wfv.instances')}</th><th></th></tr></thead><tbody>
|
|
118
119
|
${versions.map((v, i) => html`<tr>
|
|
119
|
-
<td data-label="
|
|
120
|
-
<td data-label="
|
|
121
|
-
<td data-label="
|
|
122
|
-
<td class="num" data-label="
|
|
120
|
+
<td data-label="${tx('wfv.version')}"><b>${v.version}</b></td>
|
|
121
|
+
<td data-label="${tx('wfv.state')}"><span class="tag${v.state === 'active' ? ' tag-info' : v.state === 'development' ? ' tag-warning' : ''}">${tx(`wfv.state.${v.state}` as BuilderKey)}</span></td>
|
|
122
|
+
<td data-label="${tx('wfv.activated')}">${v.state === 'development' ? '—' : when(v.at)}</td>
|
|
123
|
+
<td class="num" data-label="${tx('wfv.instances')}">${usage(v.version)}</td>
|
|
123
124
|
<td data-label="">${v.state === 'development'
|
|
124
125
|
? html`<form method="post" action="${action}" class="wf-version-actions">${csrf(s)}
|
|
125
|
-
<button class="btn btn-hot" name="action" value="activate"${problems.length || drafts.length ? raw(' disabled') : ''} data-confirm="${
|
|
126
|
-
<button class="btn btn-danger" name="action" value="discard" data-confirm="${
|
|
127
|
-
: v.steps !== shown ? html`<details class="wf-version-diagram"><summary
|
|
126
|
+
<button class="btn btn-hot" name="action" value="activate"${problems.length || drafts.length ? raw(' disabled') : ''} data-confirm="${tx('wfv.activate_confirm', { version: v.version })}">${tx('wfv.activate')}</button>
|
|
127
|
+
<button class="btn btn-danger" name="action" value="discard" data-confirm="${tx('wfv.discard_confirm', { version: v.version })}">${tx('wfv.discard')}</button></form>`
|
|
128
|
+
: v.steps !== shown ? html`<details class="wf-version-diagram"><summary>${tx('wfv.diagram')}</summary><div class="wf-wrap">${workflowDiagram(v.steps, { id: `v${i}` })}</div></details>` : ''}</td>
|
|
128
129
|
</tr>`)}
|
|
129
130
|
</tbody></table></div>
|
|
130
131
|
${dev
|
|
131
132
|
? ''
|
|
132
133
|
: html`<form method="post" action="${action}" class="search u-mwnone u-mt075">${csrf(s)}
|
|
133
|
-
<label class="sr-only" for="wf-new-version"
|
|
134
|
+
<label class="sr-only" for="wf-new-version">${tx('wfv.new_label')}</label>
|
|
134
135
|
<input id="wf-new-version" name="version" placeholder="${nextLabel}" maxlength="30" aria-describedby="wf-new-version-help">
|
|
135
|
-
<button class="btn" name="action" value="new"
|
|
136
|
-
<small id="wf-new-version-help" class="help"
|
|
136
|
+
<button class="btn" name="action" value="new">${tx('wfv.new')}</button>
|
|
137
|
+
<small id="wf-new-version-help" class="help">${tx('wfv.new_help', { version: row.version, next: nextLabel })}</small>
|
|
137
138
|
</form>`}
|
|
138
139
|
</fieldset>
|
|
139
|
-
<fieldset class="prop-group u-mt125"><legend
|
|
140
|
+
<fieldset class="prop-group u-mt125"><legend>${dev ? tx('wfv.diagram_dev', { version: row.dev_version! }) : tx('wfv.diagram_of', { version: row.version })}</legend>
|
|
140
141
|
${problems.length ? html`<div class="alert alert-error" role="alert"><ul class="u-m0">${problems.map((p) => html`<li>${p}</li>`)}</ul></div>` : ''}
|
|
141
142
|
${warnings.length ? html`<div class="alert wf-warnings" role="status"><ul class="u-m0">${warnings.map((p) => html`<li>${p}</li>`)}</ul></div>` : ''}
|
|
142
|
-
${drafts.length ? html`<div class="alert wf-warnings" role="status"
|
|
143
|
+
${drafts.length ? html`<div class="alert wf-warnings" role="status">${tx('wfv.drafts', { steps: drafts.join(', '), version: row.dev_version! })}</div>` : ''}
|
|
143
144
|
<div class="wf-wrap">${workflowDiagram(shown, { id: 'def' })}</div>
|
|
144
145
|
<p class="wf-export">
|
|
145
|
-
<a class="btn btn-sm" href="${`${base}/diagram.svg?v=${encodeURIComponent(dev ? row.dev_version! : row.version)}`}" download data-wf-svg
|
|
146
|
-
<button type="button" class="btn btn-sm" data-wf-png hidden
|
|
146
|
+
<a class="btn btn-sm" href="${`${base}/diagram.svg?v=${encodeURIComponent(dev ? row.dev_version! : row.version)}`}" download data-wf-svg>${tx('wfv.svg')}</a>
|
|
147
|
+
<button type="button" class="btn btn-sm" data-wf-png hidden>${tx('wfv.png')}</button>
|
|
147
148
|
</p>
|
|
148
|
-
<p class="muted small"
|
|
149
|
+
<p class="muted small">${tx.rich('wfv.counts', { counts: counts.length ? html`${counts.map((c, i) => html`${i ? ', ' : ''}${c.n} ${c.state}`)}` : tx('wfv.none_yet') })}</p>
|
|
149
150
|
</fieldset>
|
|
150
151
|
${instances.length
|
|
151
|
-
? html`<fieldset class="prop-group u-mt125"><legend
|
|
152
|
+
? html`<fieldset class="prop-group u-mt125"><legend>${tx('wfv.instances')}</legend>
|
|
152
153
|
${instances.some((w) => ['active', 'waiting', 'faulted'].includes(w.state) && (w.version ?? '1') !== row.version)
|
|
153
|
-
? html`<p class="u-mt0"><a class="btn btn-sm" href="${`${base}/migrate`}" title="
|
|
154
|
-
<div class="table-wrap"><table class="report report-reflow"><thead><tr><th class="num"
|
|
154
|
+
? html`<p class="u-mt0"><a class="btn btn-sm" href="${`${base}/migrate`}" title="${tx('wfv.migrate_title')}">${tx('wfv.migrate')}</a></p>` : ''}
|
|
155
|
+
<div class="table-wrap"><table class="report report-reflow"><thead><tr><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><th>${tx('wfv.started')}</th></tr></thead><tbody>
|
|
155
156
|
${instances.map((w) => html`<tr${picked?.id === w.id ? raw(' aria-current="true"') : ''}>
|
|
156
|
-
<td class="num" data-label="
|
|
157
|
-
<td data-label="
|
|
158
|
-
<td data-label="
|
|
159
|
-
<td data-label="
|
|
160
|
-
<td data-label="
|
|
161
|
-
<td data-label="
|
|
157
|
+
<td class="num" data-label="${tx('wfv.id')}"><a href="?c=workflow_definition-${row.id}&wf=${w.id}">${w.id}</a></td>
|
|
158
|
+
<td data-label="${tx('wfv.title')}">${w.title}</td>
|
|
159
|
+
<td data-label="${tx('wfv.version')}">${w.version ?? '1'}</td>
|
|
160
|
+
<td data-label="${tx('wfv.state')}"><span class="tag${w.state === 'faulted' ? ' tag-error' : w.state === 'completed' || w.state === 'terminated' ? '' : ' tag-info'}">${w.state}</span></td>
|
|
161
|
+
<td data-label="${tx('wfv.at')}">${w.active_steps.join(', ') || '—'}</td>
|
|
162
|
+
<td data-label="${tx('wfv.started')}">${when(w.started_at)} · ${w.initiator}</td></tr>`)}
|
|
162
163
|
</tbody></table></div>
|
|
163
164
|
${picked && Array.isArray(pickedSteps)
|
|
164
|
-
? html`<h3 class="u-mt125"
|
|
165
|
+
? html`<h3 class="u-mt125">${picked.active_steps.length ? tx('wfv.instance_at', { id: picked.id, version: picked.version ?? '1', steps: picked.active_steps.join(', ') }) : tx('wfv.instance', { id: picked.id, version: picked.version ?? '1' })}</h3>
|
|
165
166
|
<div class="wf-wrap">${workflowDiagram(pickedSteps, { active: picked.active_steps, id: `wf${picked.id}` })}</div>`
|
|
166
|
-
: html`<p class="muted small"
|
|
167
|
+
: html`<p class="muted small">${tx('wfv.pick')}</p>`}
|
|
167
168
|
</fieldset>`
|
|
168
169
|
: ''}`;
|
|
169
170
|
}
|
|
@@ -182,20 +183,20 @@ export async function workflowBuilderRoutes(app: FastifyInstance) {
|
|
|
182
183
|
try {
|
|
183
184
|
if (b.action === 'new') {
|
|
184
185
|
const label = String(b.version ?? '').trim();
|
|
185
|
-
if (label && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,29}$/.test(label)) throw new Error('
|
|
186
|
+
if (label && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,29}$/.test(label)) throw new Error(tx('wfv.bad_label'));
|
|
186
187
|
const v = (await owner.one('select meta.new_workflow_version($1, $2) as v', [d.id, label || null])).v;
|
|
187
|
-
flash(s,
|
|
188
|
+
flash(s, tx('wfv.created', { version: v }));
|
|
188
189
|
} else if (b.action === 'activate') {
|
|
189
|
-
if (!d.dev_version) throw new Error('
|
|
190
|
+
if (!d.dev_version) throw new Error(tx('wfv.no_dev'));
|
|
190
191
|
const tasks = new Set((await owner.query('select name from meta.task_definition where app_id = $1', [id])).rows.map((r) => r.name as string));
|
|
191
192
|
const outcomes = outcomeMap((await owner.query('select name, type, outcomes from meta.task_definition where app_id = $1', [id])).rows);
|
|
192
193
|
const problems = stepProblems(d.dev_steps, tasks, { outcomes, drafts: false });
|
|
193
|
-
if (problems.length) throw new Error(
|
|
194
|
+
if (problems.length) throw new Error(tx('wfv.cant_activate', { version: d.dev_version, problems: problems.join(' ') }));
|
|
194
195
|
const v = (await owner.one('select meta.activate_workflow_version($1) as v', [d.id])).v;
|
|
195
|
-
flash(s,
|
|
196
|
+
flash(s, tx('wfv.activated_flash', { version: v }));
|
|
196
197
|
} else if (b.action === 'discard') {
|
|
197
198
|
await owner.query('select meta.discard_workflow_version($1)', [d.id]);
|
|
198
|
-
flash(s, d.dev_version ?
|
|
199
|
+
flash(s, d.dev_version ? tx('wfv.discarded', { version: d.dev_version }) : tx('wfv.was_no_dev'));
|
|
199
200
|
} else return reply.code(400).send('Unknown action');
|
|
200
201
|
} catch (e) {
|
|
201
202
|
flash(s, (e as Error).message, 'error');
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { lockedFor } from '../cli/working-copy.ts';
|
|
11
11
|
import { appHeader, back, BASE, csrf, developer, flash, input, region, send, shell, type Req } from './ui.ts';
|
|
12
12
|
import { appAllowed } from './workspaces.ts';
|
|
13
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
13
14
|
|
|
14
15
|
// Working copies (APEX 24.1+): App → Working copies lists an application's
|
|
15
16
|
// copies and makes new ones; a copy's page compares it with its main
|
|
@@ -22,12 +23,12 @@ import { appAllowed } from './workspaces.ts';
|
|
|
22
23
|
const appRow = async (id: string) => (/^\d{1,9}$/.test(id) ? owner.one('select id, name, alias from meta.app where id = $1', [id]) : undefined);
|
|
23
24
|
|
|
24
25
|
const SIDE: Record<string, string> = { added: 'added', deleted: 'deleted', changed: 'changed' };
|
|
25
|
-
const STATUS: Record<Change['status'], string>
|
|
26
|
+
const STATUS = (): Record<Change['status'], string> => ({ copy: tx('wcp.changed_in_the_copy'), main: tx('wcp.changed_in_the_main_application'), conflict: tx('wcp.conflict_changed_on_both_sides') });
|
|
26
27
|
|
|
27
28
|
function diffPanel(ch: Change) {
|
|
28
29
|
const paths = [...new Set([...(ch.main?.keys() ?? []), ...(ch.copy?.keys() ?? [])])].sort();
|
|
29
30
|
return html`<div id="diff">
|
|
30
|
-
<p class="muted u-mt0"
|
|
31
|
+
<p class="muted u-mt0">${tx.code('wcp.lines_with_are_the_main')}</p>
|
|
31
32
|
${paths.map((p) => {
|
|
32
33
|
const a = ch.main?.get(p), b = ch.copy?.get(p);
|
|
33
34
|
if (a && b && a.equals(b)) return '';
|
|
@@ -46,38 +47,33 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
46
47
|
let body;
|
|
47
48
|
if (wc) {
|
|
48
49
|
const main = await owner.one('select id, name, alias from meta.app where id = $1', [wc.main_app_id]);
|
|
49
|
-
body = region('
|
|
50
|
-
<p class="u-mt0"
|
|
51
|
-
|
|
52
|
-
on ${wc.created_at.slice(0, 16)}.${wc.refreshed_at ? ` Last refreshed ${wc.refreshed_at.slice(0, 16)}.` : ''}${wc.merged_at ? ` Last merged ${wc.merged_at.slice(0, 16)} by ${wc.merged_by}.` : ''}</p>
|
|
53
|
-
<p class="muted">Change it like any application: it runs against the main application's schema and data, with
|
|
54
|
-
its automations and synchronisations switched off. Then compare it with the main application and merge it back.</p>
|
|
50
|
+
body = region(tx('wcp.working_copy'), html`
|
|
51
|
+
<p class="u-mt0">${tx.rich('wcp.is_copy', { name: html`<b>${wc.name}</b>`, main: html`<a href="${BASE}/apps/${main.id}">${main.name}</a>`, path: html`<code>/a/${main.alias}</code>`, by: wc.created_by, at: wc.created_at.slice(0, 16) })}${wc.refreshed_at ? ` ${tx('wcp.last_refreshed', { at: wc.refreshed_at.slice(0, 16) })}` : ''}${wc.merged_at ? ` ${tx('wcp.last_merged_by', { at: wc.merged_at.slice(0, 16), by: wc.merged_by })}` : ''}</p>
|
|
52
|
+
<p class="muted">${tx('wcp.change_it_like_any_application')}</p>
|
|
55
53
|
<div class="buttons">
|
|
56
|
-
<a class="btn btn-hot" href="${BASE}/apps/${a.id}/compare">${icon('layers')}
|
|
54
|
+
<a class="btn btn-hot" href="${BASE}/apps/${a.id}/compare">${icon('layers')} ${tx('wcp.compare_and_merge')}</a>
|
|
57
55
|
<form method="post" action="${BASE}/apps/${a.id}/working-copy/delete" class="u-inline">${csrf(s)}
|
|
58
|
-
<button class="btn btn-danger" data-confirm="
|
|
56
|
+
<button class="btn btn-danger" data-confirm="${tx('wcp.delete_confirm', { name: wc.name })}">${tx('wcp.delete_working_copy')}</button></form>
|
|
59
57
|
</div>`);
|
|
60
58
|
} else {
|
|
61
59
|
const copies = await copiesOf(a.id);
|
|
62
|
-
body = html`${region('
|
|
63
|
-
<p class="muted u-mt0">
|
|
64
|
-
(APEX: working copies). Compare it with this application and merge it back component by component;
|
|
65
|
-
components changed on both sides are shown as conflicts to resolve.</p>
|
|
60
|
+
body = html`${region(tx('wcp.working_copies'), html`
|
|
61
|
+
<p class="muted u-mt0">${tx('wcp.a_working_copy_is_a')}</p>
|
|
66
62
|
${copies.length
|
|
67
|
-
? html`<div class="table-wrap"><table class="report"><thead><tr><th
|
|
63
|
+
? html`<div class="table-wrap"><table class="report"><thead><tr><th>${tx('wcp.name')}</th><th>${tx('wcp.application')}</th><th>${tx('wcp.made_by')}</th><th>${tx('wcp.made')}</th><th>${tx('wcp.last_merged')}</th><th>${tx('wcp.actions')}</th></tr></thead><tbody>
|
|
68
64
|
${copies.map((c) => html`<tr><td>${c.name}</td><td><a href="${BASE}/apps/${c.app_id}">${c.app_id}</a> <code>/a/${c.alias}</code></td>
|
|
69
65
|
<td>${c.created_by}</td><td>${c.created_at.slice(0, 16)}</td><td>${c.merged_at ? c.merged_at.slice(0, 16) : html`<span class="muted">never</span>`}</td>
|
|
70
|
-
<td><a class="btn btn-sm" href="${BASE}/apps/${c.app_id}/compare">${icon('layers')}
|
|
66
|
+
<td><a class="btn btn-sm" href="${BASE}/apps/${c.app_id}/compare">${icon('layers')} ${tx('wcp.compare_and_merge')}</a></td></tr>`)}
|
|
71
67
|
</tbody></table></div>`
|
|
72
|
-
: html`<p class="muted"
|
|
73
|
-
${region('
|
|
68
|
+
: html`<p class="muted">${tx('wcp.no_working_copies')}</p>`}`)}
|
|
69
|
+
${region(tx('wcp.create_a_working_copy'), html`<form method="post" action="${BASE}/working-copies">${csrf(s)}
|
|
74
70
|
<input type="hidden" name="main_app_id" value="${a.id}">
|
|
75
|
-
<div class="form-grid">${input('name', '
|
|
76
|
-
<div class="buttons"><button class="btn btn-hot">${icon('plus')}
|
|
71
|
+
<div class="form-grid">${input('name', tx('wcp.name'), '', { required: true, placeholder: tx('wcp.e_g_new_dashboard'), help: `1–40 letters, digits, spaces, - or _. The copy runs at /a/${a.alias}-<name>.` })}</div>
|
|
72
|
+
<div class="buttons"><button class="btn btn-hot">${icon('plus')} ${tx('wcp.create_working_copy')}</button></div>
|
|
77
73
|
</form>`)}`;
|
|
78
74
|
}
|
|
79
75
|
const main = html`${appHeader(a, 'pages', s)}<div class="ide-body">${body}</div>`;
|
|
80
|
-
return send(reply, s, shell(s, `${a.name} working copies`, [['
|
|
76
|
+
return send(reply, s, shell(s, `${a.name} working copies`, [[tx('wcp.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('wcp.working_copies')]], main));
|
|
81
77
|
});
|
|
82
78
|
|
|
83
79
|
// create a working copy (not under /apps/:id, so a locked main application can still be copied)
|
|
@@ -90,7 +86,7 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
90
86
|
try {
|
|
91
87
|
const id = await createCopy(a.id, name, s.username!);
|
|
92
88
|
await logActivity({ appId: a.id, username: s.username, event: 'working_copy', ip: clientIp(req), detail: `created ${name} (application ${id})` });
|
|
93
|
-
flash(s,
|
|
89
|
+
flash(s, tx('wcp.created', { name }));
|
|
94
90
|
return back(reply, s, `${BASE}/apps/${id}/working-copies`);
|
|
95
91
|
} catch (e) {
|
|
96
92
|
if (!(e instanceof WorkingCopyError)) throw e;
|
|
@@ -107,44 +103,40 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
107
103
|
if (!a) return reply.code(404).send('Not found');
|
|
108
104
|
const l = await compare(a.id);
|
|
109
105
|
if (!l) {
|
|
110
|
-
flash(s, '
|
|
106
|
+
flash(s, tx('wcp.this_application_is_not_a'), 'error');
|
|
111
107
|
return back(reply, s, `${BASE}/apps/${a.id}/working-copies`);
|
|
112
108
|
}
|
|
113
109
|
const main = await owner.one('select id, name, alias from meta.app where id = $1', [l.copy.main_app_id]);
|
|
114
110
|
const shown = typeof req.query.c === 'string' ? l.changes.find((ch) => ch.id === req.query.c) : undefined;
|
|
115
111
|
const conflicts = l.changes.filter((ch) => ch.status === 'conflict').length;
|
|
116
112
|
const count = (st: Change['status']) => l.changes.filter((ch) => ch.status === st).length;
|
|
117
|
-
const side = (v: Change['inMain']) => (v ? SIDE[v] : html`<span class="muted"
|
|
113
|
+
const side = (v: Change['inMain']) => (v ? tx.dyn(`wcp.side.${v}`, SIDE[v]) : html`<span class="muted">${tx('wcp.unchanged')}</span>`);
|
|
118
114
|
const body = l.changes.length
|
|
119
|
-
? html`<p class="u-mt0">${
|
|
115
|
+
? html`<p class="u-mt0">${tx(conflicts === 1 ? 'wcp.counts_1' : 'wcp.counts', { copy: count('copy'), main: count('main'), conflicts })}</p>
|
|
120
116
|
<form method="post" action="${BASE}/apps/${a.id}/merge">${csrf(s)}<input type="hidden" name="state" value="${fingerprint(l.changes)}">
|
|
121
|
-
<div class="table-wrap"><table class="report" aria-label="
|
|
122
|
-
<thead><tr><th
|
|
123
|
-
${l.changes.map((ch, i) => html`<tr${ch.status === 'conflict' ? raw(' class="row-conflict"') : ''}><td>${ch.label}<br><small class="muted">${STATUS[ch.status]}</small></td>
|
|
117
|
+
<div class="table-wrap"><table class="report" aria-label="${tx('wcp.changed_components')}">
|
|
118
|
+
<thead><tr><th>${tx('wcp.component')}</th><th>${tx('wcp.main_application')}</th><th>${tx('wcp.working_copy')}</th><th>${tx('wcp.result')}</th><th>${tx('wcp.differences')}</th></tr></thead><tbody>
|
|
119
|
+
${l.changes.map((ch, i) => html`<tr${ch.status === 'conflict' ? raw(' class="row-conflict"') : ''}><td>${ch.label}<br><small class="muted">${STATUS()[ch.status]}</small></td>
|
|
124
120
|
<td>${side(ch.inMain)}</td><td>${side(ch.inCopy)}</td>
|
|
125
121
|
<td>${ch.status === 'conflict'
|
|
126
|
-
? html`<fieldset class="u-mt0"><legend class="sr-only"
|
|
127
|
-
<label class="check"><input type="radio" name="r_${i}" value="main" required>
|
|
128
|
-
<label class="check"><input type="radio" name="r_${i}" value="copy" required>
|
|
129
|
-
: ch.status === 'copy' ? '
|
|
130
|
-
<td><a class="btn btn-sm" href="${BASE}/apps/${a.id}/compare?c=${encodeURIComponent(ch.id)}#diff"
|
|
122
|
+
? html`<fieldset class="u-mt0"><legend class="sr-only">${tx('wcp.keep_for', { label: ch.label })}</legend>
|
|
123
|
+
<label class="check"><input type="radio" name="r_${i}" value="main" required> ${tx('wcp.main')}</label>
|
|
124
|
+
<label class="check"><input type="radio" name="r_${i}" value="copy" required> ${tx('wcp.copy')}</label></fieldset>`
|
|
125
|
+
: ch.status === 'copy' ? tx('wcp.copy') : tx('wcp.main')}</td>
|
|
126
|
+
<td><a class="btn btn-sm" href="${BASE}/apps/${a.id}/compare?c=${encodeURIComponent(ch.id)}#diff">${tx('wcp.show')}</a></td></tr>`)}
|
|
131
127
|
</tbody></table></div>
|
|
132
|
-
<p class="muted">
|
|
133
|
-
the working copy only (bringing the main application's changes in). Users, sessions, saved reports, secrets and
|
|
134
|
-
the main application's automation switches stay as they are.</p>
|
|
128
|
+
<p class="muted">${tx('wcp.merging_writes_the_result_into')}</p>
|
|
135
129
|
<div class="buttons">
|
|
136
|
-
<button class="btn btn-hot" name="direction" value="merge" data-confirm="
|
|
137
|
-
<button class="btn" name="direction" value="refresh"
|
|
130
|
+
<button class="btn btn-hot" name="direction" value="merge" data-confirm="${tx('wcp.merge_confirm', { main: main.name })}">${tx('wcp.merge_into', { main: main.name })}</button>
|
|
131
|
+
<button class="btn" name="direction" value="refresh">${tx('wcp.refresh_from', { main: main.name })}</button>
|
|
138
132
|
</div>
|
|
139
133
|
</form>`
|
|
140
|
-
: html`<p class="u-mt0"
|
|
134
|
+
: html`<p class="u-mt0">${tx('wcp.the_working_copy_and_the')}</p>`;
|
|
141
135
|
const page = html`${appHeader(a, 'pages', s)}<div class="ide-body">
|
|
142
|
-
${region(
|
|
143
|
-
|
|
144
|
-
when the copy was made${l.copy.refreshed_at || l.copy.merged_at ? ', refreshed or merged' : ''}.</p>${body}`)}
|
|
145
|
-
${shown ? region(`Differences: ${shown.label}`, diffPanel(shown)) : ''}
|
|
136
|
+
${region(tx('wcp.compare_title', { main: main.name }), html`<p class="muted u-mt0">${tx.rich(l.copy.refreshed_at || l.copy.merged_at ? 'wcp.compare_intro_since' : 'wcp.compare_intro', { copy: html`<b>${l.copy.name}</b>`, main: html`<a href="${BASE}/apps/${main.id}">${main.name}</a>` })}</p>${body}`)}
|
|
137
|
+
${shown ? region(tx('wcp.diff_title', { label: shown.label }), diffPanel(shown)) : ''}
|
|
146
138
|
</div>`;
|
|
147
|
-
return send(reply, s, shell(s, `${a.name} compare`, [['
|
|
139
|
+
return send(reply, s, shell(s, `${a.name} compare`, [[tx('wcp.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('wcp.working_copies'), `${BASE}/apps/${a.id}/working-copies`], [tx('wcp.compare')]], page));
|
|
148
140
|
});
|
|
149
141
|
|
|
150
142
|
// merge into the main application, or refresh the copy from it
|
|
@@ -158,14 +150,14 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
158
150
|
const target = `${BASE}/apps/${a.id}/compare`;
|
|
159
151
|
if (!direction) return reply.code(400).send('Bad request');
|
|
160
152
|
const l = await compare(a.id);
|
|
161
|
-
if (!l) return reply.code(404).send('
|
|
153
|
+
if (!l) return reply.code(404).send(tx('wcp.not_a_copy'));
|
|
162
154
|
// conflicts: every one needs a choice (the fingerprint checks they are the ones shown)
|
|
163
155
|
const takeCopy = new Set<string>();
|
|
164
156
|
for (const [i, ch] of l.changes.entries()) {
|
|
165
157
|
if (ch.status !== 'conflict') continue;
|
|
166
158
|
const v = b[`r_${i}`];
|
|
167
159
|
if (v !== 'main' && v !== 'copy') {
|
|
168
|
-
flash(s, '
|
|
160
|
+
flash(s, tx('wcp.choose_main_or_copy_for'), 'error');
|
|
169
161
|
return back(reply, s, target);
|
|
170
162
|
}
|
|
171
163
|
if (v === 'copy') takeCopy.add(ch.id);
|
|
@@ -173,7 +165,7 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
173
165
|
try {
|
|
174
166
|
const r = await mergeCopy(a.id, { direction, takeCopy, state: String(b.state ?? ''), username: s.username!, lockedBy: lockedFor(s.username!) });
|
|
175
167
|
await logActivity({ appId: direction === 'merge' ? r.mainId : a.id, username: s.username, event: 'working_copy', ip: clientIp(req), detail: `${direction === 'merge' ? 'merged' : 'refreshed'} ${l.copy.name} (${r.changes} components)` });
|
|
176
|
-
flash(s, direction === 'merge' ?
|
|
168
|
+
flash(s, direction === 'merge' ? tx('wcp.merged', { name: l.copy.name, n: r.changes }) : tx('wcp.refreshed', { name: l.copy.name }));
|
|
177
169
|
return back(reply, s, direction === 'merge' ? `${BASE}/apps/${r.mainId}` : target);
|
|
178
170
|
} catch (e) {
|
|
179
171
|
if (!(e instanceof WorkingCopyError)) throw e;
|
|
@@ -189,11 +181,11 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
189
181
|
if (!a) return reply.code(404).send('Not found');
|
|
190
182
|
const wc = await copyOf(a.id);
|
|
191
183
|
if (!wc || !(await deleteCopy(a.id))) {
|
|
192
|
-
flash(s, '
|
|
184
|
+
flash(s, tx('wcp.this_application_is_not_a'), 'error');
|
|
193
185
|
return back(reply, s, `${BASE}/apps/${a.id}/working-copies`);
|
|
194
186
|
}
|
|
195
187
|
await logActivity({ appId: wc.main_app_id, username: s.username, event: 'working_copy', ip: clientIp(req), detail: `deleted ${wc.name}` });
|
|
196
|
-
flash(s,
|
|
188
|
+
flash(s, tx('wcp.deleted', { name: wc.name }));
|
|
197
189
|
return back(reply, s, `${BASE}/apps/${wc.main_app_id}/working-copies`);
|
|
198
190
|
});
|
|
199
191
|
}
|
|
@@ -5,6 +5,7 @@ import { icon } from '../icons.ts';
|
|
|
5
5
|
import { clientIp, logActivity } from '../session.ts';
|
|
6
6
|
import { back, BASE, csrf, developer, flash, input, region, send, shell, type Req } from './ui.ts';
|
|
7
7
|
import { currentWorkspace } from './workspaces.ts';
|
|
8
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
8
9
|
|
|
9
10
|
// Workspace utilities → Manage backups (APEX: Workspace Utilities → Manage
|
|
10
11
|
// Backups): the backups (082) of every application of the current workspace on
|
|
@@ -26,28 +27,28 @@ export async function workspaceBackupRoutes(app: FastifyInstance) {
|
|
|
26
27
|
where meta.app_workspace(a.id) = $1
|
|
27
28
|
group by a.id order by lower(a.name), a.id`, [currentWorkspace(s)])).rows;
|
|
28
29
|
const total = rows.reduce((t, r) => t + Number(r.bytes), 0);
|
|
29
|
-
const main = html`<div class="title-row"><h1
|
|
30
|
-
<p class="muted u-mt0"
|
|
30
|
+
const main = html`<div class="title-row"><h1>${tx('wu.backups')}</h1></div>
|
|
31
|
+
<p class="muted u-mt0">${s.workspace?.name ? tx('wbk.intro', { name: s.workspace.name }) : tx('wbk.intro_this')}</p>
|
|
31
32
|
<div class="stat-grid u-mb1">
|
|
32
|
-
${[['
|
|
33
|
+
${[[tx('wbk.applications'), rows.length], [tx('wbk.backups'), rows.reduce((t, r) => t + r.backups, 0)], [tx('wbk.storage'), size(total)], [tx('wbk.changed_since_newest'), rows.filter((r) => r.changed && r.backups_keep > 0).length]]
|
|
33
34
|
.map(([l, v]) => html`<div class="stat"><b>${String(v)}</b><span>${l}</span></div>`)}
|
|
34
35
|
</div>
|
|
35
|
-
${region('
|
|
36
|
-
<thead><tr><th
|
|
36
|
+
${region(tx('wbk.applications'), html`<div class="table-wrap"><table class="report report-reflow">
|
|
37
|
+
<thead><tr><th>${tx('wbk.application')}</th><th class="num">${tx('wbk.backups')}</th><th class="num">${tx('wbk.kept')}</th><th>${tx('wbk.newest_utc')}</th><th class="num">${tx('wbk.size')}</th><th></th></tr></thead>
|
|
37
38
|
<tbody>${rows.length ? rows.map((a) => html`<tr>
|
|
38
|
-
<td data-label="
|
|
39
|
-
<td class="num" data-label="
|
|
40
|
-
<td class="num" data-label="
|
|
41
|
-
<td data-label="
|
|
42
|
-
<td class="num" data-label="
|
|
43
|
-
<td><a class="btn btn-sm" href="${BASE}/apps/${a.id}/backups">${icon('history')}
|
|
44
|
-
: html`<tr><td colspan="6" class="empty"
|
|
45
|
-
${rows.length ? region('
|
|
46
|
-
<p class="muted u-mt0"
|
|
47
|
-
<div class="form-grid">${input('note', '
|
|
48
|
-
<div class="field"><label class="check"><input type="checkbox" name="changed" value="true" checked>
|
|
49
|
-
<div class="buttons"><button class="btn btn-hot">${icon('save')}
|
|
50
|
-
return send(reply, s, shell(s, '
|
|
39
|
+
<td data-label="${tx('wbk.application')}"><a href="${BASE}/apps/${a.id}">${a.name}</a> <span class="muted">/a/${a.alias}</span></td>
|
|
40
|
+
<td class="num" data-label="${tx('wbk.backups')}">${a.backups}</td>
|
|
41
|
+
<td class="num" data-label="${tx('wbk.kept')}">${a.backups_keep === 0 ? html`<span class="tag">${tx('wbk.none')}</span>` : a.backups_keep}</td>
|
|
42
|
+
<td data-label="${tx('wbk.newest')}">${a.newest ? when(a.newest) : html`<span class="muted">${tx('wbk.never')}</span>`}${a.changed && a.backups_keep > 0 && a.newest ? html` <span class="tag">${tx('wbk.changed_since')}</span>` : ''}</td>
|
|
43
|
+
<td class="num" data-label="${tx('wbk.size')}">${size(Number(a.bytes))}</td>
|
|
44
|
+
<td><a class="btn btn-sm" href="${BASE}/apps/${a.id}/backups">${icon('history')} ${tx('wbk.backups')}</a></td></tr>`)
|
|
45
|
+
: html`<tr><td colspan="6" class="empty">${tx('wbk.no_apps')}</td></tr>`}</tbody></table></div>`)}
|
|
46
|
+
${rows.length ? region(tx('wbk.back_up_all'), html`<form method="post" action="${BASE}/backups">${csrf(s)}
|
|
47
|
+
<p class="muted u-mt0">${tx('wbk.back_up_all_text')}</p>
|
|
48
|
+
<div class="form-grid">${input('note', tx('wbk.note'), '', { placeholder: tx('wbk.note_placeholder') })}
|
|
49
|
+
<div class="field"><label class="check"><input type="checkbox" name="changed" value="true" checked> ${tx('wbk.only_changed')}</label></div></div>
|
|
50
|
+
<div class="buttons"><button class="btn btn-hot">${icon('save')} ${tx('wbk.back_up_now')}</button></div></form>`) : ''}`;
|
|
51
|
+
return send(reply, s, shell(s, tx('wu.backups'), [[tx('rail.app_builder'), BASE], [tx('wu.title'), `${BASE}/utilities`], [tx('wu.backups')]], main));
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
app.post(`${BASE}/backups`, async (req: Req, reply) => {
|
|
@@ -71,8 +72,8 @@ export async function workspaceBackupRoutes(app: FastifyInstance) {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
await logActivity({ appId: null, username: s.username, event: 'backup', ip: clientIp(req), detail: `workspace ${currentWorkspace(s)}: ${made} backup(s)` });
|
|
74
|
-
if (failed.length) flash(s,
|
|
75
|
-
else flash(s, made ?
|
|
75
|
+
if (failed.length) flash(s, tx(failed.length > 1 ? 'wbk.failed_many' : 'wbk.failed_one', { n: made, ids: failed.join(', ') }), 'error');
|
|
76
|
+
else flash(s, made ? tx(made > 1 ? 'wbk.made_many' : 'wbk.made_one', { n: made }) : tx('wbk.nothing'));
|
|
76
77
|
return back(reply, s, `${BASE}/backups`);
|
|
77
78
|
});
|
|
78
79
|
}
|
|
@@ -8,6 +8,7 @@ import { clientIp, logActivity, type Session } from '../session.ts';
|
|
|
8
8
|
import { formValues, parseForm, PROVIDER_TEXT, runCheck, serviceForm } from './mail.ts';
|
|
9
9
|
import { back, BASE, csrf, developer, flash, region, select, send, shell, type Req } from './ui.ts';
|
|
10
10
|
import { workspaceAdmin } from './workspaces.ts';
|
|
11
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
11
12
|
|
|
12
13
|
// (sprint 118, WS-9; APEX 26.1 workspace-level SMTP credentials) A workspace's
|
|
13
14
|
// own mail service, managed by its administrators on /builder/workspaces/:id/mail:
|
|
@@ -23,8 +24,8 @@ type Service = MailService & { has_secret: boolean };
|
|
|
23
24
|
|
|
24
25
|
async function wsAdminOnly(s: Session, reply: FastifyReply, wsId: string) {
|
|
25
26
|
if (await workspaceAdmin(s.username, wsId)) return true;
|
|
26
|
-
const main = html`<h1 class="u-mb1"
|
|
27
|
-
await send(reply.code(403), s, shell(s, '
|
|
27
|
+
const main = html`<h1 class="u-mb1">${tx('wml.workspace_mail_service')}</h1><div class="alert alert-error" role="alert">${tx('wml.only_the_workspace_s_administrators')}</div>`;
|
|
28
|
+
await send(reply.code(403), s, shell(s, tx('wml.workspace_mail_service'), [[tx('wml.administration')], [tx('wml.workspace_mail_service')]], main, 'users'));
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -43,22 +44,22 @@ export async function workspaceMailRoutes(app: FastifyInstance) {
|
|
|
43
44
|
from meta.app a where meta.app_workspace(a.id) = $1 order by a.alias`, [w.id])).rows;
|
|
44
45
|
const shown = typed ? { ...x, description: typed.description, provider: typed.provider, settings: typed.settings, senders: typed.senders, enabled: typed.enabled } : x ?? {};
|
|
45
46
|
const main = html`
|
|
46
|
-
<div class="title-row"><h1
|
|
47
|
-
<p class="muted u-mt0"
|
|
47
|
+
<div class="title-row"><h1>${tx('wml.title', { workspace: w.name })}</h1></div>
|
|
48
|
+
<p class="muted u-mt0">${tx.rich('wml.intro', { fn: html`<code>meta.send_mail()</code>`, network: html`<a href="${BASE}/network">${tx('wml.network_access')}</a>` })}</p>
|
|
48
49
|
<div class="mail-form-page">
|
|
49
|
-
${region('
|
|
50
|
-
<form method="get" class="provider-switch">${select('provider', '
|
|
51
|
-
x && provider !== x.provider ? '
|
|
50
|
+
${region(tx('wml.settings'), html`
|
|
51
|
+
<form method="get" class="provider-switch">${select('provider', tx('wml.provider'), provider, PROVIDERS.map((p): [string, string] => [p, PROVIDER_LABELS[p]]),
|
|
52
|
+
x && provider !== x.provider ? tx('wml.not_saved_yet_fill_in') : PROVIDER_TEXT(provider)[1])}<button class="btn btn-sm">${tx('wml.change')}</button></form>
|
|
52
53
|
${serviceForm(shown, provider, `${BASE}/workspaces/${w.id}/mail`, csrf(s), !x, tried, { id: w.id })}
|
|
53
54
|
${x ? html`<form method="post" action="${BASE}/workspaces/${w.id}/mail/delete" class="danger-zone">${csrf(s)}
|
|
54
|
-
<button class="btn btn-danger" data-confirm="
|
|
55
|
+
<button class="btn btn-danger" data-confirm="${tx('wml.delete_this_workspace_s_mail')}">${tx('wml.delete_service')}</button></form>` : ''}`)}
|
|
55
56
|
</div>
|
|
56
|
-
${region('
|
|
57
|
-
<thead><tr><th
|
|
58
|
-
<tbody>${users.length ? users.map((a) => html`<tr><td data-label="
|
|
59
|
-
<td data-label="
|
|
60
|
-
: html`<tr><td colspan="2" class="empty"
|
|
61
|
-
return send(reply, s, shell(s,
|
|
57
|
+
${region(tx('wml.applications'), html`<div class="table-wrap"><table class="report report-reflow">
|
|
58
|
+
<thead><tr><th>${tx('wml.application')}</th><th>${tx('wml.sends_through')}</th></tr></thead>
|
|
59
|
+
<tbody>${users.length ? users.map((a) => html`<tr><td data-label="${tx('wml.application')}">${a.alias}</td>
|
|
60
|
+
<td data-label="${tx('wml.sends_through')}">${a.own ? html`${a.own} <span class="muted">${tx('wml.chosen_by_an_administrator')}</span>` : x ? 'this service' : html`<span class="muted">${tx('wml.nothing_yet')}</span>`}</td></tr>`)
|
|
61
|
+
: html`<tr><td colspan="2" class="empty">${tx('wml.no_applications')}</td></tr>`}</tbody></table></div>`)}`;
|
|
62
|
+
return send(reply, s, shell(s, tx('wml.title', { workspace: w.name }), [[tx('wml.administration')], [w.name, `${BASE}/workspaces/${w.id}`], [tx('wml.mail_service')]], main, 'users'));
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
app.get(`${BASE}/workspaces/:id(^\\d{1,9}$)/mail`, async (req: Req, reply) => {
|
|
@@ -88,7 +89,7 @@ export async function workspaceMailRoutes(app: FastifyInstance) {
|
|
|
88
89
|
`insert into meta.mail_service (name, description, provider, settings, senders, enabled, secret_enc, workspace_id) values ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
89
90
|
[`WORKSPACE_${w.id}`, v.description, v.provider, JSON.stringify(v.settings), v.senders, v.enabled, secret, w.id]);
|
|
90
91
|
await logActivity({ appId: null, username: s.username, event: 'mail_service', ip: clientIp(req), detail: `${old ? 'changed' : 'added'} the mail service of workspace ${w.id} (${v.provider})` });
|
|
91
|
-
flash(s, old ? '
|
|
92
|
+
flash(s, old ? tx('wml.mail_service_saved') : tx('wml.mail_service_added_send_a'));
|
|
92
93
|
} catch (e) {
|
|
93
94
|
flash(s, (e as Error).message.includes('mail_service_name_key') ? `A service named WORKSPACE_${w.id} exists already: an administrator renames it first.` : (e as Error).message, 'error');
|
|
94
95
|
}
|
|
@@ -115,7 +116,7 @@ export async function workspaceMailRoutes(app: FastifyInstance) {
|
|
|
115
116
|
if (!s || !(await wsAdminOnly(s, reply, req.params.id))) return;
|
|
116
117
|
const r = await owner.query('delete from meta.mail_service where workspace_id = $1', [req.params.id]);
|
|
117
118
|
await logActivity({ appId: null, username: s.username, event: 'mail_service', ip: clientIp(req), detail: `deleted the mail service of workspace ${req.params.id}` });
|
|
118
|
-
flash(s, r.rowCount ? '
|
|
119
|
+
flash(s, r.rowCount ? tx('wml.mail_service_deleted') : tx('wml.there_was_no_mail_service'));
|
|
119
120
|
return back(reply, s, `${BASE}/workspaces/${req.params.id}/mail`);
|
|
120
121
|
});
|
|
121
122
|
}
|