shutro-erp 0.3.2 → 0.3.4
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/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +156 -1
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +156 -1
- package/dist/assistant/index.js +1 -1
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +156 -1
- package/dist/integrations/index.js +12 -0
- package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
- package/dist/integrations/web/OAuthAppsPage.js +79 -0
- package/dist/integrations/web/index.d.ts +1 -0
- package/dist/integrations/web/index.js +3 -0
- package/dist/integrations/web/messages.js +58 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +156 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +156 -4
- package/dist/l10n_in/module.d.ts +0 -62
- package/dist/l10n_in/module.js +2 -4
- package/dist/messaging/web/index.js +1 -1
- package/dist/notify/web/index.js +1 -1
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +157 -2
- package/dist/payment/bkash.d.ts +28 -0
- package/dist/payment/bkash.js +169 -0
- package/dist/payment/index.d.ts +4 -0
- package/dist/payment/index.js +80 -24
- package/dist/payment/module.d.ts +7 -4
- package/dist/payment/module.js +3 -0
- package/dist/payment/money.d.ts +7 -0
- package/dist/payment/money.js +37 -0
- package/dist/payment/providers.d.ts +13 -2
- package/dist/payment/razorpay.d.ts +12 -0
- package/dist/payment/razorpay.js +149 -0
- package/dist/payment/sslcommerz.js +1 -1
- package/dist/payment/stripe.d.ts +15 -0
- package/dist/payment/stripe.js +182 -0
- package/dist/permissions/admin.d.ts +3 -0
- package/dist/permissions/admin.js +108 -10
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/roles.d.ts +2 -0
- package/dist/permissions/roles.js +236 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +156 -1
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +156 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +156 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +156 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +156 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +156 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +156 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +156 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +156 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +156 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +156 -1
- package/dist/server/auth/base.js +98 -0
- package/dist/server/auth/oauth.d.ts +21 -0
- package/dist/server/auth/oauth.js +579 -0
- package/dist/server/auth/routes.d.ts +3 -0
- package/dist/server/auth/routes.js +29 -0
- package/dist/server/auth/service.d.ts +56 -0
- package/dist/server/auth/service.js +185 -3
- package/dist/server/identity.d.ts +4 -0
- package/dist/server/images.d.ts +12 -0
- package/dist/server/images.js +139 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +1 -0
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.js +20 -4
- package/dist/settings/company.d.ts +34 -0
- package/dist/settings/company.js +189 -0
- package/dist/settings/index.d.ts +10 -10
- package/dist/settings/index.js +159 -30
- package/dist/settings/people.d.ts +16 -0
- package/dist/settings/people.js +507 -0
- package/dist/settings/security.d.ts +13 -0
- package/dist/settings/security.js +158 -0
- package/dist/settings/web/AuditPage.d.ts +8 -0
- package/dist/settings/web/AuditPage.js +74 -0
- package/dist/settings/web/CompaniesPage.d.ts +8 -0
- package/dist/settings/web/CompaniesPage.js +52 -0
- package/dist/settings/web/CompanyPage.d.ts +9 -0
- package/dist/settings/web/CompanyPage.js +105 -0
- package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
- package/dist/settings/web/ConnectedAppsPage.js +33 -0
- package/dist/settings/web/OverviewPage.d.ts +9 -0
- package/dist/settings/web/OverviewPage.js +77 -0
- package/dist/settings/web/PeoplePage.d.ts +9 -0
- package/dist/settings/web/PeoplePage.js +154 -0
- package/dist/settings/web/ProfilePage.d.ts +14 -0
- package/dist/settings/web/ProfilePage.js +196 -0
- package/dist/settings/web/RolesEditor.d.ts +35 -0
- package/dist/settings/web/RolesEditor.js +49 -0
- package/dist/settings/web/RolesPage.d.ts +9 -0
- package/dist/settings/web/RolesPage.js +296 -0
- package/dist/settings/web/SecurityPage.d.ts +8 -0
- package/dist/settings/web/SecurityPage.js +81 -0
- package/dist/settings/web/index.d.ts +10 -0
- package/dist/settings/web/index.js +31 -1
- package/dist/settings/web/messages.js +594 -0
- package/dist/ui/api.d.ts +8 -0
- package/dist/ui/app.d.ts +5 -0
- package/dist/ui/app.js +143 -33
- package/dist/ui/components/AppIcon.js +11 -1
- package/dist/ui/components/Avatar.d.ts +21 -0
- package/dist/ui/components/Avatar.js +71 -0
- package/dist/ui/components/primitives.d.ts +9 -1
- package/dist/ui/components/primitives.js +12 -4
- package/dist/ui/context.d.ts +10 -1
- package/dist/ui/context.js +60 -2
- package/dist/ui/i18n/messages.d.ts +76 -0
- package/dist/ui/i18n/messages.js +152 -0
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.js +3 -1
- package/dist/ui/lib/accent.d.ts +13 -0
- package/dist/ui/lib/accent.js +36 -0
- package/dist/ui/registry.d.ts +7 -1
- package/dist/ui/registry.js +10 -1
- package/dist/ui/search/ViewTabs.d.ts +14 -0
- package/dist/ui/search/ViewTabs.js +42 -0
- package/dist/ui/shell/Activate.d.ts +6 -0
- package/dist/ui/shell/Activate.js +39 -0
- package/dist/ui/shell/CommandPalette.d.ts +7 -1
- package/dist/ui/shell/CommandPalette.js +16 -2
- package/dist/ui/shell/Consent.d.ts +5 -0
- package/dist/ui/shell/Consent.js +35 -0
- package/dist/ui/shell/Home.js +85 -16
- package/dist/ui/shell/Sidebar.d.ts +27 -0
- package/dist/ui/shell/Sidebar.js +77 -0
- package/dist/ui/shell/TwoFactorSetup.d.ts +5 -0
- package/dist/ui/shell/TwoFactorSetup.js +32 -0
- package/dist/ui/shell/shortcuts.d.ts +3 -0
- package/dist/ui/shell/shortcuts.js +13 -4
- package/dist/ui/styles.css +70 -28
- package/dist/ui/views/DateField.d.ts +10 -0
- package/dist/ui/views/DateField.js +129 -0
- package/dist/ui/views/FormView.js +161 -21
- package/dist/ui/views/ListView.d.ts +2 -0
- package/dist/ui/views/ListView.js +57 -25
- package/dist/ui/views/StatusChip.d.ts +10 -0
- package/dist/ui/views/StatusChip.js +22 -0
- package/dist/ui/views/widgets.d.ts +3 -0
- package/dist/ui/views/widgets.js +26 -8
- package/dist/views/types.d.ts +7 -0
- package/dist/views/validate.js +4 -0
- package/package.json +2 -2
|
@@ -27,6 +27,11 @@ import { PrefetchedNames, useRowNames } from './widgets.js';
|
|
|
27
27
|
const features = tableFeatures({});
|
|
28
28
|
const helper = createColumnHelper();
|
|
29
29
|
const HEIGHTS = { comfortable: 40, compact: 32 };
|
|
30
|
+
/** The record's page in a new tab: the list's address with the record's id (`/<app>/a/<action>/<id>`). */
|
|
31
|
+
function openInNewTab(id) {
|
|
32
|
+
const path = window.location.pathname.replace(/\/$/, '');
|
|
33
|
+
window.open(`${path}/${id}`, '_blank', 'noopener');
|
|
34
|
+
}
|
|
30
35
|
/** Requests a bulk action sends at once. */
|
|
31
36
|
const PARALLEL = 6;
|
|
32
37
|
/** How long a bulk delete waits for Undo before it runs. */
|
|
@@ -188,7 +193,6 @@ function Rows({ props, where, columns, fields, offset, limit, height, selection,
|
|
|
188
193
|
offset,
|
|
189
194
|
count: true,
|
|
190
195
|
});
|
|
191
|
-
onCount?.(result.count ?? result.items.length);
|
|
192
196
|
rememberList(model, result.items.map((r) => r.id), offset, result.count ?? result.items.length);
|
|
193
197
|
return result;
|
|
194
198
|
},
|
|
@@ -199,6 +203,11 @@ function Rows({ props, where, columns, fields, offset, limit, height, selection,
|
|
|
199
203
|
if (query.data !== undefined)
|
|
200
204
|
onRows?.(query.data.items);
|
|
201
205
|
}, [query.data, onRows]);
|
|
206
|
+
// From the data, not the fetch: a search answered from the cache (back to All) has its count too.
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (query.data !== undefined)
|
|
209
|
+
onCount?.(query.data.count ?? query.data.items.length);
|
|
210
|
+
}, [query.data, onCount]);
|
|
202
211
|
const names = useRowNames(items, Object.values(fields));
|
|
203
212
|
const tableColumns = useMemo(() => helper.columns(columns.map((node) => helper.accessor((row) => row[node.field], {
|
|
204
213
|
id: node.id,
|
|
@@ -488,7 +497,11 @@ function Rows({ props, where, columns, fields, offset, limit, height, selection,
|
|
|
488
497
|
if (query.isError)
|
|
489
498
|
return (_jsx(Alert, { action: _jsx(Button, { size: "sm", onClick: () => void query.refetch(), children: t('app.retry') }), children: query.error instanceof ApiError ? query.error.message : t('app.error') }));
|
|
490
499
|
if (items.length === 0)
|
|
491
|
-
return (_jsx(EmptyState, { icon: _jsx(Inbox, { "aria-hidden": true }), title: filtered === true
|
|
500
|
+
return (_jsx(EmptyState, { icon: _jsx(Inbox, { "aria-hidden": true }), title: filtered === true
|
|
501
|
+
? t('views.emptyFiltered')
|
|
502
|
+
: props.served.view.label === undefined
|
|
503
|
+
? t('views.emptyTitle')
|
|
504
|
+
: t('views.emptyNamed', { name: props.served.view.label.toLocaleLowerCase() }), ...(filtered === true ? {} : { description: t('views.emptyHint') }), ...(props.served.operations.create && offset === 0
|
|
492
505
|
? {
|
|
493
506
|
action: (_jsxs(Button, { variant: "primary", size: "sm", onClick: () => props.onOpen('new'), children: [_jsx(Plus, { "aria-hidden": true }), t('views.createFirst')] })),
|
|
494
507
|
}
|
|
@@ -525,8 +538,20 @@ function Rows({ props, where, columns, fields, offset, limit, height, selection,
|
|
|
525
538
|
return null;
|
|
526
539
|
const id = row.original.id;
|
|
527
540
|
const isSelected = selection?.selected.has(id) === true;
|
|
528
|
-
return (_jsxs("tr", { tabIndex: 0, "aria-selected": selection === undefined ? undefined : isSelected, style: { height }, className: cn('cursor-pointer border-b hover:bg-muted/60 focus-visible:bg-muted', isSelected && 'bg-accent/60 hover:bg-accent/70'),
|
|
529
|
-
|
|
541
|
+
return (_jsxs("tr", { tabIndex: 0, "aria-selected": selection === undefined ? undefined : isSelected, style: { height }, className: cn('cursor-pointer border-b hover:bg-muted/60 focus-visible:bg-muted', isSelected && 'bg-accent/60 hover:bg-accent/70'),
|
|
542
|
+
// Ctrl / Cmd + click or the middle button: the record in a new tab, the list stays.
|
|
543
|
+
onClick: (e) => {
|
|
544
|
+
if (e.ctrlKey || e.metaKey)
|
|
545
|
+
openInNewTab(id);
|
|
546
|
+
else
|
|
547
|
+
props.onOpen(id);
|
|
548
|
+
}, onAuxClick: (e) => {
|
|
549
|
+
if (e.button === 1)
|
|
550
|
+
openInNewTab(id);
|
|
551
|
+
}, onKeyDown: (e) => {
|
|
552
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey))
|
|
553
|
+
openInNewTab(id);
|
|
554
|
+
else if (e.key === 'Enter')
|
|
530
555
|
props.onOpen(id);
|
|
531
556
|
// Space on the row itself selects it (the checkbox handles its own keys).
|
|
532
557
|
if (e.key === ' ' && selection !== undefined && e.target === e.currentTarget) {
|
|
@@ -597,7 +622,7 @@ function Totals({ props, columns, fields, }) {
|
|
|
597
622
|
: v === null || v === undefined
|
|
598
623
|
? '0'
|
|
599
624
|
: String(v);
|
|
600
|
-
return (_jsxs("div", { className: "flex gap-2", children: [
|
|
625
|
+
return (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-muted-foreground", children: totalLabel(t('views.total'), c.label ?? fields[c.field]?.label ?? c.field) }), _jsx("dd", { className: "font-medium tabular-nums", children: format.decimal(amount) })] }, c.id));
|
|
601
626
|
}) }));
|
|
602
627
|
}
|
|
603
628
|
function Groups({ props, columns, fields, height, }) {
|
|
@@ -653,6 +678,12 @@ function saveFile(blob, filename) {
|
|
|
653
678
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
654
679
|
}
|
|
655
680
|
/** The default list renderer. */
|
|
681
|
+
/** A sum's label: "Total Credit limit", but "Total" alone for a column already called "Total" (no "Total Total"). */
|
|
682
|
+
export function totalLabel(word, label) {
|
|
683
|
+
return label.toLocaleLowerCase().startsWith(word.toLocaleLowerCase())
|
|
684
|
+
? label
|
|
685
|
+
: `${word} ${label}`;
|
|
686
|
+
}
|
|
656
687
|
export function ListView(props) {
|
|
657
688
|
const { api, uid } = useKosh();
|
|
658
689
|
const { t } = useTranslation();
|
|
@@ -669,7 +700,7 @@ export function ListView(props) {
|
|
|
669
700
|
const allColumns = view.columns.filter((c) => fields[c.field] !== undefined);
|
|
670
701
|
const columnsKey = `kosh.columns.${uid}.${view.id}`;
|
|
671
702
|
const [hidden, setHidden] = useState(() => new Set(stored.get(columnsKey, [])));
|
|
672
|
-
const
|
|
703
|
+
const { density, setDensity } = useKosh();
|
|
673
704
|
const height = HEIGHTS[density];
|
|
674
705
|
const columns = allColumns.filter((c) => !hidden.has(c.id));
|
|
675
706
|
const design = useDesign();
|
|
@@ -808,25 +839,26 @@ export function ListView(props) {
|
|
|
808
839
|
tone: 'danger',
|
|
809
840
|
}),
|
|
810
841
|
});
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
842
|
+
// Selected rows: a floating bar at the bottom of the screen (RFC 0018 §4.3); the list's own toolbar stays put.
|
|
843
|
+
const floating = ids.length > 0 ? (_jsxs("div", { role: "toolbar", "aria-label": t('views.selected', { count: ids.length }), "data-kosh-selection-bar": true, className: "fixed inset-x-0 bottom-4 z-30 mx-auto flex w-fit max-w-[calc(100vw-2rem)] flex-wrap items-center gap-2 rounded-xl border bg-surface px-3 py-2 text-sm shadow-lg", children: [_jsx("span", { className: "font-medium", children: t('views.selected', { count: ids.length }) }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('views.clearSelection'), onClick: () => setSelected(new Set()), children: _jsx(X, { "aria-hidden": true }) }), _jsx("span", { className: "flex-1" }), transitions.map((tr) => (_jsx(Button, { size: "sm", busy: transition.isPending && transition.variables?.name === tr.name, onClick: () => transition.mutate({ name: tr.name, what: ids }), children: tr.name.charAt(0).toUpperCase() + tr.name.slice(1).replace(/_/g, ' ') }, tr.name))), archivable ? (_jsxs(Button, { size: "sm", busy: archive.isPending, onClick: () => archive.mutate(ids), children: [_jsx(Archive, { "aria-hidden": true }), t('views.archive')] })) : null, served.operations.unlink ? (_jsxs(Button, { size: "sm", variant: "danger", onClick: () => setConfirmingDelete(true), children: [_jsx(Trash2, { "aria-hidden": true }), t('views.delete')] })) : null, canExport ? (_jsxs(Button, { size: "sm", busy: exportAs.isPending, onClick: () => exportAs.mutate('csv'), children: [_jsx(Download, { "aria-hidden": true }), t('views.export')] })) : null] })) : null;
|
|
844
|
+
const toolbar = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b px-3 py-1.5 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: count === null ? (_jsx(Skeleton, { className: "inline-block h-4 w-20" })) : (t('views.records', { count })) }), _jsx("span", { className: "flex-1" }), design.active ? (_jsxs(Button, { size: "sm", variant: "ghost", className: "border border-dashed border-primary/50 text-primary", onClick: () => design.choose({
|
|
845
|
+
kind: 'list',
|
|
846
|
+
view: view.id,
|
|
847
|
+
model,
|
|
848
|
+
after: allColumns.at(-1)?.id ?? '',
|
|
849
|
+
shown: allColumns.map((c) => c.field),
|
|
850
|
+
}), children: [_jsx(Plus, { "aria-hidden": true }), t('views.design.addColumn')] })) : null, _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('views.density'), "aria-pressed": density === 'compact', title: density === 'compact' ? t('views.comfortable') : t('views.compact'), onClick: () => {
|
|
851
|
+
const next = density === 'compact' ? 'comfortable' : 'compact';
|
|
852
|
+
setDensity(next);
|
|
853
|
+
}, children: _jsx(Rows3, { "aria-hidden": true }) }), _jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('views.columns'), children: _jsx(Columns3, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { align: "end", children: [_jsx(MenuLabel, { children: t('views.columns') }), allColumns.map((c) => (_jsx(MenuCheckboxItem, { checked: !hidden.has(c.id), onCheckedChange: (checked) => {
|
|
854
|
+
const next = new Set(hidden);
|
|
855
|
+
if (checked)
|
|
856
|
+
next.delete(c.id);
|
|
857
|
+
else if (next.size < allColumns.length - 1)
|
|
858
|
+
next.add(c.id);
|
|
859
|
+
setHidden(next);
|
|
860
|
+
stored.set(columnsKey, [...next]);
|
|
861
|
+
}, children: c.label ?? fields[c.field]?.label ?? c.field }, c.id)))] })] }), canExport ? (_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('views.export'), busy: exportAs.isPending, children: _jsx(Download, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { align: "end", children: [_jsx(MenuLabel, { children: t('views.export') }), _jsx(MenuItem, { onSelect: () => exportAs.mutate('csv'), children: t('views.exportCsv') }), _jsx(MenuItem, { onSelect: () => exportAs.mutate('xlsx'), children: t('views.exportXlsx') })] })] })) : null] }), floating] }));
|
|
830
862
|
return (_jsxs("div", { className: "rounded-lg border bg-surface", children: [props.groupBy.length > 0 ? (_jsx(Groups, { props: props, columns: columns, fields: fields, height: height })) : (_jsxs(_Fragment, { children: [toolbar, _jsx(Rows, { props: props, where: props.where, columns: columns, fields: fields, offset: offset, limit: pageSize, height: height, selection: { selected, onToggle: toggle, onToggleAll: toggleAll }, hiddenIds: pendingDelete, onCount: setCount, onRows: setPageRows, filtered: filtered }), count !== null && count > pageSize ? (_jsxs("nav", { "aria-label": "Pagination", className: "flex items-center justify-end gap-2 border-t px-3 py-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: t('views.range', {
|
|
831
863
|
from: offset + 1,
|
|
832
864
|
to: Math.min(offset + pageSize, count),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { ViewField } from '../api.js';
|
|
3
|
+
export declare function StatusChip({ field, value, transitions, }: {
|
|
4
|
+
readonly field: ViewField;
|
|
5
|
+
readonly value: unknown;
|
|
6
|
+
readonly transitions: readonly {
|
|
7
|
+
readonly from: readonly string[];
|
|
8
|
+
readonly to: string;
|
|
9
|
+
}[];
|
|
10
|
+
}): ReactElement;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A record's state as one chip in its meaning's tone (RFC 0018 §4.2), instead of a row of every state: it opens the
|
|
4
|
+
* whole workflow — the states passed, the current one, the ones a transition leads to next. The server serves the
|
|
5
|
+
* transitions; the chip only shows them.
|
|
6
|
+
*/
|
|
7
|
+
import { Check, ChevronDown, Circle } from 'lucide-react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { badgeVariants, Popover, PopoverContent, PopoverTrigger, } from '../components/primitives.js';
|
|
10
|
+
import { cn } from '../lib/cn.js';
|
|
11
|
+
import { toneOf } from './widgets.js';
|
|
12
|
+
export function StatusChip({ field, value, transitions, }) {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const options = field.options ?? [];
|
|
15
|
+
const index = options.findIndex((o) => o.value === value);
|
|
16
|
+
const current = options[index];
|
|
17
|
+
const next = new Set(transitions.filter((tr) => tr.from.includes(String(value))).map((tr) => tr.to));
|
|
18
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": t('views.statusOf', { field: field.label, state: current?.label ?? '—' }), className: cn(badgeVariants({ tone: toneOf(String(value ?? '')) }), 'gap-1.5 px-2.5 py-1 hover:brightness-95 focus-visible:outline-2'), children: [_jsx("span", { className: "size-1.5 rounded-full bg-current", "aria-hidden": true }), current?.label ?? '—', _jsx(ChevronDown, { className: "size-3.5 opacity-70", "aria-hidden": true })] }) }), _jsxs(PopoverContent, { className: "w-64", children: [_jsx("p", { className: "mb-2 px-2 text-xs font-medium text-muted-foreground", children: field.label }), _jsx("ol", { className: "grid gap-0.5", children: options.map((o, i) => {
|
|
19
|
+
const isCurrent = i === index;
|
|
20
|
+
return (_jsxs("li", { "aria-current": isCurrent ? 'step' : undefined, className: cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm', isCurrent ? 'bg-accent font-medium text-accent-foreground' : 'text-foreground', i > index && !next.has(o.value) && 'text-muted-foreground'), children: [i < index ? (_jsx(Check, { className: "size-3.5 text-success", "aria-hidden": true })) : (_jsx(Circle, { className: cn('size-3.5', isCurrent && 'fill-current'), "aria-hidden": true })), _jsx("span", { className: "flex-1", children: o.label }), next.has(o.value) ? (_jsx("span", { className: "text-xs text-muted-foreground", children: t('views.nextState') })) : null] }, o.value));
|
|
21
|
+
}) })] })] }));
|
|
22
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Context } from 'react';
|
|
2
2
|
import type { Registry } from '../registry.js';
|
|
3
|
+
type Tone = 'info' | 'success' | 'danger' | 'warning' | 'neutral';
|
|
4
|
+
export declare function toneOf(value: string): Tone;
|
|
3
5
|
/** Display names a list or kanban loaded in one batch per model, so cells do not query one by one. */
|
|
4
6
|
export declare const PrefetchedNames: Context<ReadonlyMap<string, ReadonlyMap<number, string>>>;
|
|
5
7
|
/** Display names of ids: from {@link PrefetchedNames}, else batched through the name-search endpoint (cached). */
|
|
@@ -12,3 +14,4 @@ export declare function useRowNames(rows: readonly Readonly<Record<string, unkno
|
|
|
12
14
|
}[]): ReadonlyMap<string, ReadonlyMap<number, string>>;
|
|
13
15
|
/** Registers the default widgets. */
|
|
14
16
|
export declare function registerDefaultWidgets(registry: Registry): Registry;
|
|
17
|
+
export {};
|
package/dist/ui/views/widgets.js
CHANGED
|
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
11
11
|
import { Badge, Checkbox, Input, Select, Textarea } from '../components/primitives.js';
|
|
12
12
|
import { useKosh } from '../context.js';
|
|
13
13
|
import { cn } from '../lib/cn.js';
|
|
14
|
+
import { DateField } from './DateField.js';
|
|
14
15
|
const text = (v) => (v === null || v === undefined ? '' : String(v));
|
|
15
16
|
function CharWidget({ id, value, readonly, onChange, field, invalid }) {
|
|
16
17
|
if (onChange === undefined || readonly)
|
|
@@ -56,11 +57,11 @@ function BooleanWidget({ id, value, readonly, onChange, field }) {
|
|
|
56
57
|
return _jsx("span", { children: value === true ? t('views.yes') : t('views.no') });
|
|
57
58
|
return (_jsx(Checkbox, { id: id, checked: value === true, disabled: readonly, "aria-label": field.label, onCheckedChange: (c) => onChange(c === true) }));
|
|
58
59
|
}
|
|
59
|
-
function DateWidget({ id, value, readonly, onChange }) {
|
|
60
|
+
function DateWidget({ id, value, readonly, onChange, field, invalid }) {
|
|
60
61
|
const { format } = useKosh();
|
|
61
62
|
if (onChange === undefined || readonly)
|
|
62
63
|
return _jsx("span", { children: format.date(value === null || value === undefined ? null : String(value)) });
|
|
63
|
-
return (_jsx(
|
|
64
|
+
return (_jsx(DateField, { id: id, value: value === null || value === undefined || value === '' ? null : text(value).slice(0, 10), onChange: onChange, required: field.required, ...(invalid === undefined ? {} : { invalid }) }));
|
|
64
65
|
}
|
|
65
66
|
function DatetimeWidget({ id, value, readonly, onChange }) {
|
|
66
67
|
const { format } = useKosh();
|
|
@@ -80,14 +81,31 @@ function SelectionWidget({ id, value, readonly, onChange, field }) {
|
|
|
80
81
|
return _jsx("span", { children: label });
|
|
81
82
|
return (_jsx(Select, { id: id, label: field.label, value: value === null || value === undefined ? null : String(value), options: options, onChange: onChange }));
|
|
82
83
|
}
|
|
83
|
-
|
|
84
|
+
/** What a state's value means, by the words ERP states use (`posted`, `to_approve`, `cancel`…). */
|
|
85
|
+
const MEANINGS = [
|
|
86
|
+
[
|
|
87
|
+
/(^|_)(cancel|cancelled|canceled|failed|refused|rejected|lost|blocked|overdue|error|bounced)($|_)/,
|
|
88
|
+
'danger',
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
/(^|_)(to|not|partial|partially|pending|waiting|wait|hold|review|approve|quarantine)($|_)/,
|
|
92
|
+
'warning',
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
/(^|_)(done|paid|posted|sale|confirmed|approved|won|delivered|validated|completed|complete|reconciled|invoiced|handled|active|accepted|signed|locked)($|_)/,
|
|
96
|
+
'success',
|
|
97
|
+
],
|
|
98
|
+
[/(^|_)(sent|open|progress|received|in|scheduled|running|purchase|ordered)($|_)/, 'info'],
|
|
99
|
+
];
|
|
100
|
+
export function toneOf(value) {
|
|
101
|
+
const v = value.toLowerCase();
|
|
102
|
+
return MEANINGS.find(([pattern]) => pattern.test(v))?.[1] ?? 'neutral';
|
|
103
|
+
}
|
|
84
104
|
function BadgeWidget({ value, field }) {
|
|
85
105
|
if (value === null || value === undefined || value === '')
|
|
86
106
|
return null;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const label = options[index]?.label ?? text(value);
|
|
90
|
-
return _jsx(Badge, { tone: TONES[Math.max(index, 0) % TONES.length] ?? 'neutral', children: label });
|
|
107
|
+
const label = (field.options ?? []).find((o) => o.value === value)?.label ?? text(value);
|
|
108
|
+
return _jsx(Badge, { tone: toneOf(text(value)), children: label });
|
|
91
109
|
}
|
|
92
110
|
/** Display names a list or kanban loaded in one batch per model, so cells do not query one by one. */
|
|
93
111
|
export const PrefetchedNames = createContext(new Map());
|
|
@@ -166,7 +184,7 @@ function Many2oneWidget({ id, value, readonly, onChange, field, invalid, }) {
|
|
|
166
184
|
const label = current === null ? '' : (names.get(current) ?? `#${current}`);
|
|
167
185
|
if (onChange === undefined || readonly)
|
|
168
186
|
return _jsx("span", { children: label });
|
|
169
|
-
return (_jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx("button", { id: id, type: "button", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": invalid === true ? true : undefined, className: "flex h-
|
|
187
|
+
return (_jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx("button", { id: id, type: "button", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": invalid === true ? true : undefined, className: "flex h-[var(--control-h)] w-full items-center justify-between rounded-md border border-input bg-surface px-3 text-start text-sm aria-invalid:border-danger", children: _jsx("span", { className: cn(current === null && 'text-muted-foreground'), children: current === null ? t('views.choose') : label }) }) }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: "start", sideOffset: 4, className: "z-50 w-[var(--radix-popover-trigger-width)] rounded-md border bg-surface p-1 shadow-lg", children: [_jsx(Input, { autoFocus: true, "aria-label": field.label, placeholder: t('search.placeholder'), value: q, onChange: (e) => setQ(e.target.value) }), _jsxs("div", { id: listId, role: "listbox", "aria-label": field.label, className: "mt-1 max-h-60 overflow-auto", children: [field.required ? null : (_jsx("button", { type: "button", role: "option", "aria-selected": current === null, className: "w-full rounded-sm px-2 py-1.5 text-start text-sm text-muted-foreground hover:bg-muted", onClick: () => {
|
|
170
188
|
onChange(null);
|
|
171
189
|
setOpen(false);
|
|
172
190
|
}, children: t('views.none') })), (results.data ?? []).map((r) => (_jsx("button", { type: "button", role: "option", "aria-selected": r.id === current, className: "w-full rounded-sm px-2 py-1.5 text-start text-sm hover:bg-muted", onClick: () => {
|
package/dist/views/types.d.ts
CHANGED
|
@@ -96,6 +96,13 @@ export interface FormView extends ViewBase {
|
|
|
96
96
|
readonly readonlyWhen?: Condition;
|
|
97
97
|
/** Messages and activities (RFC 0006). */
|
|
98
98
|
readonly chatter?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* The key facts under the title (RFC 0018 §4.2): the customer, the date, the total. Default: the first many2one,
|
|
101
|
+
* date and monetary fields of the form.
|
|
102
|
+
*/
|
|
103
|
+
readonly subtitle?: readonly string[];
|
|
104
|
+
/** The fields of the right rail's Summary (RFC 0018 §4.2). Default: the group named `totals`. */
|
|
105
|
+
readonly summary?: readonly string[];
|
|
99
106
|
}
|
|
100
107
|
export interface KanbanCard {
|
|
101
108
|
readonly title: string;
|
package/dist/views/validate.js
CHANGED
|
@@ -58,6 +58,10 @@ export function validateView(view, models) {
|
|
|
58
58
|
if (node.kind === 'button' && !info.transitions.includes(node.transition))
|
|
59
59
|
problems.push(`unknown transition '${node.transition}' (has ${info.transitions.join(', ') || 'none'})`);
|
|
60
60
|
}
|
|
61
|
+
if (view.type === 'form')
|
|
62
|
+
for (const key of [...(view.subtitle ?? []), ...(view.summary ?? [])])
|
|
63
|
+
if (!info.fields.has(key))
|
|
64
|
+
problems.push(`unknown field '${key}' in the subtitle or summary`);
|
|
61
65
|
if (view.type === 'form' && view.header.statusbar !== undefined) {
|
|
62
66
|
const kind = info.fields.get(view.header.statusbar)?.kind;
|
|
63
67
|
if (kind !== 'state' && kind !== 'selection')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shutro-erp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Shutro ERP (Kosh): a modern ERP framework on TypeScript, PostgreSQL and the Shutro ORM — typed end to end, permissions enforced on the server, metadata-driven views, reporting, documents and a chatter on every record.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"erp",
|
|
@@ -289,7 +289,7 @@
|
|
|
289
289
|
"postal-mime": "^4.1.4",
|
|
290
290
|
"radix-ui": "^1.8.1",
|
|
291
291
|
"sanitize-html": "^2.18.1",
|
|
292
|
-
"shutro": "^0.3.
|
|
292
|
+
"shutro": "^0.3.3",
|
|
293
293
|
"tailwind-merge": "^3.7.0",
|
|
294
294
|
"web-push": "^3.6.7"
|
|
295
295
|
},
|