shutro-erp 0.2.1 → 0.3.1
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/README.md +74 -67
- package/dist/account_portal/.kosh/bindings.d.ts +22 -0
- package/dist/account_portal/.kosh/bindings.js +15 -0
- package/dist/account_portal/.kosh/schema.d.ts +1380 -0
- package/dist/account_portal/index.d.ts +3 -0
- package/dist/account_portal/index.js +73 -0
- package/dist/account_portal/module.d.ts +5983 -0
- package/dist/account_portal/module.js +18 -0
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +172 -9
- package/dist/accounting/chart.d.ts +40 -8
- package/dist/accounting/chart.js +124 -30
- package/dist/accounting/closing.d.ts +6 -1
- package/dist/accounting/closing.js +5 -1
- package/dist/accounting/demo.js +6 -5
- package/dist/accounting/index.d.ts +3 -3
- package/dist/accounting/index.js +200 -23
- package/dist/accounting/module.d.ts +3326 -1086
- package/dist/accounting/module.js +408 -66
- package/dist/accounting/payments.d.ts +25 -0
- package/dist/accounting/payments.js +96 -4
- package/dist/accounting/web/RegisterPayment.d.ts +3 -2
- package/dist/accounting/web/RegisterPayment.js +39 -6
- package/dist/accounting/web/SettingsPage.js +5 -1
- package/dist/accounting/web/messages.js +10 -0
- package/dist/assistant/routes.js +1 -1
- package/dist/automation/engine.js +1 -1
- package/dist/automation/web/messages.js +2 -2
- package/dist/cli/apps.d.ts +1 -1
- package/dist/cli/apps.js +14 -14
- package/dist/cli/bin.js +3 -0
- package/dist/cli/index.js +11 -11
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +224 -78
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/project.d.ts +7 -7
- package/dist/cli/project.js +25 -25
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +48 -2
- package/dist/cli/templates.d.ts +14 -6
- package/dist/cli/templates.js +61 -34
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +21 -8
- package/dist/cli/ui.js +65 -19
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +33 -1
- package/dist/contacts/index.js +24 -1
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +3 -1
- package/dist/import/convert.d.ts +55 -0
- package/dist/import/convert.js +224 -0
- package/dist/import/csv.d.ts +28 -0
- package/dist/import/csv.js +115 -0
- package/dist/import/index.d.ts +33 -0
- package/dist/import/index.js +60 -0
- package/dist/import/mapping.d.ts +15 -0
- package/dist/import/mapping.js +33 -0
- package/dist/import/match.d.ts +24 -0
- package/dist/import/match.js +60 -0
- package/dist/import/models.d.ts +2 -0
- package/dist/import/models.js +102 -0
- package/dist/import/routes.d.ts +13 -0
- package/dist/import/routes.js +392 -0
- package/dist/import/run.d.ts +131 -0
- package/dist/import/run.js +568 -0
- package/dist/import/service.d.ts +153 -0
- package/dist/import/service.js +367 -0
- package/dist/import/web/ImportButton.d.ts +7 -0
- package/dist/import/web/ImportButton.js +31 -0
- package/dist/import/web/ImportWizard.d.ts +75 -0
- package/dist/import/web/ImportWizard.js +217 -0
- package/dist/import/web/index.d.ts +13 -0
- package/dist/import/web/index.js +18 -0
- package/dist/import/web/messages.d.ts +2 -0
- package/dist/import/web/messages.js +139 -0
- package/dist/import/worker.d.ts +3 -0
- package/dist/import/worker.js +52 -0
- package/dist/import/xlsx.d.ts +20 -0
- package/dist/import/xlsx.js +192 -0
- package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_bd/.kosh/bindings.js +15 -0
- package/dist/l10n_bd/.kosh/schema.d.ts +1311 -0
- package/dist/l10n_bd/documents.d.ts +8 -0
- package/dist/l10n_bd/documents.js +183 -0
- package/dist/l10n_bd/index.d.ts +8 -0
- package/dist/l10n_bd/index.js +156 -0
- package/dist/l10n_bd/module.d.ts +5705 -0
- package/dist/l10n_bd/module.js +163 -0
- package/dist/l10n_bd/mushak.d.ts +67 -0
- package/dist/l10n_bd/mushak.js +139 -0
- package/dist/l10n_bd/routes.d.ts +3 -0
- package/dist/l10n_bd/routes.js +205 -0
- package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
- package/dist/l10n_bd/web/MushakPage.js +53 -0
- package/dist/l10n_bd/web/index.d.ts +13 -0
- package/dist/l10n_bd/web/index.js +18 -0
- package/dist/l10n_bd/web/messages.d.ts +2 -0
- package/dist/l10n_bd/web/messages.js +101 -0
- package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_in/.kosh/bindings.js +15 -0
- package/dist/l10n_in/.kosh/schema.d.ts +1373 -0
- package/dist/l10n_in/documents.d.ts +8 -0
- package/dist/l10n_in/documents.js +114 -0
- package/dist/l10n_in/einvoice.d.ts +253 -0
- package/dist/l10n_in/einvoice.js +485 -0
- package/dist/l10n_in/gstr.d.ts +109 -0
- package/dist/l10n_in/gstr.js +430 -0
- package/dist/l10n_in/index.d.ts +20 -0
- package/dist/l10n_in/index.js +232 -0
- package/dist/l10n_in/module.d.ts +7823 -0
- package/dist/l10n_in/module.js +427 -0
- package/dist/l10n_in/routes-einvoice.d.ts +15 -0
- package/dist/l10n_in/routes-einvoice.js +124 -0
- package/dist/l10n_in/routes.d.ts +17 -0
- package/dist/l10n_in/routes.js +282 -0
- package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
- package/dist/l10n_in/web/EInvoicePanel.js +63 -0
- package/dist/l10n_in/web/GstPage.d.ts +7 -0
- package/dist/l10n_in/web/GstPage.js +57 -0
- package/dist/l10n_in/web/GstSettings.d.ts +10 -0
- package/dist/l10n_in/web/GstSettings.js +36 -0
- package/dist/l10n_in/web/index.d.ts +15 -0
- package/dist/l10n_in/web/index.js +22 -0
- package/dist/l10n_in/web/messages.d.ts +2 -0
- package/dist/l10n_in/web/messages.js +166 -0
- package/dist/mail/index.js +2 -2
- package/dist/modules-sdk/demo.d.ts +2 -0
- package/dist/modules-sdk/demo.js +7 -3
- package/dist/modules-sdk/index.d.ts +2 -2
- package/dist/modules-sdk/index.js +1 -1
- package/dist/modules-sdk/loader.js +2 -2
- package/dist/modules-sdk/manifest.d.ts +132 -0
- package/dist/modules-sdk/manifest.js +22 -0
- package/dist/notify/inbox.js +21 -0
- package/dist/notify/messages.js +7 -3
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +5 -1
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +376 -0
- package/dist/portal/announce.d.ts +16 -0
- package/dist/portal/announce.js +117 -0
- package/dist/portal/documents.d.ts +17 -0
- package/dist/portal/documents.js +61 -0
- package/dist/portal/index.d.ts +20 -0
- package/dist/portal/index.js +65 -0
- package/dist/portal/module.d.ts +1405 -0
- package/dist/portal/module.js +84 -0
- package/dist/portal/routes-documents.d.ts +2 -0
- package/dist/portal/routes-documents.js +391 -0
- package/dist/portal/routes.d.ts +13 -0
- package/dist/portal/routes.js +313 -0
- package/dist/portal/rules.d.ts +11 -0
- package/dist/portal/rules.js +25 -0
- package/dist/portal/web/PortalAccess.d.ts +8 -0
- package/dist/portal/web/PortalAccess.js +66 -0
- package/dist/portal/web/index.d.ts +12 -0
- package/dist/portal/web/index.js +17 -0
- package/dist/portal/web/messages.d.ts +2 -0
- package/dist/portal/web/messages.js +33 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +42 -3
- package/dist/product/demo.js +6 -5
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +17 -4
- package/dist/product/module.d.ts +346 -14
- package/dist/product/module.js +48 -18
- package/dist/product/taxes.d.ts +59 -0
- package/dist/product/taxes.js +61 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +33 -1
- package/dist/project/module.d.ts +20 -0
- package/dist/project_portal/.kosh/bindings.d.ts +22 -0
- package/dist/project_portal/.kosh/bindings.js +15 -0
- package/dist/project_portal/.kosh/schema.d.ts +701 -0
- package/dist/project_portal/index.d.ts +9 -0
- package/dist/project_portal/index.js +42 -0
- package/dist/project_portal/module.d.ts +2142 -0
- package/dist/project_portal/module.js +22 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +45 -4
- package/dist/purchase/demo.js +3 -3
- package/dist/purchase/index.js +1 -1
- package/dist/purchase/module.d.ts +232 -8
- package/dist/purchase/module.js +51 -9
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +174 -10
- package/dist/purchase_account/module.d.ts +2008 -641
- package/dist/purchase_account/module.js +2 -2
- package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_portal/.kosh/bindings.js +15 -0
- package/dist/purchase_portal/.kosh/schema.d.ts +852 -0
- package/dist/purchase_portal/index.d.ts +9 -0
- package/dist/purchase_portal/index.js +53 -0
- package/dist/purchase_portal/module.d.ts +2758 -0
- package/dist/purchase_portal/module.js +18 -0
- package/dist/reporting/documents.js +20 -3
- package/dist/reporting/index.d.ts +1 -0
- package/dist/reporting/index.js +1 -0
- package/dist/reporting/routes-documents.js +9 -2
- package/dist/reporting/words.d.ts +11 -0
- package/dist/reporting/words.js +229 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +174 -10
- package/dist/sale_account/module.d.ts +2008 -641
- package/dist/sale_account/module.js +3 -3
- package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
- package/dist/sale_portal/.kosh/bindings.js +15 -0
- package/dist/sale_portal/.kosh/schema.d.ts +766 -0
- package/dist/sale_portal/index.d.ts +9 -0
- package/dist/sale_portal/index.js +71 -0
- package/dist/sale_portal/module.d.ts +2718 -0
- package/dist/sale_portal/module.js +19 -0
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +45 -4
- package/dist/sale_project/module.d.ts +390 -10
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +45 -4
- package/dist/sales/demo.js +3 -3
- package/dist/sales/index.js +67 -1
- package/dist/sales/module.d.ts +232 -8
- package/dist/sales/module.js +51 -12
- package/dist/server/api/client.js +1 -1
- package/dist/server/api/openapi.js +17 -1
- package/dist/server/apps.js +5 -5
- package/dist/server/auth/api-keys.d.ts +13 -0
- package/dist/server/auth/api-keys.js +65 -0
- package/dist/server/auth/base.js +26 -0
- package/dist/server/auth/crypto.js +1 -1
- package/dist/server/auth/scopes.d.ts +12 -0
- package/dist/server/auth/scopes.js +51 -0
- package/dist/server/auth/service.d.ts +59 -1
- package/dist/server/auth/service.js +236 -6
- package/dist/server/demo.d.ts +1 -1
- package/dist/server/demo.js +1 -1
- package/dist/server/env-names.d.ts +2 -0
- package/dist/server/env-names.js +16 -0
- package/dist/server/host.js +5 -1
- package/dist/server/identity.d.ts +7 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/registry.js +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.js +71 -6
- package/dist/settings/routes.js +1 -1
- package/dist/settings/web/ApiKeysPage.d.ts +8 -0
- package/dist/settings/web/ApiKeysPage.js +70 -0
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +4 -0
- package/dist/settings/web/messages.js +62 -0
- package/dist/storage/routes.js +1 -1
- package/dist/studio/build.js +1 -1
- package/dist/studio/generate.js +4 -4
- package/dist/studio/module.js +5 -5
- package/dist/studio/web/messages.js +2 -2
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +27 -4
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/i18n/messages.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/lib/format.d.ts +2 -0
- package/dist/ui/lib/format.js +3 -1
- package/dist/ui/pwa/worker.js +2 -2
- package/dist/ui/registry.d.ts +15 -0
- package/dist/ui/registry.js +20 -0
- package/dist/ui/views/FormView.js +19 -6
- package/package.json +52 -4
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Settings › API keys (RFC 0017 §5.1): your keys — and the service accounts' for administrators —, a new key with its
|
|
4
|
+
* scopes and expiry, its secret shown once, revocation. The server decides what each key may do; scopes only narrow
|
|
5
|
+
* the user's own rights.
|
|
6
|
+
*/
|
|
7
|
+
import { Alert, ApiError, Button, Input, Label, Select, useKosh } from '../../ui/index.js';
|
|
8
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { useId, useState } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
export function ApiKeysPage({ api }) {
|
|
12
|
+
const { t } = useTranslation('settings');
|
|
13
|
+
const { format } = useKosh();
|
|
14
|
+
const id = useId();
|
|
15
|
+
const queryClient = useQueryClient();
|
|
16
|
+
const keys = useQuery({
|
|
17
|
+
queryKey: ['settings', 'api-keys'],
|
|
18
|
+
queryFn: () => api.call('GET', '/api/v1/auth/api-keys'),
|
|
19
|
+
});
|
|
20
|
+
const scopes = useQuery({
|
|
21
|
+
queryKey: ['settings', 'scopes'],
|
|
22
|
+
queryFn: () => api.call('GET', '/api/v1/auth/scopes'),
|
|
23
|
+
});
|
|
24
|
+
const apps = [
|
|
25
|
+
...new Set((scopes.data?.scopes ?? [])
|
|
26
|
+
.map((s) => s.scope)
|
|
27
|
+
.filter((s) => s.startsWith('read:'))
|
|
28
|
+
.map((s) => s.slice(5))),
|
|
29
|
+
];
|
|
30
|
+
const [name, setName] = useState('');
|
|
31
|
+
const [days, setDays] = useState('365');
|
|
32
|
+
const [everything, setEverything] = useState(false);
|
|
33
|
+
const [levels, setLevels] = useState({});
|
|
34
|
+
const [secret, setSecret] = useState(null);
|
|
35
|
+
const chosen = everything
|
|
36
|
+
? ['*']
|
|
37
|
+
: Object.entries(levels).flatMap(([app, level]) => level === 'none' ? [] : [`${level}:${app}`]);
|
|
38
|
+
const refresh = () => queryClient.invalidateQueries({ queryKey: ['settings', 'api-keys'] });
|
|
39
|
+
const create = useMutation({
|
|
40
|
+
mutationFn: () => api.call('POST', '/api/v1/auth/api-keys', {
|
|
41
|
+
name,
|
|
42
|
+
scopes: chosen,
|
|
43
|
+
expiresInDays: Number(days),
|
|
44
|
+
}),
|
|
45
|
+
onSuccess: async (key) => {
|
|
46
|
+
setSecret(key.token);
|
|
47
|
+
setName('');
|
|
48
|
+
setLevels({});
|
|
49
|
+
setEverything(false);
|
|
50
|
+
await refresh();
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const revoke = useMutation({
|
|
54
|
+
mutationFn: (key) => api.call('POST', `/api/v1/auth/api-keys/${key}/revoke`),
|
|
55
|
+
onSuccess: refresh,
|
|
56
|
+
});
|
|
57
|
+
const error = create.error ?? revoke.error;
|
|
58
|
+
const message = (e) => (e instanceof ApiError ? e.message : String(e));
|
|
59
|
+
return (_jsxs("div", { className: "mx-auto grid max-w-4xl gap-6 p-4", children: [_jsx("h1", { className: "text-xl font-semibold", children: t('apiKeys.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('apiKeys.intro') }), error === null ? null : _jsx(Alert, { tone: "danger", children: message(error) }), secret === null ? null : (_jsx(Alert, { tone: "info", children: _jsxs("div", { className: "grid gap-2", children: [_jsx("p", { children: t('apiKeys.secretOnce') }), _jsx("code", { className: "break-all rounded bg-muted px-2 py-1 font-mono text-sm", children: secret }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "secondary", onClick: () => void navigator.clipboard?.writeText(secret), children: t('apiKeys.copy') }), _jsx(Button, { variant: "secondary", onClick: () => setSecret(null), children: t('apiKeys.done') })] })] }) })), _jsxs("section", { className: "grid gap-3 rounded-lg border p-4", "aria-labelledby": `${id}-new`, children: [_jsx("h2", { id: `${id}-new`, className: "font-medium", children: t('apiKeys.new') }), _jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-name`, children: t('apiKeys.name') }), _jsx(Input, { id: `${id}-name`, className: "w-64", value: name, onChange: (e) => setName(e.target.value) })] }), _jsx(Select, { id: `${id}-days`, label: t('apiKeys.expires'), className: "w-40", value: days, options: ['30', '90', '365', '730'].map((d) => ({
|
|
60
|
+
value: d,
|
|
61
|
+
label: t('apiKeys.days', { count: Number(d) }),
|
|
62
|
+
})), onChange: setDays })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: everything, onChange: (e) => setEverything(e.target.checked) }), t('apiKeys.everything')] }), everything ? null : (_jsxs("fieldset", { className: "grid gap-2 sm:grid-cols-2", children: [_jsx("legend", { className: "mb-1 text-sm text-muted-foreground", children: t('apiKeys.perApp') }), apps.map((app) => (_jsx(Select, { id: `${id}-app-${app}`, label: app, value: levels[app] ?? 'none', options: ['none', 'read', 'write'].map((l) => ({
|
|
63
|
+
value: l,
|
|
64
|
+
label: t(`apiKeys.levels.${l}`),
|
|
65
|
+
})), onChange: (v) => setLevels({ ...levels, [app]: (v ?? 'none') }) }, app)))] })), _jsx("div", { children: _jsx(Button, { variant: "primary", disabled: name.trim() === '' || chosen.length === 0, busy: create.isPending, onClick: () => create.mutate(), children: t('apiKeys.create') }) })] }), (keys.data?.keys ?? []).length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('apiKeys.none') })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted", children: _jsxs("tr", { children: [['name', 'user', 'key', 'scopes', 'expires', 'lastUsed', 'status'].map((c) => (_jsx("th", { className: "px-2 py-1 text-start", children: t(`apiKeys.columns.${c}`) }, c))), _jsx("th", { className: "px-2 py-1", children: _jsx("span", { className: "sr-only", children: t('apiKeys.revoke') }) })] }) }), _jsx("tbody", { children: (keys.data?.keys ?? []).map((k) => (_jsxs("tr", { className: "border-b", children: [_jsx("td", { className: "px-2 py-1", children: k.name }), _jsx("td", { className: "px-2 py-1", children: k.userName }), _jsxs("td", { className: "px-2 py-1 font-mono text-xs", children: [k.prefix, "\u2026"] }), _jsx("td", { className: "px-2 py-1", children: k.scopes.join(' ') }), _jsx("td", { className: "px-2 py-1", children: format.datetime(k.expiresAt) }), _jsx("td", { className: "px-2 py-1", children: k.lastUsedAt === null ? '—' : format.datetime(k.lastUsedAt) }), _jsx("td", { className: "px-2 py-1", children: k.revoked
|
|
66
|
+
? t('apiKeys.revoked')
|
|
67
|
+
: new Date(k.expiresAt).getTime() < Date.now()
|
|
68
|
+
? t('apiKeys.expired')
|
|
69
|
+
: t('apiKeys.active') }), _jsx("td", { className: "px-2 py-1 text-end", children: k.revoked ? null : (_jsx(Button, { variant: "secondary", "aria-label": t('apiKeys.revokeKey', { name: k.name }), busy: revoke.isPending && revoke.variables === k.id, onClick: () => revoke.mutate(k.id), children: t('apiKeys.revoke') })) })] }, k.id))) })] }) }))] }));
|
|
70
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { type Registry } from '../../ui/index.js';
|
|
7
7
|
import type { i18n } from 'i18next';
|
|
8
8
|
import { messages } from './messages.js';
|
|
9
|
+
export { ApiKeysPage } from './ApiKeysPage.js';
|
|
9
10
|
export { AppsPage } from './AppsPage.js';
|
|
10
11
|
/** Registers the Apps page (and its messages in `i18n`, when given). */
|
|
11
12
|
export declare function registerSettingsWeb(registry: Registry, i18n?: i18n): Registry;
|
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
* client action `settings.action_apps`) and its messages.
|
|
5
5
|
*/
|
|
6
6
|
import { addMessages } from '../../ui/index.js';
|
|
7
|
+
import { ApiKeysPage } from './ApiKeysPage.js';
|
|
7
8
|
import { AppsPage } from './AppsPage.js';
|
|
8
9
|
import { messages } from './messages.js';
|
|
10
|
+
export { ApiKeysPage } from './ApiKeysPage.js';
|
|
9
11
|
export { AppsPage } from './AppsPage.js';
|
|
10
12
|
/** Registers the Apps page (and its messages in `i18n`, when given). */
|
|
11
13
|
export function registerSettingsWeb(registry, i18n) {
|
|
12
14
|
registry.registerPage('settings.Apps', AppsPage);
|
|
15
|
+
// Every signed-in user's own keys (RFC 0017 §5.1): in the user menu, not in the Settings app.
|
|
16
|
+
registry.registerUserPage('settings.ApiKeys', ApiKeysPage, { label: 'settings:apiKeys.title' });
|
|
13
17
|
if (i18n !== undefined)
|
|
14
18
|
addMessages(i18n, 'settings', messages);
|
|
15
19
|
return registry;
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
/** Messages of the Settings app (namespace `settings`). */
|
|
2
2
|
export const messages = {
|
|
3
3
|
en: {
|
|
4
|
+
apiKeys: {
|
|
5
|
+
title: 'API keys',
|
|
6
|
+
intro: 'Keys let scripts and other systems call the API as you, within the scopes you give them. Your own rights still apply; a key can never do more than you.',
|
|
7
|
+
new: 'New key',
|
|
8
|
+
name: 'Name',
|
|
9
|
+
expires: 'Expires in',
|
|
10
|
+
days_one: '{{count}} day',
|
|
11
|
+
days_other: '{{count}} days',
|
|
12
|
+
everything: 'Everything I may do (*)',
|
|
13
|
+
perApp: 'Or per app:',
|
|
14
|
+
levels: { none: 'No access', read: 'Read', write: 'Read and change' },
|
|
15
|
+
create: 'Create key',
|
|
16
|
+
secretOnce: 'Copy the key now: it is shown only once.',
|
|
17
|
+
copy: 'Copy',
|
|
18
|
+
done: 'Done',
|
|
19
|
+
none: 'No API keys yet.',
|
|
20
|
+
columns: {
|
|
21
|
+
name: 'Name',
|
|
22
|
+
user: 'User',
|
|
23
|
+
key: 'Key',
|
|
24
|
+
scopes: 'Scopes',
|
|
25
|
+
expires: 'Expires',
|
|
26
|
+
lastUsed: 'Last used',
|
|
27
|
+
status: 'Status',
|
|
28
|
+
},
|
|
29
|
+
active: 'Active',
|
|
30
|
+
revoked: 'Revoked',
|
|
31
|
+
expired: 'Expired',
|
|
32
|
+
revoke: 'Revoke',
|
|
33
|
+
revokeKey: 'Revoke {{name}}',
|
|
34
|
+
},
|
|
4
35
|
apps: 'Apps',
|
|
5
36
|
appsIntro: 'Install the apps your business needs. Disabling keeps the data, so nothing is lost.',
|
|
6
37
|
search: 'Search apps',
|
|
@@ -56,6 +87,37 @@ export const messages = {
|
|
|
56
87
|
version: 'Version {{version}}',
|
|
57
88
|
},
|
|
58
89
|
bn: {
|
|
90
|
+
apiKeys: {
|
|
91
|
+
title: 'এপিআই কী',
|
|
92
|
+
intro: 'কী দিয়ে স্ক্রিপ্ট ও অন্য সিস্টেম আপনার হয়ে এপিআই ব্যবহার করে, আপনার দেওয়া পরিসরের মধ্যে। আপনার নিজের অধিকারই প্রযোজ্য; কোনো কী আপনার চেয়ে বেশি করতে পারে না।',
|
|
93
|
+
new: 'নতুন কী',
|
|
94
|
+
name: 'নাম',
|
|
95
|
+
expires: 'মেয়াদ',
|
|
96
|
+
days_one: '{{count}} দিন',
|
|
97
|
+
days_other: '{{count}} দিন',
|
|
98
|
+
everything: 'আমি যা করতে পারি সবকিছু (*)',
|
|
99
|
+
perApp: 'অথবা প্রতিটি অ্যাপের জন্য:',
|
|
100
|
+
levels: { none: 'প্রবেশাধিকার নেই', read: 'পড়া', write: 'পড়া ও পরিবর্তন' },
|
|
101
|
+
create: 'কী তৈরি করুন',
|
|
102
|
+
secretOnce: 'কী-টি এখনই কপি করুন: এটি একবারই দেখানো হয়।',
|
|
103
|
+
copy: 'কপি',
|
|
104
|
+
done: 'সম্পন্ন',
|
|
105
|
+
none: 'এখনো কোনো এপিআই কী নেই।',
|
|
106
|
+
columns: {
|
|
107
|
+
name: 'নাম',
|
|
108
|
+
user: 'ব্যবহারকারী',
|
|
109
|
+
key: 'কী',
|
|
110
|
+
scopes: 'পরিসর',
|
|
111
|
+
expires: 'মেয়াদ শেষ',
|
|
112
|
+
lastUsed: 'শেষ ব্যবহার',
|
|
113
|
+
status: 'অবস্থা',
|
|
114
|
+
},
|
|
115
|
+
active: 'সক্রিয়',
|
|
116
|
+
revoked: 'বাতিল',
|
|
117
|
+
expired: 'মেয়াদোত্তীর্ণ',
|
|
118
|
+
revoke: 'বাতিল করুন',
|
|
119
|
+
revokeKey: '{{name}} বাতিল করুন',
|
|
120
|
+
},
|
|
59
121
|
apps: 'অ্যাপস',
|
|
60
122
|
appsIntro: 'আপনার ব্যবসার প্রয়োজনীয় অ্যাপগুলো ইনস্টল করুন। নিষ্ক্রিয় করলে ডেটা থেকে যায়, কিছুই হারায় না।',
|
|
61
123
|
search: 'অ্যাপ খুঁজুন',
|
package/dist/storage/routes.js
CHANGED
|
@@ -25,7 +25,7 @@ export function storageOf(app) {
|
|
|
25
25
|
if (storage === undefined) {
|
|
26
26
|
const configured = storageFromEnv();
|
|
27
27
|
if (configured === null)
|
|
28
|
-
throw new HttpProblem(503, 'storage-unavailable', 'File storage is not configured', 'No object store is configured for attachments.', 'Set
|
|
28
|
+
throw new HttpProblem(503, 'storage-unavailable', 'File storage is not configured', 'No object store is configured for attachments.', 'Set SHUTRO_ERP_STORAGE_DIR, or S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY and S3_SECRET_KEY.');
|
|
29
29
|
storage = configured;
|
|
30
30
|
stores.set(app, storage);
|
|
31
31
|
}
|
package/dist/studio/build.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function studioModules(cwd) {
|
|
|
42
42
|
export async function runStudio(argv, write, cwd) {
|
|
43
43
|
const [action, ...folders] = argv;
|
|
44
44
|
if (action !== 'build') {
|
|
45
|
-
write('Usage:
|
|
45
|
+
write('Usage: shutro-erp studio build [folder …] (default: every src/modules/*/studio.json)\n');
|
|
46
46
|
return 2;
|
|
47
47
|
}
|
|
48
48
|
const dirs = folders.length > 0 ? folders.map((f) => resolve(cwd, f)) : await studioModules(cwd);
|
package/dist/studio/generate.js
CHANGED
|
@@ -14,7 +14,7 @@ export class StudioSpecError extends Error {
|
|
|
14
14
|
this.name = 'StudioSpecError';
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
const HEADER = '// Generated by
|
|
17
|
+
const HEADER = '// Generated by Shutro ERP Studio from studio.json: do not edit (edit studio.json, then `shutro-erp studio build`).';
|
|
18
18
|
/** A TypeScript literal with single quotes and stable key order (as given). */
|
|
19
19
|
export function lit(value, indent = '') {
|
|
20
20
|
if (value === null || value === undefined)
|
|
@@ -181,7 +181,7 @@ export function generateModule(spec, options = {}) {
|
|
|
181
181
|
HEADER,
|
|
182
182
|
'/**',
|
|
183
183
|
` * ${spec.label}${spec.description === undefined ? '' : `: ${spec.description}`}`,
|
|
184
|
-
' * A
|
|
184
|
+
' * A Shutro ERP Studio module (RFC 0012): its fields, models, view changes, menus, approvals and rules, from studio.json.',
|
|
185
185
|
' */',
|
|
186
186
|
`import { base } from '${im.base}';`,
|
|
187
187
|
...deps.map((d) => `import { ${camel(d)} } from '${im.module(d)}';`),
|
|
@@ -222,8 +222,8 @@ export function generateModule(spec, options = {}) {
|
|
|
222
222
|
const readme = [
|
|
223
223
|
`# ${spec.label} (\`${name}\` ${spec.version})`,
|
|
224
224
|
'',
|
|
225
|
-
'A
|
|
226
|
-
'(`
|
|
225
|
+
'A Shutro ERP Studio module (RFC 0012): `studio.json` is its source; `index.ts` and `models.ts` are generated from it',
|
|
226
|
+
'(`shutro-erp studio build`). To take it over as code, delete `studio.json` and edit the TypeScript.',
|
|
227
227
|
'',
|
|
228
228
|
...(spec.description === undefined ? [] : [spec.description, '']),
|
|
229
229
|
...((spec.fields ?? []).length === 0
|
package/dist/studio/module.js
CHANGED
|
@@ -368,7 +368,7 @@ export function registerStudioRoutes(context, options = {}) {
|
|
|
368
368
|
await guard(request);
|
|
369
369
|
const pullRequest = request.body?.pullRequest === true;
|
|
370
370
|
if (pullRequest && git === null)
|
|
371
|
-
throw new HttpProblem(409, 'studio-git-unconfigured', 'No repository for proposals', 'Set
|
|
371
|
+
throw new HttpProblem(409, 'studio-git-unconfigured', 'No repository for proposals', 'Set SHUTRO_ERP_STUDIO_GIT_URL and SHUTRO_ERP_STUDIO_GIT_TOKEN to open pull requests; download the patch meanwhile.');
|
|
372
372
|
const { review, files, modulesTs } = await prepare(specOf(request.body));
|
|
373
373
|
const patch = review.diff.map((d) => d.diff).join('');
|
|
374
374
|
if (!pullRequest || git === null)
|
|
@@ -384,7 +384,7 @@ export function registerStudioRoutes(context, options = {}) {
|
|
|
384
384
|
});
|
|
385
385
|
const command = review.change === 'install' ? 'install' : 'upgrade';
|
|
386
386
|
const body = [
|
|
387
|
-
`A
|
|
387
|
+
`A Shutro ERP Studio change: **${review.spec.label}** (\`${name}\` ${review.spec.version}).`,
|
|
388
388
|
'',
|
|
389
389
|
review.change === 'install'
|
|
390
390
|
? `Installs the module \`${name}\`.`
|
|
@@ -393,7 +393,7 @@ export function registerStudioRoutes(context, options = {}) {
|
|
|
393
393
|
'After deploying:',
|
|
394
394
|
'',
|
|
395
395
|
'```sh',
|
|
396
|
-
`npx
|
|
396
|
+
`npx shutro-erp apps ${command} ${name}`,
|
|
397
397
|
'```',
|
|
398
398
|
'',
|
|
399
399
|
...(review.statements.length === 0
|
|
@@ -417,14 +417,14 @@ export function registerStudioRoutes(context, options = {}) {
|
|
|
417
417
|
}
|
|
418
418
|
catch (error) {
|
|
419
419
|
if (error instanceof GitProposalError)
|
|
420
|
-
throw new HttpProblem(502, 'studio-git-failed', 'The repository refused the proposal', error.message, 'Check
|
|
420
|
+
throw new HttpProblem(502, 'studio-git-failed', 'The repository refused the proposal', error.message, 'Check SHUTRO_ERP_STUDIO_GIT_URL, the token and its rights (contents and pull requests); the patch can be downloaded meanwhile.');
|
|
421
421
|
throw error;
|
|
422
422
|
}
|
|
423
423
|
});
|
|
424
424
|
app.post('/api/v1/studio/apply', async (request) => {
|
|
425
425
|
const uid = await guard(request);
|
|
426
426
|
if (mode !== 'write')
|
|
427
|
-
throw new HttpProblem(409, 'studio-propose-only', 'Studio proposes changes here', 'This server runs Studio in propose mode: it never writes code. Download the patch and deploy it.', '
|
|
427
|
+
throw new HttpProblem(409, 'studio-propose-only', 'Studio proposes changes here', 'This server runs Studio in propose mode: it never writes code. Download the patch and deploy it.', 'SHUTRO_ERP_STUDIO=write is for development and single-server installations.');
|
|
428
428
|
const planId = request.body?.planId;
|
|
429
429
|
if (typeof planId !== 'string')
|
|
430
430
|
return invalid('planId (from /preview) is required.');
|
|
@@ -35,7 +35,7 @@ export const messages = {
|
|
|
35
35
|
openPullRequest: 'Open a pull request',
|
|
36
36
|
openMergeRequest: 'Open a merge request',
|
|
37
37
|
proposalOpened: 'Opened on {{repository}}:',
|
|
38
|
-
proposeNote: 'This server proposes changes: download the patch, commit it, and deploy it (then
|
|
38
|
+
proposeNote: 'This server proposes changes: download the patch, commit it, and deploy it (then shutro-erp apps upgrade).',
|
|
39
39
|
modulesTitle: 'Studio modules',
|
|
40
40
|
modulesIntro: 'Each Studio change is saved in a module of your project: reviewed as code, versioned, and upgraded like any app.',
|
|
41
41
|
modeWrite: 'Write mode: changes are applied on this server.',
|
|
@@ -109,7 +109,7 @@ export const messages = {
|
|
|
109
109
|
openPullRequest: 'পুল রিকোয়েস্ট খুলুন',
|
|
110
110
|
openMergeRequest: 'মার্জ রিকোয়েস্ট খুলুন',
|
|
111
111
|
proposalOpened: '{{repository}}-এ খোলা হয়েছে:',
|
|
112
|
-
proposeNote: 'এই সার্ভার পরিবর্তন প্রস্তাব করে: প্যাচ ডাউনলোড করে কমিট ও ডিপ্লয় করুন (তারপর
|
|
112
|
+
proposeNote: 'এই সার্ভার পরিবর্তন প্রস্তাব করে: প্যাচ ডাউনলোড করে কমিট ও ডিপ্লয় করুন (তারপর shutro-erp apps upgrade)।',
|
|
113
113
|
modulesTitle: 'স্টুডিও মডিউল',
|
|
114
114
|
modulesIntro: 'স্টুডিওর প্রতিটি পরিবর্তন আপনার প্রজেক্টের একটি মডিউলে সংরক্ষিত হয়: কোডের মতো পর্যালোচিত, সংস্করণযুক্ত এবং যেকোনো অ্যাপের মতো আপগ্রেডযোগ্য।',
|
|
115
115
|
modeWrite: 'রাইট মোড: পরিবর্তন এই সার্ভারে প্রয়োগ হয়।',
|
package/dist/ui/api.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface CurrentUser {
|
|
|
65
65
|
readonly name: string;
|
|
66
66
|
readonly company: number | null;
|
|
67
67
|
readonly companies: readonly number[];
|
|
68
|
+
/** Number grouping per company (RFC 0016); older servers send none. */
|
|
69
|
+
readonly groupings?: Readonly<Record<string, 'international' | 'lakh'>>;
|
|
68
70
|
readonly twoFactorEnabled: boolean;
|
|
69
71
|
readonly administrator: boolean;
|
|
70
72
|
readonly administratorNeedsSecondFactor: boolean;
|
package/dist/ui/app.js
CHANGED
|
@@ -97,7 +97,7 @@ const ServicesContext = createContext(null);
|
|
|
97
97
|
function useServices() {
|
|
98
98
|
const value = useContext(ServicesContext);
|
|
99
99
|
if (value === null)
|
|
100
|
-
throw new Error('
|
|
100
|
+
throw new Error('Shutro ERP routes render inside <KoshApp>.');
|
|
101
101
|
return value;
|
|
102
102
|
}
|
|
103
103
|
// ------------------------------------------------------------------------------------------------- shell
|
|
@@ -177,11 +177,11 @@ function Shell() {
|
|
|
177
177
|
const user = me.data;
|
|
178
178
|
const current = company ?? user.company ?? user.companies[0] ?? 0;
|
|
179
179
|
const app = apps.data?.find((a) => a.code === params.app);
|
|
180
|
-
return (_jsx(KoshProvider, { api: api, registry: registry, uid: user.id, companyId: current, lang: lang, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, ...(realtime === undefined ? {} : { realtime }), ...(offline === undefined ? {} : { offline }), children: _jsx(ToastProvider, { children: _jsxs(DesignProvider, { children: [_jsx(OfflineAgent, { uid: user.id }), _jsxs(PaletteContext.Provider, { value: openPalette, children: [_jsx("a", { href: "#content", className: "sr-only focus:not-sr-only focus:absolute focus:start-2 focus:top-2 focus:z-50 focus:rounded focus:bg-surface focus:px-3 focus:py-2", children: t('shell.skip') }), _jsx("header", { className: "sticky top-0 z-30 border-b bg-surface", children: _jsxs("div", { className: "flex h-12 items-center gap-2 px-3", children: [_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('shell.apps'), children: _jsx(Grid3x3, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { align: "start", children: [_jsx(MenuItem, { onSelect: () => void navigate({ to: '/' }), children: t('shell.apps') }), _jsx(MenuSeparator, {}), (apps.data ?? []).map((a) => (_jsx(MenuItem, { onSelect: () => void navigate({ to: '/$app', params: { app: a.code } }), children: a.label }, a.code)))] })] }), _jsx(Link, { to: "/", className: "font-semibold", children: app?.label ?? t('app.name') }), _jsx("nav", { "aria-label": t('shell.menu'), className: "ms-2 flex-1", children: params.app === undefined ? null : (_jsx(MenuLinks, { app: params.app, menus: menus.data ?? [] })) }), _jsxs(Button, { variant: "secondary", size: "sm", className: "hidden h-8 w-56 justify-between text-muted-foreground sm:flex", onClick: () => setPalette(true), "aria-keyshortcuts": "Control+K Meta+K", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(Search, { className: "size-4", "aria-hidden": true }), t('palette.button')] }), _jsx("kbd", { className: "rounded border bg-surface px-1.5 text-xs", children: "Ctrl K" })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "sm:hidden", "aria-label": t('palette.button'), onClick: () => setPalette(true), children: _jsx(Search, { "aria-hidden": true }) }), _jsx(OfflineMenu, {}), registry.headerItems().map(({ name, component: Item }) => (_jsx(Item, { api: api }, name))), _jsx(CompanySwitcher, { me: user, company: current, onChange: (id) => {
|
|
180
|
+
return (_jsx(KoshProvider, { api: api, registry: registry, uid: user.id, companyId: current, lang: lang, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, grouping: user.groupings?.[String(current)] ?? 'international', ...(realtime === undefined ? {} : { realtime }), ...(offline === undefined ? {} : { offline }), children: _jsx(ToastProvider, { children: _jsxs(DesignProvider, { children: [_jsx(OfflineAgent, { uid: user.id }), _jsxs(PaletteContext.Provider, { value: openPalette, children: [_jsx("a", { href: "#content", className: "sr-only focus:not-sr-only focus:absolute focus:start-2 focus:top-2 focus:z-50 focus:rounded focus:bg-surface focus:px-3 focus:py-2", children: t('shell.skip') }), _jsx("header", { className: "sticky top-0 z-30 border-b bg-surface", children: _jsxs("div", { className: "flex h-12 items-center gap-2 px-3", children: [_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('shell.apps'), children: _jsx(Grid3x3, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { align: "start", children: [_jsx(MenuItem, { onSelect: () => void navigate({ to: '/' }), children: t('shell.apps') }), _jsx(MenuSeparator, {}), (apps.data ?? []).map((a) => (_jsx(MenuItem, { onSelect: () => void navigate({ to: '/$app', params: { app: a.code } }), children: a.label }, a.code)))] })] }), _jsx(Link, { to: "/", className: "font-semibold", children: app?.label ?? t('app.name') }), _jsx("nav", { "aria-label": t('shell.menu'), className: "ms-2 flex-1", children: params.app === undefined ? null : (_jsx(MenuLinks, { app: params.app, menus: menus.data ?? [] })) }), _jsxs(Button, { variant: "secondary", size: "sm", className: "hidden h-8 w-56 justify-between text-muted-foreground sm:flex", onClick: () => setPalette(true), "aria-keyshortcuts": "Control+K Meta+K", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(Search, { className: "size-4", "aria-hidden": true }), t('palette.button')] }), _jsx("kbd", { className: "rounded border bg-surface px-1.5 text-xs", children: "Ctrl K" })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "sm:hidden", "aria-label": t('palette.button'), onClick: () => setPalette(true), children: _jsx(Search, { "aria-hidden": true }) }), _jsx(OfflineMenu, {}), registry.headerItems().map(({ name, component: Item }) => (_jsx(Item, { api: api }, name))), _jsx(CompanySwitcher, { me: user, company: current, onChange: (id) => {
|
|
181
181
|
api.setCompany(id);
|
|
182
182
|
setCompany(id);
|
|
183
183
|
void queryClient.invalidateQueries();
|
|
184
|
-
} }), _jsx(LanguageMenu, {}), _jsx(ThemeMenu, {}), _jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": user.name, children: _jsx(UserRound, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { children: [_jsx(MenuLabel, { children: user.name }), _jsxs(MenuItem, { onSelect: async () => {
|
|
184
|
+
} }), _jsx(LanguageMenu, {}), _jsx(ThemeMenu, {}), _jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": user.name, children: _jsx(UserRound, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { children: [_jsx(MenuLabel, { children: user.name }), registry.userPages().map((p) => (_jsx(MenuItem, { onSelect: () => void navigate({ to: '/me/$page', params: { page: p.name } }), children: t(p.label) }, p.name))), _jsxs(MenuItem, { onSelect: async () => {
|
|
185
185
|
await api.logout();
|
|
186
186
|
queryClient.clear();
|
|
187
187
|
await dropOfflineCache(persister);
|
|
@@ -451,9 +451,32 @@ const openRoute = createRoute({
|
|
|
451
451
|
path: '/open/$model/$id',
|
|
452
452
|
component: OpenRecordPage,
|
|
453
453
|
});
|
|
454
|
+
/** A personal page from the user menu (`registry.registerUserPage`), outside any app. */
|
|
455
|
+
function UserPage() {
|
|
456
|
+
const { page } = useParams({ strict: false });
|
|
457
|
+
const { api, registry } = useServices();
|
|
458
|
+
const { t } = useTranslation();
|
|
459
|
+
const entry = registry.userPages().find((p) => p.name === page);
|
|
460
|
+
if (entry === undefined)
|
|
461
|
+
return _jsx("p", { className: "p-4 text-sm", children: t('notFound') });
|
|
462
|
+
const Page = entry.component;
|
|
463
|
+
return _jsx(Page, { api: api, label: t(entry.label) });
|
|
464
|
+
}
|
|
465
|
+
const userPageRoute = createRoute({
|
|
466
|
+
getParentRoute: () => shellRoute,
|
|
467
|
+
path: '/me/$page',
|
|
468
|
+
component: UserPage,
|
|
469
|
+
});
|
|
454
470
|
const routeTree = rootRoute.addChildren([
|
|
455
471
|
loginRoute,
|
|
456
|
-
shellRoute.addChildren([
|
|
472
|
+
shellRoute.addChildren([
|
|
473
|
+
indexRoute,
|
|
474
|
+
openRoute,
|
|
475
|
+
userPageRoute,
|
|
476
|
+
appRoute,
|
|
477
|
+
actionRoute,
|
|
478
|
+
recordRoute,
|
|
479
|
+
]),
|
|
457
480
|
]);
|
|
458
481
|
const LANG_KEY = 'kosh.lang';
|
|
459
482
|
function initialLanguage() {
|
package/dist/ui/context.d.ts
CHANGED
|
@@ -26,13 +26,15 @@ export interface KoshContextValue {
|
|
|
26
26
|
export declare function useKosh(): KoshContextValue;
|
|
27
27
|
/** Applies the theme to `<html>` (the `dark` class) and follows the system when asked to. */
|
|
28
28
|
declare function useTheme(): [ThemeChoice, (t: ThemeChoice) => void];
|
|
29
|
-
export declare function KoshProvider({ api, registry, uid, companyId, lang, tz, realtime, offline, children, }: {
|
|
29
|
+
export declare function KoshProvider({ api, registry, uid, companyId, lang, tz, grouping, realtime, offline, children, }: {
|
|
30
30
|
readonly api: KoshApi;
|
|
31
31
|
readonly registry: Registry;
|
|
32
32
|
readonly uid: number;
|
|
33
33
|
readonly companyId: number;
|
|
34
34
|
readonly lang: string;
|
|
35
35
|
readonly tz: string;
|
|
36
|
+
/** The company's number grouping (RFC 0016). */
|
|
37
|
+
readonly grouping?: 'international' | 'lakh';
|
|
36
38
|
readonly realtime?: RealtimeClient;
|
|
37
39
|
readonly offline?: OfflineQueue;
|
|
38
40
|
readonly children: ReactNode;
|
package/dist/ui/context.js
CHANGED
|
@@ -47,9 +47,9 @@ function useTheme() {
|
|
|
47
47
|
}, [theme]);
|
|
48
48
|
return [theme, setTheme];
|
|
49
49
|
}
|
|
50
|
-
export function KoshProvider({ api, registry, uid, companyId, lang, tz, realtime, offline, children, }) {
|
|
50
|
+
export function KoshProvider({ api, registry, uid, companyId, lang, tz, grouping, realtime, offline, children, }) {
|
|
51
51
|
const [theme, setTheme] = useTheme();
|
|
52
|
-
const format = useMemo(() => createFormatter(lang, tz), [lang, tz]);
|
|
52
|
+
const format = useMemo(() => createFormatter(lang, tz, grouping === undefined ? {} : { grouping }), [lang, tz, grouping]);
|
|
53
53
|
const value = useMemo(() => ({
|
|
54
54
|
api,
|
|
55
55
|
registry,
|
package/dist/ui/i18n/messages.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const en = {
|
|
6
6
|
app: {
|
|
7
|
-
name: '
|
|
7
|
+
name: 'Shutro ERP',
|
|
8
8
|
loading: 'Loading…',
|
|
9
9
|
error: 'Something went wrong.',
|
|
10
10
|
retry: 'Retry',
|
|
@@ -169,7 +169,7 @@ export const en = {
|
|
|
169
169
|
synced_other: '{{count}} changes saved on this device were sent',
|
|
170
170
|
needsAttention_one: '{{count}} change could not be sent. Open the cloud in the header.',
|
|
171
171
|
needsAttention_other: '{{count}} changes could not be sent. Open the cloud in the header.',
|
|
172
|
-
newVersion: 'A new version of
|
|
172
|
+
newVersion: 'A new version of Shutro ERP is ready.',
|
|
173
173
|
reload: 'Reload',
|
|
174
174
|
},
|
|
175
175
|
home: {
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { ApiError, type ApiOptions, type AppEntry, type CurrentUser, createKoshA
|
|
|
11
11
|
export { KoshApp, type KoshAppProps } from './app.js';
|
|
12
12
|
export { AppIcon } from './components/AppIcon.js';
|
|
13
13
|
export * from './components/primitives.js';
|
|
14
|
-
export { type KoshContextValue, KoshProvider, type ThemeChoice, useKosh } from './context.js';
|
|
14
|
+
export { type KoshContextValue, KoshProvider, type ThemeChoice, useKosh, useTheme, } from './context.js';
|
|
15
15
|
export { type DesignContextValue, DesignProvider, type DesignSpot, useDesign, } from './design.js';
|
|
16
16
|
export { addMessages, createI18n, directionOf, NAMESPACE } from './i18n/index.js';
|
|
17
17
|
export { LANGUAGES } from './i18n/messages.js';
|
package/dist/ui/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { ApiError, createKoshApi, NetworkError, } from './api.js';
|
|
|
15
15
|
export { KoshApp } from './app.js';
|
|
16
16
|
export { AppIcon } from './components/AppIcon.js';
|
|
17
17
|
export * from './components/primitives.js';
|
|
18
|
-
export { KoshProvider, useKosh } from './context.js';
|
|
18
|
+
export { KoshProvider, useKosh, useTheme, } from './context.js';
|
|
19
19
|
export { DesignProvider, useDesign, } from './design.js';
|
|
20
20
|
export { addMessages, createI18n, directionOf, NAMESPACE } from './i18n/index.js';
|
|
21
21
|
export { LANGUAGES } from './i18n/messages.js';
|
package/dist/ui/lib/format.d.ts
CHANGED
|
@@ -14,4 +14,6 @@ export interface Formatter {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function createFormatter(lang: string, tz: string, options?: {
|
|
16
16
|
readonly nativeDigits?: boolean;
|
|
17
|
+
/** `lakh`: 12,34,567 (RFC 0016). Bangla groups so already; English takes the Indian English pattern. */
|
|
18
|
+
readonly grouping?: 'international' | 'lakh';
|
|
17
19
|
}): Formatter;
|
package/dist/ui/lib/format.js
CHANGED
|
@@ -11,7 +11,9 @@ export function createFormatter(lang, tz, options = {}) {
|
|
|
11
11
|
// Bangla defaults to Bengali digits in ICU; RFC 0007 makes them a choice (Latin digits unless asked).
|
|
12
12
|
const locale = base.startsWith('bn')
|
|
13
13
|
? `${base}-u-nu-${options.nativeDigits === true ? 'beng' : 'latn'}`
|
|
14
|
-
: base
|
|
14
|
+
: options.grouping === 'lakh' && base.startsWith('en')
|
|
15
|
+
? 'en-IN'
|
|
16
|
+
: base;
|
|
15
17
|
const integer = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
|
|
16
18
|
const digits = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
17
19
|
const separator = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === 'decimal')?.value ??
|
package/dist/ui/pwa/worker.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** The worker's JavaScript. */
|
|
9
9
|
export function serviceWorkerSource(config) {
|
|
10
|
-
return `/*
|
|
10
|
+
return `/* Shutro ERP service worker ${config.version}: generated by shutro-erp/ui/vite, do not edit. */
|
|
11
11
|
const VERSION = ${JSON.stringify(config.version)};
|
|
12
12
|
const ASSETS = ${JSON.stringify(config.assets)};
|
|
13
13
|
const CACHE = 'kosh-' + VERSION;
|
|
@@ -57,7 +57,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
self.addEventListener('push', (event) => {
|
|
60
|
-
let data = { title: '
|
|
60
|
+
let data = { title: 'Shutro ERP', body: '', url: '/', id: 0 };
|
|
61
61
|
try {
|
|
62
62
|
data = { ...data, ...event.data.json() };
|
|
63
63
|
} catch {
|
package/dist/ui/registry.d.ts
CHANGED
|
@@ -128,6 +128,21 @@ export declare class Registry {
|
|
|
128
128
|
registerHeaderItem(name: string, component: ComponentType<HeaderItemProps>, options?: {
|
|
129
129
|
readonly replace?: boolean;
|
|
130
130
|
}): this;
|
|
131
|
+
/**
|
|
132
|
+
* A personal page every signed-in user reaches from the user menu, outside any app (`settings.ApiKeys`):
|
|
133
|
+
* served at `/me/<name>`; `label` is an i18n key (`settings:apiKeys.title`). The server still decides what the
|
|
134
|
+
* page may show and do.
|
|
135
|
+
*/
|
|
136
|
+
registerUserPage(name: string, component: ComponentType<PageProps>, options: {
|
|
137
|
+
readonly label: string;
|
|
138
|
+
readonly replace?: boolean;
|
|
139
|
+
}): this;
|
|
140
|
+
/** The user pages, in registration order, with their label keys. */
|
|
141
|
+
userPages(): readonly {
|
|
142
|
+
readonly name: string;
|
|
143
|
+
readonly label: string;
|
|
144
|
+
readonly component: ComponentType<PageProps>;
|
|
145
|
+
}[];
|
|
131
146
|
/** The header items, in registration order. */
|
|
132
147
|
headerItems(): readonly {
|
|
133
148
|
readonly name: string;
|
package/dist/ui/registry.js
CHANGED
|
@@ -12,6 +12,7 @@ export class Registry {
|
|
|
12
12
|
component: new Map(),
|
|
13
13
|
page: new Map(),
|
|
14
14
|
header: new Map(),
|
|
15
|
+
userPage: new Map(),
|
|
15
16
|
searchAssist: new Map(),
|
|
16
17
|
chatterAssist: new Map(),
|
|
17
18
|
createAssist: new Map(),
|
|
@@ -58,6 +59,25 @@ export class Registry {
|
|
|
58
59
|
registerHeaderItem(name, component, options = {}) {
|
|
59
60
|
return this.#add('header', name, component, options.replace === true);
|
|
60
61
|
}
|
|
62
|
+
#userPageLabels = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* A personal page every signed-in user reaches from the user menu, outside any app (`settings.ApiKeys`):
|
|
65
|
+
* served at `/me/<name>`; `label` is an i18n key (`settings:apiKeys.title`). The server still decides what the
|
|
66
|
+
* page may show and do.
|
|
67
|
+
*/
|
|
68
|
+
registerUserPage(name, component, options) {
|
|
69
|
+
this.#add('userPage', name, component, options.replace === true);
|
|
70
|
+
this.#userPageLabels.set(name, options.label);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
/** The user pages, in registration order, with their label keys. */
|
|
74
|
+
userPages() {
|
|
75
|
+
return [...this.#entries.userPage].map(([name, component]) => ({
|
|
76
|
+
name,
|
|
77
|
+
label: this.#userPageLabels.get(name) ?? name,
|
|
78
|
+
component: component,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
61
81
|
/** The header items, in registration order. */
|
|
62
82
|
headerItems() {
|
|
63
83
|
return [...this.#entries.header].map(([name, component]) => ({
|
|
@@ -19,23 +19,36 @@ import { cn } from '../lib/cn.js';
|
|
|
19
19
|
import { useLiveRecord } from '../live.js';
|
|
20
20
|
import { useToast } from '../shell/toast.js';
|
|
21
21
|
import { PrefetchedNames, useRowNames } from './widgets.js';
|
|
22
|
-
|
|
22
|
+
/** A line's values as the server takes them: many2many as `{ set }` (a line's taxes). */
|
|
23
|
+
function lineValues(values, fields) {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [key, value] of Object.entries(values))
|
|
26
|
+
out[key] = fields[key]?.kind === 'many2many' && Array.isArray(value) ? { set: value } : value;
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function commandsOf(lines, fields) {
|
|
23
30
|
return {
|
|
24
31
|
...(lines.rows.some((r) => r.id === undefined)
|
|
25
|
-
? {
|
|
32
|
+
? {
|
|
33
|
+
create: lines.rows
|
|
34
|
+
.filter((r) => r.id === undefined)
|
|
35
|
+
.map((r) => lineValues(r.values, fields)),
|
|
36
|
+
}
|
|
26
37
|
: {}),
|
|
27
38
|
...(lines.rows.some((r) => r.id !== undefined && r.dirty)
|
|
28
39
|
? {
|
|
29
40
|
update: lines.rows
|
|
30
41
|
.filter((r) => r.id !== undefined && r.dirty)
|
|
31
|
-
.map((r) => ({ id: r.id, values: r.values })),
|
|
42
|
+
.map((r) => ({ id: r.id, values: lineValues(r.values, fields) })),
|
|
32
43
|
}
|
|
33
44
|
: {}),
|
|
34
45
|
...(lines.deleted.length > 0 ? { delete: lines.deleted } : {}),
|
|
35
46
|
};
|
|
36
47
|
}
|
|
37
48
|
/** The payload of a save: changed values, many2many as `{ set }`. */
|
|
38
|
-
function payloadOf(changes, fields, lines
|
|
49
|
+
function payloadOf(changes, fields, lines,
|
|
50
|
+
/** The fields of a one2many's lines. */
|
|
51
|
+
lineFields) {
|
|
39
52
|
const out = {};
|
|
40
53
|
for (const [key, value] of Object.entries(changes)) {
|
|
41
54
|
if (fields[key]?.kind === 'many2many')
|
|
@@ -44,7 +57,7 @@ function payloadOf(changes, fields, lines) {
|
|
|
44
57
|
out[key] = value;
|
|
45
58
|
}
|
|
46
59
|
for (const [key, state] of lines) {
|
|
47
|
-
const commands = commandsOf(state);
|
|
60
|
+
const commands = commandsOf(state, lineFields(key));
|
|
48
61
|
if (Object.keys(commands).length > 0)
|
|
49
62
|
out[key] = commands;
|
|
50
63
|
}
|
|
@@ -177,7 +190,7 @@ export function FormView({ served, recordId, defaults, onSaved, onBack, crumbs,
|
|
|
177
190
|
};
|
|
178
191
|
const save = useMutation({
|
|
179
192
|
mutationFn: async () => {
|
|
180
|
-
const payload = payloadOf(changes, fields, lines);
|
|
193
|
+
const payload = payloadOf(changes, fields, lines, (key) => served.fields[fields[key]?.target ?? ''] ?? {});
|
|
181
194
|
const etag = loaded.data?.etag ?? null;
|
|
182
195
|
try {
|
|
183
196
|
return isNew
|