shutro-erp 0.3.3 → 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 +128 -1
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +128 -1
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +128 -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 +128 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +128 -1
- 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 +128 -1
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +128 -1
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +128 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +128 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +128 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +128 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +128 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +128 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +128 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +128 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +128 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +128 -1
- package/dist/server/auth/base.js +76 -0
- package/dist/server/auth/oauth.d.ts +21 -0
- package/dist/server/auth/oauth.js +579 -0
- package/dist/server/auth/service.d.ts +4 -0
- package/dist/server/auth/service.js +17 -0
- package/dist/server/identity.d.ts +2 -0
- package/dist/server/images.d.ts +12 -0
- package/dist/server/images.js +139 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.js +9 -1
- package/dist/settings/company.js +3 -1
- package/dist/settings/people.js +3 -1
- package/dist/settings/web/CompaniesPage.js +2 -2
- package/dist/settings/web/CompanyPage.js +13 -2
- package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
- package/dist/settings/web/ConnectedAppsPage.js +33 -0
- package/dist/settings/web/OverviewPage.js +12 -1
- package/dist/settings/web/PeoplePage.js +3 -12
- package/dist/settings/web/ProfilePage.js +9 -7
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +5 -0
- package/dist/settings/web/messages.js +28 -0
- package/dist/ui/api.d.ts +4 -0
- package/dist/ui/app.js +49 -16
- 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/i18n/messages.d.ts +30 -0
- package/dist/ui/i18n/messages.js +60 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -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/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.js +15 -22
- package/dist/ui/views/ListView.js +49 -22
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Saved views as tabs (RFC 0018 §4.3): above the search, *All* and the saved searches of this model and view type —
|
|
4
|
+
* the user's own and the shared ones —, the one matching the current search marked. A tab applies its search; more
|
|
5
|
+
* than a handful go into a *More* menu. Saving and the full list stay in the search bar's favorites menu; with
|
|
6
|
+
* nothing saved, there are no tabs.
|
|
7
|
+
*/
|
|
8
|
+
import { encodeSearch } from '../../search/index.js';
|
|
9
|
+
import { useQuery } from '@tanstack/react-query';
|
|
10
|
+
import { ChevronDown, Users } from 'lucide-react';
|
|
11
|
+
import { useTranslation } from 'react-i18next';
|
|
12
|
+
import { Button, Menu, MenuContent, MenuItem, MenuTrigger } from '../components/primitives.js';
|
|
13
|
+
import { useKosh } from '../context.js';
|
|
14
|
+
import { cn } from '../lib/cn.js';
|
|
15
|
+
/** Tabs shown before the rest go into *More*. */
|
|
16
|
+
const VISIBLE = 6;
|
|
17
|
+
/** The search without its order: a tab matches whatever the columns are sorted by. */
|
|
18
|
+
const key = (state) => encodeSearch({ ...state, order: [] });
|
|
19
|
+
export function ViewTabs({ model, viewType, state, onChange, }) {
|
|
20
|
+
const { api } = useKosh();
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
// The search bar's favorites query: one request, one cache.
|
|
23
|
+
const favorites = useQuery({
|
|
24
|
+
queryKey: ['saved-views', model],
|
|
25
|
+
queryFn: () => api.list('kosh.saved_view', {
|
|
26
|
+
filter: { targetModel: model },
|
|
27
|
+
fields: ['name', 'state', 'shared', 'viewType'],
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const views = (favorites.data?.items ?? []).filter((v) => v['viewType'] === viewType || v['viewType'] === null);
|
|
31
|
+
if (views.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const current = key(state);
|
|
34
|
+
const stateOf = (v) => v['state'] ?? {};
|
|
35
|
+
const active = views.find((v) => key(stateOf(v)) === current);
|
|
36
|
+
const tab = (label, selected, apply, shared = false) => (_jsxs(Button, { size: "sm", variant: "ghost", "aria-current": selected ? 'true' : undefined, onClick: apply, className: cn('h-8 rounded-none border-b-2 border-transparent px-3 text-muted-foreground hover:text-foreground', selected && 'border-primary font-medium text-foreground'), children: [label, shared ? _jsx(Users, { className: "size-3.5 opacity-60", "aria-label": t('search.shared') }) : null] }));
|
|
37
|
+
const shown = views.slice(0, VISIBLE);
|
|
38
|
+
const more = views.slice(VISIBLE);
|
|
39
|
+
return (_jsxs("nav", { "aria-label": t('search.savedViews'), className: "-mb-1 flex items-end gap-1 border-b", children: [tab(t('search.allRecords'), current === '' && active === undefined, () => onChange({})), shown.map((v) => (_jsx("span", { className: "contents", children: tab(String(v['name']), v === active, () => onChange(stateOf(v)), v['shared'] === true) }, v.id))), more.length === 0 ? null : (_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", variant: "ghost", className: cn('h-8 rounded-none border-b-2 border-transparent px-3 text-muted-foreground', active !== undefined && more.includes(active) && 'border-primary text-foreground'), children: [active !== undefined && more.includes(active)
|
|
40
|
+
? String(active['name'])
|
|
41
|
+
: t('search.moreViews', { count: more.length }), _jsx(ChevronDown, { className: "size-3.5", "aria-hidden": true })] }) }), _jsx(MenuContent, { align: "start", children: more.map((v) => (_jsx(MenuItem, { onSelect: () => onChange(stateOf(v)), children: String(v['name']) }, v.id))) })] }))] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The OAuth consent page (RFC 0017 §5.2), at `/oauth/authorize`: an app asks to act for the signed-in user. It names
|
|
4
|
+
* the app, where it sends the user back, and what it may do in words; *Allow* or *Cancel* — either returns to the
|
|
5
|
+
* app. The server checks the request (registered redirect, allowed scopes, PKCE) before anything is shown; signed out,
|
|
6
|
+
* the person signs in and comes back here.
|
|
7
|
+
*/
|
|
8
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { ExternalLink, ShieldCheck } from 'lucide-react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import { ApiError } from '../api.js';
|
|
12
|
+
import { Alert, Button, Card, Spinner } from '../components/primitives.js';
|
|
13
|
+
export function Consent({ api }) {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const search = window.location.search;
|
|
16
|
+
const params = Object.fromEntries(new URLSearchParams(search));
|
|
17
|
+
const me = useQuery({ queryKey: ['me'], queryFn: () => api.me(), retry: false });
|
|
18
|
+
const screen = useQuery({
|
|
19
|
+
queryKey: ['oauth', 'consent', search],
|
|
20
|
+
queryFn: () => api.call('GET', `/api/v1/oauth/authorize${search}`),
|
|
21
|
+
enabled: me.isSuccess,
|
|
22
|
+
retry: false,
|
|
23
|
+
});
|
|
24
|
+
const decide = useMutation({
|
|
25
|
+
mutationFn: (decision) => api.call('POST', '/api/v1/oauth/authorize', { ...params, decision }),
|
|
26
|
+
onSuccess: ({ redirect }) => window.location.assign(redirect),
|
|
27
|
+
});
|
|
28
|
+
if (me.error instanceof ApiError && me.error.status === 401) {
|
|
29
|
+
// Signed out: sign in, then back here with the same request.
|
|
30
|
+
window.location.assign(`/login?next=${encodeURIComponent(`/oauth/authorize${search}`)}`);
|
|
31
|
+
return _jsx(Spinner, { label: t('app.loading') });
|
|
32
|
+
}
|
|
33
|
+
const error = me.error ?? screen.error ?? decide.error;
|
|
34
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center bg-background p-4", children: _jsx(Card, { className: "grid w-full max-w-md gap-5 p-6", children: error !== null ? (_jsx(Alert, { children: error instanceof ApiError ? error.message : String(error) })) : screen.data === undefined || me.data === undefined ? (_jsx(Spinner, { label: t('app.loading') })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-1", children: [_jsx("span", { "aria-hidden": true, className: "mb-2 grid size-10 place-items-center rounded-lg bg-accent text-accent-foreground", children: _jsx(ShieldCheck, { className: "size-5" }) }), _jsx("h1", { className: "text-xl font-semibold", children: t('oauth.title', { app: screen.data.app.name }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('oauth.as', { name: me.data.name, login: me.data.login }) })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx("h2", { className: "text-sm font-medium", children: t('oauth.may') }), _jsx("ul", { className: "grid gap-1.5 rounded-md border p-3 text-sm", children: screen.data.scopes.map((s) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx(ShieldCheck, { className: "size-4 text-success", "aria-hidden": true }), s.label] }, s.scope))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('oauth.limits') })] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [t('oauth.back', { host: screen.data.redirectHost }), screen.data.app.homepage === null ? null : (_jsxs(_Fragment, { children: [' ', _jsxs("a", { className: "inline-flex items-center gap-1 text-primary underline", href: screen.data.app.homepage, target: "_blank", rel: "noreferrer noopener", children: [t('oauth.homepage'), _jsx(ExternalLink, { className: "size-3", "aria-hidden": true })] })] }))] }), screen.data.alreadyAllowed ? _jsx(Alert, { tone: "info", children: t('oauth.already') }) : null, _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { busy: decide.isPending && decide.variables === 'deny', onClick: () => decide.mutate('deny'), children: t('oauth.cancel') }), _jsx(Button, { variant: "primary", busy: decide.isPending && decide.variables === 'allow', onClick: () => decide.mutate('allow'), children: t('oauth.allow') })] })] })) }) }));
|
|
35
|
+
}
|
package/dist/ui/shell/Home.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Today, the home page (RFC 0010 §3.8, RFC 0018 §4.4): a greeting with the date, the palette's search box, what needs
|
|
4
|
+
* the user — each approval waiting for them by record, the activities due, unread mentions — as a list with its
|
|
5
|
+
* action, recently opened records, the saved views they keep (lists, pivots, charts: one click back to them), and the
|
|
6
|
+
* apps. Everything comes from routes the server already filters per user; apps that are not installed (Permissions,
|
|
7
|
+
* Messaging, Notifications) simply contribute nothing.
|
|
6
8
|
*/
|
|
9
|
+
import { encodeSearch } from '../../search/index.js';
|
|
7
10
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
11
|
import { Link, useNavigate } from '@tanstack/react-router';
|
|
9
|
-
import { CalendarClock, CircleCheckBig, History, Search } from 'lucide-react';
|
|
12
|
+
import { AtSign, BarChart3, CalendarClock, CircleCheckBig, History, List, Search, ShieldCheck, Star, Table2, } from 'lucide-react';
|
|
10
13
|
import { useTranslation } from 'react-i18next';
|
|
11
14
|
import { AppIcon } from '../components/AppIcon.js';
|
|
12
15
|
import { Badge, Button, Card, Skeleton } from '../components/primitives.js';
|
|
@@ -22,6 +25,15 @@ const TINTS = [
|
|
|
22
25
|
'bg-primary/10 text-primary',
|
|
23
26
|
'bg-danger/10 text-danger',
|
|
24
27
|
];
|
|
28
|
+
const VIEW_ICONS = {
|
|
29
|
+
pivot: Table2,
|
|
30
|
+
graph: BarChart3,
|
|
31
|
+
chart: BarChart3,
|
|
32
|
+
};
|
|
33
|
+
/** One row of "Needs your attention": an icon, what it is, and its actions. */
|
|
34
|
+
function Item({ icon, title, detail, children, }) {
|
|
35
|
+
return (_jsxs("li", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx("span", { "aria-hidden": true, className: "grid size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: icon }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: title }), detail === undefined ? null : (_jsx("p", { className: "truncate text-xs text-muted-foreground", children: detail }))] }), _jsx("div", { className: "flex shrink-0 items-center gap-1", children: children })] }));
|
|
36
|
+
}
|
|
25
37
|
export function Home() {
|
|
26
38
|
const { api, uid, format } = useKosh();
|
|
27
39
|
const { t } = useTranslation();
|
|
@@ -34,10 +46,25 @@ export function Home() {
|
|
|
34
46
|
// Each is a module's route: absent (404) when the module is not installed, so an absent list is empty.
|
|
35
47
|
const approvals = useQuery({
|
|
36
48
|
queryKey: ['home', 'approvals'],
|
|
37
|
-
queryFn: () =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
queryFn: async () => {
|
|
50
|
+
const requests = await api
|
|
51
|
+
.call('GET', '/api/v1/approvals/inbox')
|
|
52
|
+
.then((r) => r.requests)
|
|
53
|
+
.catch(() => []);
|
|
54
|
+
// The records' names, one request per model.
|
|
55
|
+
const names = new Map();
|
|
56
|
+
for (const model of new Set(requests.map((r) => r.targetModel)))
|
|
57
|
+
for (const n of await api
|
|
58
|
+
.names(model, {
|
|
59
|
+
ids: requests.filter((r) => r.targetModel === model).map((r) => r.targetId),
|
|
60
|
+
})
|
|
61
|
+
.catch(() => []))
|
|
62
|
+
names.set(`${model}:${n.id}`, n.displayName);
|
|
63
|
+
return requests.map((r) => ({
|
|
64
|
+
...r,
|
|
65
|
+
record: names.get(`${r.targetModel}:${r.targetId}`) ?? `#${r.targetId}`,
|
|
66
|
+
}));
|
|
67
|
+
},
|
|
41
68
|
});
|
|
42
69
|
const activities = useQuery({
|
|
43
70
|
queryKey: ['home', 'activities'],
|
|
@@ -46,13 +73,38 @@ export function Home() {
|
|
|
46
73
|
.then((r) => r.activities)
|
|
47
74
|
.catch(() => []),
|
|
48
75
|
});
|
|
76
|
+
const mentions = useQuery({
|
|
77
|
+
queryKey: ['home', 'mentions'],
|
|
78
|
+
queryFn: () => api
|
|
79
|
+
.call('GET', '/api/v1/notifications?unread=true&limit=50')
|
|
80
|
+
.then((r) => r.items.filter((n) => n.kind === 'mention'))
|
|
81
|
+
.catch(() => []),
|
|
82
|
+
});
|
|
83
|
+
const saved = useQuery({
|
|
84
|
+
queryKey: ['home', 'saved-views'],
|
|
85
|
+
queryFn: () => api
|
|
86
|
+
.list('kosh.saved_view', {
|
|
87
|
+
fields: ['name', 'targetModel', 'viewType', 'state'],
|
|
88
|
+
limit: 12,
|
|
89
|
+
})
|
|
90
|
+
.then((r) => r.items)
|
|
91
|
+
.catch(() => []),
|
|
92
|
+
});
|
|
49
93
|
const done = useMutation({
|
|
50
94
|
mutationFn: (id) => api.call('POST', `/api/v1/activities/${id}/done`, {}),
|
|
51
95
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['home', 'activities'] }),
|
|
52
96
|
});
|
|
53
|
-
const
|
|
97
|
+
const read = useMutation({
|
|
98
|
+
mutationFn: (id) => api.call('POST', `/api/v1/notifications/${id}/read`, {}),
|
|
99
|
+
onSuccess: () => {
|
|
100
|
+
void queryClient.invalidateQueries({ queryKey: ['home', 'mentions'] });
|
|
101
|
+
void queryClient.invalidateQueries({ queryKey: ['notifications'] });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const now = new Date();
|
|
105
|
+
const hour = now.getHours();
|
|
54
106
|
const greeting = hour < 12 ? 'morning' : hour < 18 ? 'afternoon' : 'evening';
|
|
55
|
-
const today =
|
|
107
|
+
const today = now.toISOString().slice(0, 10);
|
|
56
108
|
const due = (activities.data ?? []).filter((a) => a.deadline <= today);
|
|
57
109
|
const open = (model, id) => {
|
|
58
110
|
const route = routes.data?.[model];
|
|
@@ -63,13 +115,30 @@ export function Home() {
|
|
|
63
115
|
params: { app: route.app, action: route.action, id: String(id) },
|
|
64
116
|
});
|
|
65
117
|
};
|
|
118
|
+
const openSaved = (view) => {
|
|
119
|
+
const route = routes.data?.[view.targetModel];
|
|
120
|
+
if (route?.app === undefined)
|
|
121
|
+
return;
|
|
122
|
+
const q = encodeSearch((view.state ?? {}));
|
|
123
|
+
void navigate({
|
|
124
|
+
to: '/$app/a/$action',
|
|
125
|
+
params: { app: route.app, action: route.action },
|
|
126
|
+
search: { view: view.viewType, ...(q === '' ? {} : { q }) },
|
|
127
|
+
});
|
|
128
|
+
};
|
|
66
129
|
const categories = [...new Set((apps.data ?? []).map((a) => a.category ?? ''))].sort((a, b) => a.localeCompare(b));
|
|
67
130
|
const tint = (a) => TINTS[categories.indexOf(a.category ?? '') % TINTS.length];
|
|
68
131
|
const recent = recentRecords(uid);
|
|
69
|
-
const waiting = (approvals.data?.length ?? 0) + due.length;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
132
|
+
const waiting = (approvals.data?.length ?? 0) + due.length + (mentions.data?.length ?? 0);
|
|
133
|
+
const views = (saved.data ?? []).filter((v) => routes.data?.[v.targetModel]?.app !== undefined);
|
|
134
|
+
return (_jsxs("div", { className: "mx-auto grid max-w-5xl gap-8 py-4", children: [_jsxs("header", { className: "grid gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: t('home.today', { date: format.date(today) }) }), _jsx("h1", { className: "text-2xl font-semibold", children: me.data === undefined ? (_jsx(Skeleton, { className: "h-8 w-64" })) : (t(`home.${greeting}`, { name: me.data.name.split(' ')[0] })) })] }), _jsxs("button", { type: "button", onClick: openPalette, className: "flex h-12 w-full items-center gap-3 rounded-xl border bg-surface px-4 text-start text-muted-foreground shadow-sm hover:bg-muted", children: [_jsx(Search, { className: "size-5", "aria-hidden": true }), _jsx("span", { className: "flex-1", children: t('home.search') }), _jsx("kbd", { className: "rounded border px-1.5 text-xs", children: "Ctrl K" })] })] }), _jsxs("section", { "aria-labelledby": "home-attention", className: "grid gap-3", children: [_jsxs("h2", { id: "home-attention", className: "flex items-center gap-2 text-sm font-semibold text-muted-foreground", children: [t('home.attention'), waiting === 0 ? null : _jsx(Badge, { tone: "info", children: waiting })] }), approvals.isPending || activities.isPending || mentions.isPending ? (_jsx(Skeleton, { className: "h-16" })) : waiting === 0 ? (_jsxs(Card, { className: "flex items-center gap-3 p-4 text-sm text-muted-foreground", children: [_jsx(CircleCheckBig, { className: "size-5 text-success", "aria-hidden": true }), t('home.nothing')] })) : (_jsx(Card, { className: "p-0", children: _jsxs("ul", { className: "divide-y", children: [(approvals.data ?? []).map((a) => (_jsx(Item, { icon: _jsx(ShieldCheck, { className: "size-4" }), title: t('home.approve', { record: a.record }), detail: a.levelName, children: _jsx(Button, { size: "sm", onClick: () => open(a.targetModel, a.targetId), children: t('home.review') }) }, `approval-${a.id}`))), due.map((a) => (_jsxs(Item, { icon: _jsx(CalendarClock, { className: "size-4" }), title: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: a.summary ?? a.type.name }), a.deadline < today ? (_jsx(Badge, { tone: "danger", children: t('home.overdue') })) : (_jsx(Badge, { tone: "warning", children: t('home.dueToday') }))] }), detail: `${a.type.name} · ${a.record}`, children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => open(a.model, a.resId), children: t('home.open') }), _jsx(Button, { size: "sm", busy: done.isPending && done.variables === a.id, onClick: () => done.mutate(a.id), children: t('home.done') })] }, `activity-${a.id}`))), (mentions.data ?? []).map((m) => (_jsx(Item, { icon: _jsx(AtSign, { className: "size-4" }), title: t('home.mentioned', {
|
|
135
|
+
actor: m.params.actor ?? '',
|
|
136
|
+
record: m.params.record ?? '',
|
|
137
|
+
}), detail: m.params.preview ?? format.datetime(m.createdAt), children: _jsx(Button, { size: "sm", onClick: () => {
|
|
138
|
+
read.mutate(m.id);
|
|
139
|
+
open(m.model, m.resId);
|
|
140
|
+
}, children: t('home.open') }) }, `mention-${m.id}`)))] }) }))] }), recent.length === 0 ? null : (_jsxs("section", { "aria-labelledby": "home-recent", className: "grid gap-3", children: [_jsx("h2", { id: "home-recent", className: "text-sm font-semibold text-muted-foreground", children: t('home.recent') }), _jsx("ul", { className: "flex flex-wrap gap-2", children: recent.slice(0, 8).map((r) => (_jsx("li", { children: _jsxs(Link, { to: "/$app/a/$action/$id", params: { app: r.app, action: r.action, id: String(r.id) }, className: "flex items-center gap-2 rounded-full border bg-surface px-3 py-1.5 text-sm hover:bg-muted", children: [_jsx(History, { className: "size-3.5 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "max-w-56 truncate", children: r.title }), _jsx("span", { className: "text-xs text-muted-foreground", children: format.date(new Date(r.at).toISOString().slice(0, 10)) })] }) }, `${r.model}:${r.id}`))) })] })), views.length === 0 ? null : (_jsxs("section", { "aria-labelledby": "home-saved", className: "grid gap-3", children: [_jsx("h2", { id: "home-saved", className: "text-sm font-semibold text-muted-foreground", children: t('home.saved') }), _jsx("ul", { className: "grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] gap-2", children: views.map((v) => {
|
|
141
|
+
const Icon = VIEW_ICONS[v.viewType] ?? List;
|
|
142
|
+
return (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => openSaved(v), className: "flex w-full items-center gap-2 rounded-lg border bg-surface px-3 py-2 text-start text-sm hover:bg-muted", children: [_jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: v.name }), _jsx(Star, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true })] }) }, v.id));
|
|
143
|
+
}) })] })), _jsxs("section", { "aria-labelledby": "apps-title", className: "grid gap-3", children: [_jsx("h2", { id: "apps-title", className: "text-sm font-semibold text-muted-foreground", children: t('home.apps') }), apps.isPending ? (_jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(11rem,1fr))] gap-3", children: [0, 1, 2, 3].map((i) => (_jsx(Skeleton, { className: "h-16" }, i))) })) : (apps.data ?? []).length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: t('shell.noApps') })) : (_jsx("ul", { className: "grid grid-cols-[repeat(auto-fill,minmax(13rem,1fr))] gap-2", children: (apps.data ?? []).map((a) => (_jsx("li", { children: _jsx(Link, { to: "/$app", params: { app: a.code }, className: "block h-full", children: _jsxs(Card, { className: "flex h-full items-center gap-3 p-3 hover:bg-muted", children: [_jsx(AppIcon, { name: a.icon ?? a.code, className: cn('size-9', tint(a)) }), _jsxs("div", { className: "grid min-w-0 gap-0.5", children: [_jsx("span", { className: "truncate font-medium", children: a.label }), a.summary === undefined ? null : (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: a.summary }))] })] }) }) }, a.code))) }))] })] }));
|
|
75
144
|
}
|
package/dist/ui/shell/Sidebar.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* The Workspace sidebar (RFC 0018 §4.1): the company at the top, search, Home, then the
|
|
4
|
-
*
|
|
5
|
-
* to an icon rail (`Ctrl+B`, remembered per browser) and becomes a drawer on small screens.
|
|
6
|
-
* apps and menus the user sees; the sidebar shows them.
|
|
3
|
+
* The Workspace sidebar (RFC 0018 §4.1), shown inside an app: the company at the top, search, Home, then the open
|
|
4
|
+
* app — its name switches to another app — with only its own sections, and the user at the bottom. The home page has
|
|
5
|
+
* no sidebar. It collapses to an icon rail (`Ctrl+B`, remembered per browser) and becomes a drawer on small screens.
|
|
6
|
+
* The server decides which apps and menus the user sees; the sidebar shows them.
|
|
7
7
|
*/
|
|
8
8
|
import { useQuery } from '@tanstack/react-query';
|
|
9
9
|
import { Link, useNavigate } from '@tanstack/react-router';
|
|
10
|
-
import {
|
|
10
|
+
import { ChevronsUpDown, House, PanelLeftClose, PanelLeftOpen, Search, X } from 'lucide-react';
|
|
11
11
|
import { Dialog } from 'radix-ui';
|
|
12
|
-
import {
|
|
12
|
+
import { useState } from 'react';
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
14
|
import { AppIcon } from '../components/AppIcon.js';
|
|
15
|
-
import { Button } from '../components/primitives.js';
|
|
15
|
+
import { Button, Menu, MenuCheckboxItem, MenuContent, MenuLabel, MenuTrigger, } from '../components/primitives.js';
|
|
16
16
|
import { cn } from '../lib/cn.js';
|
|
17
17
|
const SIDEBAR_KEY = 'kosh.sidebar';
|
|
18
18
|
/** Whether the sidebar is collapsed to its rail, remembered per browser. */
|
|
@@ -56,26 +56,19 @@ function AppSections({ api, app, company, onNavigate, }) {
|
|
|
56
56
|
queryKey: ['menus', app, company],
|
|
57
57
|
queryFn: () => api.menus(app),
|
|
58
58
|
});
|
|
59
|
-
return (_jsx("ul", { className: "
|
|
59
|
+
return (_jsx("ul", { className: "grid gap-0.5 pt-1 pb-2", children: entriesOf(menus.data ?? []).map((node) => node.children.length > 0 ? (_jsxs("li", { className: "pt-1", children: [_jsx("p", { className: "px-2 pb-0.5 text-xs font-medium tracking-wide text-muted-foreground uppercase", children: node.label }), _jsx("ul", { className: "grid gap-0.5", children: node.children.map((child) => (_jsx("li", { children: _jsx(SectionLink, { app: app, node: child, onNavigate: onNavigate }) }, child.id))) })] }, node.id)) : (_jsx("li", { children: _jsx(SectionLink, { app: app, node: node, onNavigate: onNavigate }) }, node.id))) }));
|
|
60
60
|
}
|
|
61
61
|
export function Sidebar({ api, apps, current, company, collapsed, onCollapse, onSearch, top, bottom, onNavigate, }) {
|
|
62
62
|
const { t } = useTranslation();
|
|
63
63
|
const navigate = useNavigate();
|
|
64
|
-
const
|
|
65
|
-
useEffect(() => setOpen(current), [current]);
|
|
64
|
+
const app = apps.find((a) => a.code === current);
|
|
66
65
|
const item = 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm hover:bg-muted focus-visible:outline-2';
|
|
67
|
-
return (_jsxs("div", { className: "flex h-full flex-col gap-1 p-2", children: [_jsx("div", { className: cn('pb-1', collapsed && 'flex justify-center'), children: top }), _jsxs("button", { type: "button", className: cn(item, 'text-muted-foreground', collapsed && 'justify-center'), onClick: onSearch, "aria-keyshortcuts": "Control+K Meta+K", "aria-label": collapsed ? t('palette.button') : undefined, title: collapsed ? t('palette.button') : undefined, children: [_jsx(Search, { className: "size-4 shrink-0", "aria-hidden": true }), collapsed ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-start", children: t('palette.button') }), _jsx("kbd", { className: "rounded border bg-surface px-1 text-[0.65rem]", children: "Ctrl K" })] }))] }), _jsxs(Link, { to: "/", onClick: onNavigate, className: cn(item, collapsed && 'justify-center'), activeOptions: { exact: true }, activeProps: { 'aria-current': 'page', className: 'bg-muted font-medium' }, "aria-label": collapsed ? t('shell.home') : undefined, title: collapsed ? t('shell.home') : undefined, children: [_jsx(House, { className: "size-4 shrink-0", "aria-hidden": true }), collapsed ? null : t('shell.home')] }), _jsx("nav", { "aria-label": t('shell.menu'), className: "-mx-2 mt-2 flex-1 overflow-y-auto border-t px-2 pt-2", children: _jsx("
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
onNavigate?.();
|
|
74
|
-
}
|
|
75
|
-
else
|
|
76
|
-
setOpen(expanded ? undefined : a.code);
|
|
77
|
-
}, children: [_jsx(AppIcon, { name: a.icon ?? a.code, className: "size-6 rounded-md [&_svg]:size-3.5" }), collapsed ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 truncate text-start", children: a.label }), _jsx(ChevronDown, { className: cn('size-3.5 text-muted-foreground transition-transform', !expanded && '-rotate-90 rtl:rotate-90'), "aria-hidden": true })] }))] }), expanded ? (_jsx(AppSections, { api: api, app: a.code, company: company, onNavigate: onNavigate })) : null] }, a.code));
|
|
78
|
-
}) }) }), _jsxs("div", { className: cn('flex items-center gap-1 border-t pt-2', collapsed && 'flex-col'), children: [_jsx("div", { className: "min-w-0 flex-1", children: bottom }), onCollapse === undefined ? null : (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": collapsed ? t('shell.expandSidebar') : t('shell.collapseSidebar'), title: `${collapsed ? t('shell.expandSidebar') : t('shell.collapseSidebar')} (Ctrl B)`, "aria-keyshortcuts": "Control+B Meta+B", onClick: () => onCollapse(!collapsed), children: collapsed ? (_jsx(PanelLeftOpen, { className: "rtl:rotate-180", "aria-hidden": true })) : (_jsx(PanelLeftClose, { className: "rtl:rotate-180", "aria-hidden": true })) }))] })] }));
|
|
66
|
+
return (_jsxs("div", { className: "flex h-full flex-col gap-1 p-2", children: [_jsx("div", { className: cn('pb-1', collapsed && 'flex justify-center'), children: top }), _jsxs("button", { type: "button", className: cn(item, 'text-muted-foreground', collapsed && 'justify-center'), onClick: onSearch, "aria-keyshortcuts": "Control+K Meta+K", "aria-label": collapsed ? t('palette.button') : undefined, title: collapsed ? t('palette.button') : undefined, children: [_jsx(Search, { className: "size-4 shrink-0", "aria-hidden": true }), collapsed ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-start", children: t('palette.button') }), _jsx("kbd", { className: "rounded border bg-surface px-1 text-[0.65rem]", children: "Ctrl K" })] }))] }), _jsxs(Link, { to: "/", onClick: onNavigate, className: cn(item, collapsed && 'justify-center'), activeOptions: { exact: true }, activeProps: { 'aria-current': 'page', className: 'bg-muted font-medium' }, "aria-label": collapsed ? t('shell.home') : undefined, title: collapsed ? t('shell.home') : undefined, children: [_jsx(House, { className: "size-4 shrink-0", "aria-hidden": true }), collapsed ? null : t('shell.home')] }), _jsx("nav", { "aria-label": t('shell.menu'), className: "-mx-2 mt-2 flex-1 overflow-y-auto border-t px-2 pt-2", children: app === undefined ? null : (_jsxs(_Fragment, { children: [_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": t('shell.appNamed', { app: app.label }), title: collapsed ? app.label : undefined, className: cn(item, 'font-semibold', collapsed && 'justify-center'), children: [_jsx(AppIcon, { name: app.icon ?? app.code, className: "size-6 rounded-md [&_svg]:size-3.5" }), collapsed ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 truncate text-start", children: app.label }), _jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground", "aria-hidden": true })] }))] }) }), _jsxs(MenuContent, { align: "start", className: "w-60", children: [_jsx(MenuLabel, { children: t('shell.apps') }), apps.map((a) => (_jsxs(MenuCheckboxItem, { checked: a.code === current, onCheckedChange: () => {
|
|
67
|
+
if (a.code === current)
|
|
68
|
+
return;
|
|
69
|
+
void navigate({ to: '/$app', params: { app: a.code } });
|
|
70
|
+
onNavigate?.();
|
|
71
|
+
}, children: [_jsx(AppIcon, { name: a.icon ?? a.code, className: "size-5 rounded [&_svg]:size-3" }), a.label] }, a.code)))] })] }), collapsed ? null : (_jsx(AppSections, { api: api, app: app.code, company: company, onNavigate: onNavigate }))] })) }), _jsxs("div", { className: cn('flex items-center gap-1 border-t pt-2', collapsed && 'flex-col'), children: [_jsx("div", { className: "min-w-0 flex-1", children: bottom }), onCollapse === undefined ? null : (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": collapsed ? t('shell.expandSidebar') : t('shell.collapseSidebar'), title: `${collapsed ? t('shell.expandSidebar') : t('shell.collapseSidebar')} (Ctrl B)`, "aria-keyshortcuts": "Control+B Meta+B", onClick: () => onCollapse(!collapsed), children: collapsed ? (_jsx(PanelLeftOpen, { className: "rtl:rotate-180", "aria-hidden": true })) : (_jsx(PanelLeftClose, { className: "rtl:rotate-180", "aria-hidden": true })) }))] })] }));
|
|
79
72
|
}
|
|
80
73
|
/** The sidebar as a drawer on small screens. */
|
|
81
74
|
export function SidebarDrawer({ open, onOpenChange, children, }) {
|
|
@@ -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) {
|
|
@@ -814,24 +839,26 @@ export function ListView(props) {
|
|
|
814
839
|
tone: 'danger',
|
|
815
840
|
}),
|
|
816
841
|
});
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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] }));
|
|
835
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', {
|
|
836
863
|
from: offset + 1,
|
|
837
864
|
to: Math.min(offset + pageSize, count),
|
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",
|