@nocobase/app-template-default 1.0.0-beta.67 → 1.0.0-beta.69
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/.agents/rules/components.md +2 -2
- package/.agents/rules/shell.md +5 -1
- package/AGENTS.md +5 -3
- package/Dockerfile.dockerignore +0 -1
- package/README.MD +3 -1
- package/client/app.ts +15 -0
- package/client/components/data-table/column-header.tsx +91 -0
- package/client/components/data-table/data-table.tsx +216 -0
- package/client/components/data-table/index.ts +13 -0
- package/client/components/data-table/pagination.tsx +165 -0
- package/client/components/data-table/view-options.tsx +77 -0
- package/client/components/page-header.tsx +1 -1
- package/client/components/permission-editor.tsx +589 -0
- package/client/components/permission-filter.tsx +455 -0
- package/client/components/route-overlay.tsx +26 -13
- package/client/components/status-badge.tsx +7 -1
- package/client/components/ui/checkbox.tsx +25 -0
- package/client/components/ui/sidebar.tsx +1 -4
- package/client/components/ui/switch.tsx +30 -0
- package/client/components/ui/table.tsx +113 -0
- package/client/extensions/nocobase-inbox/inbox-item.tsx +44 -58
- package/client/extensions/nocobase-inbox/inbox-list.tsx +73 -75
- package/client/index.tsx +4 -0
- package/client/layouts/components/app-sidebar.tsx +6 -6
- package/client/layouts/components/layout-header.tsx +1 -1
- package/client/layouts/components/navigation-menu.tsx +223 -12
- package/client/layouts/components/sidebar-footer.tsx +1 -1
- package/client/layouts/components/user-menu.tsx +1 -1
- package/client/layouts/use-sidebar-preference.ts +62 -42
- package/client/lib/navigation-transform.ts +8 -0
- package/client/lib/scrollbar.ts +88 -0
- package/client/locales/system/en-US.ts +11 -0
- package/client/locales/system/settings-en-US.ts +141 -0
- package/client/locales/system/settings-zh-CN.ts +135 -0
- package/client/locales/system/zh-CN.ts +22 -12
- package/client/pages/auth/auth-page.css +52 -0
- package/client/pages/auth/login.tsx +1 -1
- package/client/pages/auth/reset-password.tsx +57 -14
- package/client/pages/auth/shared.tsx +26 -24
- package/client/pages/home/index.tsx +2 -2
- package/client/pages/settings/hooks.tsx +137 -0
- package/client/pages/settings/permission-filters.tsx +119 -0
- package/client/pages/settings/permissions-model.ts +260 -0
- package/client/pages/settings/role-form.tsx +426 -0
- package/client/pages/settings/roles.tsx +163 -0
- package/client/pages/settings/routes.ts +79 -0
- package/client/pages/settings/shared.tsx +119 -0
- package/client/pages/settings/user-detail.tsx +268 -0
- package/client/pages/settings/user-form.tsx +427 -0
- package/client/pages/settings/users.tsx +451 -0
- package/client/routing/app-router.tsx +4 -1
- package/client/routing/route-navigation.ts +10 -1
- package/client/routing/session-preparation.tsx +65 -0
- package/client/styles.css +257 -24
- package/client/theme/theme-settings.tsx +12 -7
- package/client/theme/themes/compact.css +9 -14
- package/client/theme/themes/default.css +7 -12
- package/package.json +26 -25
- package/server/app.ts +2 -0
- package/server/config/notification.ts +4 -7
- package/tests/components/app-layout-routes.test.tsx +14 -7
- package/tests/components/auth-i18n.test.tsx +57 -5
- package/tests/components/data-table.test.tsx +127 -0
- package/tests/components/layout-i18n.test.tsx +10 -3
- package/tests/components/navigation-menu.test.tsx +155 -16
- package/tests/components/permission-editor.test.tsx +477 -0
- package/tests/components/permission-filter-binding.test.tsx +150 -0
- package/tests/components/permission-filter.test.tsx +202 -0
- package/tests/components/settings.test.tsx +294 -0
- package/tests/components/sidebar-permissions.test.tsx +17 -19
- package/tests/components/sidebar-preference.test.tsx +209 -14
- package/tests/components/status-badge.test.tsx +22 -0
- package/tests/components/user-permissions.test.tsx +142 -0
- package/tests/fixtures/theme-tokens.tsx +9 -1
- package/tests/logic/asset-url-build.test.ts +0 -1
- package/tests/logic/client-routes.test.ts +40 -0
- package/tests/logic/client-shell.test.tsx +10 -2
- package/tests/logic/config-example.test.ts +30 -0
- package/tests/logic/dev-client-watch.test.ts +8 -1
- package/tests/logic/route-overlay.test.tsx +42 -0
- package/tests/logic/scrollbar.test.ts +88 -0
- package/tests/logic/session-preparation.test.tsx +223 -0
- package/tests/logic/settings-permissions.test.ts +168 -0
- package/tests/logic/theme-tokens.test.ts +1 -0
- package/vite.config.ts +0 -22
- package/vitest.config.ts +1 -0
- package/client/agent-annotations-host.ts +0 -25
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
|
|
4
|
+
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot='table-container'
|
|
8
|
+
className='relative w-full overflow-x-auto'
|
|
9
|
+
>
|
|
10
|
+
<table
|
|
11
|
+
data-slot='table'
|
|
12
|
+
className={cn('w-full caption-bottom text-sm', className)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
|
20
|
+
return (
|
|
21
|
+
<thead
|
|
22
|
+
data-slot='table-header'
|
|
23
|
+
className={cn('[&_tr]:border-b', className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
|
30
|
+
return (
|
|
31
|
+
<tbody
|
|
32
|
+
data-slot='table-body'
|
|
33
|
+
className={cn('[&_tr:last-child]:border-0', className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
|
40
|
+
return (
|
|
41
|
+
<tfoot
|
|
42
|
+
data-slot='table-footer'
|
|
43
|
+
className={cn(
|
|
44
|
+
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
|
53
|
+
return (
|
|
54
|
+
<tr
|
|
55
|
+
data-slot='table-row'
|
|
56
|
+
className={cn(
|
|
57
|
+
'border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted',
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
|
66
|
+
return (
|
|
67
|
+
<th
|
|
68
|
+
data-slot='table-head'
|
|
69
|
+
className={cn(
|
|
70
|
+
'h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0',
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
|
79
|
+
return (
|
|
80
|
+
<td
|
|
81
|
+
data-slot='table-cell'
|
|
82
|
+
className={cn(
|
|
83
|
+
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function TableCaption({
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
}: React.ComponentProps<'caption'>) {
|
|
95
|
+
return (
|
|
96
|
+
<caption
|
|
97
|
+
data-slot='table-caption'
|
|
98
|
+
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
Table,
|
|
106
|
+
TableHeader,
|
|
107
|
+
TableBody,
|
|
108
|
+
TableFooter,
|
|
109
|
+
TableHead,
|
|
110
|
+
TableRow,
|
|
111
|
+
TableCell,
|
|
112
|
+
TableCaption,
|
|
113
|
+
};
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CheckCircle2Icon,
|
|
3
|
-
ChevronRightIcon,
|
|
4
|
-
MoreHorizontalIcon,
|
|
5
|
-
} from 'lucide-react';
|
|
1
|
+
import { CheckCircle2Icon, MoreHorizontalIcon } from 'lucide-react';
|
|
6
2
|
import { createElement, type KeyboardEvent, type ReactElement } from 'react';
|
|
7
3
|
|
|
8
4
|
import { Button } from '#components/ui/button';
|
|
@@ -99,11 +95,12 @@ export function InboxItemCard(props: InboxItemCardProps): ReactElement {
|
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
/**
|
|
102
|
-
* One compact card of the inbox list. Clicking anywhere on it shows it in the detail pane (and marks it read)
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* slides in. The menu button
|
|
98
|
+
* One compact card of the inbox list. Clicking anywhere on it shows it in the detail pane (and marks it read); Enter on
|
|
99
|
+
* a focused card opens its route. The card reads: its title, ×N and time, then one line of particulars: whether it was
|
|
100
|
+
* handled, its type, what it is about (an issue's identifier) and one sentence, as its contributor words them. A
|
|
101
|
+
* decision that still waits carries an amber bar, an unread card a fainter bar and a bold title, a settled decision is
|
|
102
|
+
* dimmed with a check, and an item that arrived after the list loaded slides in. The menu button, in the time's place
|
|
103
|
+
* while the card is hovered, and a right-click offer read / unread and delete.
|
|
107
104
|
*/
|
|
108
105
|
function ItemCard({
|
|
109
106
|
entry,
|
|
@@ -158,8 +155,10 @@ function ItemCard({
|
|
|
158
155
|
data-inbox-item={item.id}
|
|
159
156
|
data-selected={selected ? 'true' : undefined}
|
|
160
157
|
className={cn(
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
// Pulled out by its padding and border, so its words start where the tabs' labels do and its bar sits in the
|
|
159
|
+
// list's gutter.
|
|
160
|
+
'group relative -mx-[calc(--spacing(3)+1px)] flex items-start gap-3 rounded-lg border border-transparent p-3 transition-[background-color,border-color,opacity] duration-200',
|
|
161
|
+
selected ? 'bg-hover' : 'hover:bg-hover',
|
|
163
162
|
settled && 'opacity-55',
|
|
164
163
|
fresh &&
|
|
165
164
|
'animate-in duration-200 fade-in slide-in-from-top-1 motion-reduce:animate-none',
|
|
@@ -168,73 +167,69 @@ function ItemCard({
|
|
|
168
167
|
<span
|
|
169
168
|
aria-hidden='true'
|
|
170
169
|
className={cn(
|
|
171
|
-
'absolute top-3 bottom-3 left-
|
|
170
|
+
'absolute top-3 bottom-3 -left-px w-0.5 rounded-full',
|
|
172
171
|
selected
|
|
173
172
|
? 'bg-primary'
|
|
174
173
|
: waiting
|
|
175
174
|
? 'bg-amber-500'
|
|
176
|
-
:
|
|
175
|
+
: unread
|
|
176
|
+
? 'bg-primary/70'
|
|
177
|
+
: 'bg-transparent',
|
|
177
178
|
)}
|
|
178
179
|
/>
|
|
179
|
-
<span className='mt-0.5 flex shrink-0'>
|
|
180
|
-
<InboxTypeIcon entry={entry} renderer={renderer} />
|
|
181
|
-
</span>
|
|
182
180
|
<Button
|
|
183
181
|
variant='ghost'
|
|
184
182
|
type='button'
|
|
185
183
|
aria-current={selected ? 'true' : undefined}
|
|
186
184
|
// Stretched over the whole card, so a click anywhere on it selects the item.
|
|
187
|
-
className="
|
|
185
|
+
className="grid h-auto min-w-0 flex-1 cursor-pointer grid-cols-[minmax(0,1fr)] gap-1 rounded-none border-0 bg-transparent p-0 text-left font-normal whitespace-normal hover:bg-transparent active:translate-y-0 after:absolute after:inset-0 after:rounded-lg after:content-[''] focus-visible:outline-none"
|
|
188
186
|
onClick={() => onSelect(entry)}
|
|
189
187
|
onKeyDown={onKeyDown}
|
|
190
188
|
>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
189
|
+
{/* The title, bold while unread, with the time at the right; the menu button takes the time's place while
|
|
190
|
+
the card is hovered or its menu open. */}
|
|
191
|
+
<span className='flex min-w-0 items-baseline gap-1.5'>
|
|
192
|
+
<span
|
|
193
|
+
className={cn(
|
|
194
|
+
'min-w-0 truncate text-sm group-has-focus-visible:underline',
|
|
195
|
+
unread ? 'font-semibold' : 'font-medium',
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{title}
|
|
199
|
+
{unread ? (
|
|
200
|
+
<span className='sr-only'>
|
|
201
|
+
{t('inbox.unread', { defaultValue: 'Unread' })}
|
|
202
|
+
</span>
|
|
203
|
+
) : null}
|
|
204
|
+
</span>
|
|
196
205
|
{notice && notice.count > 1 ? (
|
|
197
|
-
<span className='shrink-0 tabular-nums'>
|
|
206
|
+
<span className='shrink-0 text-xs text-muted-foreground tabular-nums'>
|
|
198
207
|
{t('inbox.count', {
|
|
199
208
|
count: notice.count,
|
|
200
209
|
defaultValue: '×{{count}}',
|
|
201
210
|
})}
|
|
202
211
|
</span>
|
|
203
212
|
) : null}
|
|
204
|
-
{settled ? (
|
|
205
|
-
<span className='shrink-0'>
|
|
206
|
-
{t('inbox.resolved', { defaultValue: 'Handled' })}
|
|
207
|
-
</span>
|
|
208
|
-
) : null}
|
|
209
213
|
{/* The latest notice's time: a merged item (×N) is sent anew, replacing the previous one. */}
|
|
210
214
|
<time
|
|
211
|
-
className='ml-auto shrink-0 tabular-nums'
|
|
215
|
+
className='ml-auto shrink-0 text-xs text-muted-foreground tabular-nums group-has-focus-visible:invisible group-hover:invisible group-has-data-popup-open:invisible'
|
|
212
216
|
dateTime={item.createdAt}
|
|
213
217
|
title={new Date(item.createdAt).toLocaleString(language)}
|
|
214
218
|
>
|
|
215
219
|
{relativeTime(item.createdAt, language)}
|
|
216
220
|
</time>
|
|
217
221
|
</span>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{
|
|
225
|
-
<span className='
|
|
226
|
-
<span className='sr-only'>
|
|
227
|
-
{t('inbox.unread', { defaultValue: 'Unread' })}
|
|
228
|
-
</span>
|
|
229
|
-
</span>
|
|
222
|
+
{/* Then one line of particulars: whether it was handled, its kind and what it is about, and its sentence. */}
|
|
223
|
+
<span className='line-clamp-1 text-xs text-muted-foreground wrap-anywhere'>
|
|
224
|
+
{settled
|
|
225
|
+
? `${t('inbox.resolved', { defaultValue: 'Handled' })} · `
|
|
226
|
+
: null}
|
|
227
|
+
{wording.label(entry)}
|
|
228
|
+
{notice?.subject?.label ? (
|
|
229
|
+
<span className='font-mono'> {notice.subject.label}</span>
|
|
230
230
|
) : null}
|
|
231
|
-
|
|
231
|
+
{sentence ? ` · ${sentence}` : null}
|
|
232
232
|
</span>
|
|
233
|
-
{sentence ? (
|
|
234
|
-
<span className='line-clamp-1 text-sm text-muted-foreground wrap-anywhere'>
|
|
235
|
-
{sentence}
|
|
236
|
-
</span>
|
|
237
|
-
) : null}
|
|
238
233
|
</Button>
|
|
239
234
|
<DropdownMenu>
|
|
240
235
|
<DropdownMenuTrigger
|
|
@@ -243,7 +238,7 @@ function ItemCard({
|
|
|
243
238
|
variant='ghost'
|
|
244
239
|
size='icon-xs'
|
|
245
240
|
disabled={busy}
|
|
246
|
-
className='
|
|
241
|
+
className='absolute top-2 right-2 opacity-0 group-hover:opacity-100 group-has-focus-visible:opacity-100 focus-visible:opacity-100 data-popup-open:opacity-100'
|
|
247
242
|
aria-label={t('inbox.actionsFor', {
|
|
248
243
|
title,
|
|
249
244
|
defaultValue: 'Actions for {{title}}',
|
|
@@ -257,15 +252,6 @@ function ItemCard({
|
|
|
257
252
|
<DropdownMenuGroup>{menuItems(DropdownMenuItem)}</DropdownMenuGroup>
|
|
258
253
|
</DropdownMenuContent>
|
|
259
254
|
</DropdownMenu>
|
|
260
|
-
<ChevronRightIcon
|
|
261
|
-
aria-hidden='true'
|
|
262
|
-
className={cn(
|
|
263
|
-
'size-4 shrink-0 self-center transition-colors',
|
|
264
|
-
selected
|
|
265
|
-
? 'text-foreground'
|
|
266
|
-
: 'text-muted-foreground group-hover:text-foreground',
|
|
267
|
-
)}
|
|
268
|
-
/>
|
|
269
255
|
</ContextMenuTrigger>
|
|
270
256
|
<ContextMenuContent>
|
|
271
257
|
<ContextMenuGroup>{menuItems(ContextMenuItem)}</ContextMenuGroup>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AlertCircleIcon, InboxIcon } from 'lucide-react';
|
|
1
|
+
import { AlertCircleIcon, InboxIcon, ListTodoIcon } from 'lucide-react';
|
|
2
2
|
import type { ReactElement, ReactNode } from 'react';
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
AlertDescription,
|
|
8
8
|
AlertTitle,
|
|
9
9
|
} from '#components/ui/alert';
|
|
10
|
-
import { Badge } from '#components/ui/badge';
|
|
11
10
|
import { Button } from '#components/ui/button';
|
|
12
11
|
import {
|
|
13
12
|
Empty,
|
|
@@ -28,7 +27,6 @@ import {
|
|
|
28
27
|
import { Skeleton } from '#components/ui/skeleton';
|
|
29
28
|
import { Spinner } from '#components/ui/spinner';
|
|
30
29
|
import { Tabs, TabsList, TabsTrigger } from '#components/ui/tabs';
|
|
31
|
-
import { cn } from 'cn';
|
|
32
30
|
|
|
33
31
|
import { useInboxFrameText } from './frame.js';
|
|
34
32
|
import type { InboxAction } from './inbox-actions.js';
|
|
@@ -261,7 +259,7 @@ export function InboxList({
|
|
|
261
259
|
<section aria-labelledby={heading} className='space-y-1'>
|
|
262
260
|
<h2
|
|
263
261
|
id={heading}
|
|
264
|
-
className='flex items-center gap-2
|
|
262
|
+
className='flex items-center gap-2 text-xs font-medium tracking-wider text-muted-foreground uppercase'
|
|
265
263
|
>
|
|
266
264
|
{category.title(t)}
|
|
267
265
|
{/* What still waits, as To do counts it; handled items stay listed in All but are not counted. */}
|
|
@@ -311,80 +309,80 @@ export function InboxList({
|
|
|
311
309
|
|
|
312
310
|
return (
|
|
313
311
|
<>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
312
|
+
{/* The views as line tabs (client/styles.css draws them as a record page's), each led by its icon, a count in
|
|
313
|
+
muted figures. */}
|
|
314
|
+
<Tabs
|
|
315
|
+
value={view}
|
|
316
|
+
onValueChange={(value) => {
|
|
317
|
+
const next = value as InboxView;
|
|
318
|
+
onFilter({
|
|
319
|
+
view: next,
|
|
320
|
+
kind: kindsOf(next, categories).includes(kind) ? kind : 'all',
|
|
321
|
+
});
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
<TabsList
|
|
325
|
+
variant='line'
|
|
326
|
+
className='shrink-0 [--tabs-inset:--spacing(6)]'
|
|
327
|
+
>
|
|
328
|
+
{INBOX_VIEWS.map((value) => {
|
|
329
|
+
const count = value === 'todo' ? pending : unread;
|
|
330
|
+
const Icon = value === 'todo' ? ListTodoIcon : InboxIcon;
|
|
331
|
+
return (
|
|
332
|
+
<TabsTrigger key={value} value={value}>
|
|
333
|
+
<Icon aria-hidden='true' />
|
|
334
|
+
{value === 'todo'
|
|
335
|
+
? t('inbox.views.todo', { defaultValue: 'To do' })
|
|
336
|
+
: t('inbox.views.all', { defaultValue: 'All' })}
|
|
337
|
+
{count > 0 ? (
|
|
338
|
+
<span
|
|
339
|
+
className='text-xs text-muted-foreground tabular-nums'
|
|
340
|
+
aria-label={
|
|
341
|
+
value === 'todo'
|
|
342
|
+
? t('inbox.pendingDecisions', {
|
|
343
|
+
count,
|
|
344
|
+
defaultValue: '{{count}} waiting',
|
|
345
|
+
})
|
|
346
|
+
: t('inbox.unreadCount', {
|
|
347
|
+
count,
|
|
348
|
+
defaultValue: '{{count}} unread',
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
>
|
|
352
|
+
{count}
|
|
353
|
+
</span>
|
|
354
|
+
) : null}
|
|
355
|
+
</TabsTrigger>
|
|
356
|
+
);
|
|
357
|
+
})}
|
|
358
|
+
</TabsList>
|
|
359
|
+
</Tabs>
|
|
360
|
+
<div className='flex shrink-0 flex-wrap items-center justify-between gap-2 border-b border-border px-6 py-2.5'>
|
|
361
|
+
<Select
|
|
362
|
+
items={kinds}
|
|
363
|
+
value={kind}
|
|
364
|
+
onValueChange={(next: string | null) => {
|
|
365
|
+
if (next) onFilter({ view, kind: next });
|
|
323
366
|
}}
|
|
324
367
|
>
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<TabsTrigger key={value} value={value}>
|
|
330
|
-
{value === 'todo'
|
|
331
|
-
? t('inbox.views.todo', { defaultValue: 'To do' })
|
|
332
|
-
: t('inbox.views.all', { defaultValue: 'All' })}
|
|
333
|
-
{count > 0 ? (
|
|
334
|
-
<Badge
|
|
335
|
-
variant={value === 'todo' ? 'outline' : 'secondary'}
|
|
336
|
-
className={cn(
|
|
337
|
-
'h-4 px-1.5 tabular-nums',
|
|
338
|
-
value === 'todo' &&
|
|
339
|
-
'border-transparent bg-amber-500/15 text-amber-700 dark:text-amber-300',
|
|
340
|
-
)}
|
|
341
|
-
aria-label={
|
|
342
|
-
value === 'todo'
|
|
343
|
-
? t('inbox.pendingDecisions', {
|
|
344
|
-
count,
|
|
345
|
-
defaultValue: '{{count}} waiting',
|
|
346
|
-
})
|
|
347
|
-
: t('inbox.unreadCount', {
|
|
348
|
-
count,
|
|
349
|
-
defaultValue: '{{count}} unread',
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
>
|
|
353
|
-
{count}
|
|
354
|
-
</Badge>
|
|
355
|
-
) : null}
|
|
356
|
-
</TabsTrigger>
|
|
357
|
-
);
|
|
358
|
-
})}
|
|
359
|
-
</TabsList>
|
|
360
|
-
</Tabs>
|
|
361
|
-
<div className='flex flex-wrap items-center justify-between gap-2'>
|
|
362
|
-
<Select
|
|
363
|
-
items={kinds}
|
|
364
|
-
value={kind}
|
|
365
|
-
onValueChange={(next: string | null) => {
|
|
366
|
-
if (next) onFilter({ view, kind: next });
|
|
367
|
-
}}
|
|
368
|
+
<SelectTrigger
|
|
369
|
+
size='sm'
|
|
370
|
+
className='w-40'
|
|
371
|
+
aria-label={t('inbox.kinds.label', { defaultValue: 'Type' })}
|
|
368
372
|
>
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
</SelectItem>
|
|
381
|
-
))}
|
|
382
|
-
</SelectContent>
|
|
383
|
-
</Select>
|
|
384
|
-
{footer}
|
|
385
|
-
</div>
|
|
373
|
+
<SelectValue />
|
|
374
|
+
</SelectTrigger>
|
|
375
|
+
<SelectContent className='w-auto max-w-[min(var(--container-sm),var(--available-width))] min-w-(--anchor-width) [&_[data-slot=select-item]>:first-child]:whitespace-normal'>
|
|
376
|
+
{kinds.map((item) => (
|
|
377
|
+
<SelectItem key={item.value} value={item.value}>
|
|
378
|
+
{item.label}
|
|
379
|
+
</SelectItem>
|
|
380
|
+
))}
|
|
381
|
+
</SelectContent>
|
|
382
|
+
</Select>
|
|
383
|
+
{footer}
|
|
386
384
|
</div>
|
|
387
|
-
<div className='min-h-0 flex-1 overflow-y-auto px-
|
|
385
|
+
<div className='min-h-0 flex-1 overflow-y-auto px-6 py-3'>{content}</div>
|
|
388
386
|
<p className='hidden shrink-0 items-center gap-3 border-t px-4 py-2 text-xs text-muted-foreground lg:flex'>
|
|
389
387
|
<span className='inline-flex items-center gap-1'>
|
|
390
388
|
<Kbd>J</Kbd>
|
|
@@ -410,7 +408,7 @@ function ListEmpty({
|
|
|
410
408
|
readonly action?: ReactNode;
|
|
411
409
|
}): ReactElement {
|
|
412
410
|
return (
|
|
413
|
-
<Empty className='
|
|
411
|
+
<Empty className='min-h-48'>
|
|
414
412
|
<EmptyHeader>
|
|
415
413
|
<EmptyMedia variant='icon'>
|
|
416
414
|
<InboxIcon />
|
package/client/index.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { resolveAppRuntime } from '@nocobase/app-client/runtime';
|
|
8
8
|
import { createRoot } from 'react-dom/client';
|
|
9
9
|
|
|
10
|
+
import { revealScrollbarsWhileScrolling } from './lib/scrollbar';
|
|
10
11
|
import appRuntime from './runtime';
|
|
11
12
|
import { AppStartupError } from './startup';
|
|
12
13
|
import './styles.css';
|
|
@@ -19,6 +20,9 @@ if (!container) {
|
|
|
19
20
|
throw new Error('Missing application root element.');
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// Scrollbars show only while their element scrolls (client/styles.css).
|
|
24
|
+
revealScrollbarsWhileScrolling();
|
|
25
|
+
|
|
22
26
|
const root = createRoot(container);
|
|
23
27
|
let app: ClientApplication | undefined;
|
|
24
28
|
let applicationStarted = false;
|
|
@@ -33,7 +33,7 @@ import { AppBrand } from './app-brand.js';
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* The shell's sidebar state: shadcn's provider, with the desktop icon mode kept in the shared sidebar preference
|
|
36
|
-
* (`use-sidebar-preference.ts`) so every layout
|
|
36
|
+
* (`use-sidebar-preference.ts`) so every layout of this application opens it the same way. Whether the phone's sheet is
|
|
37
37
|
* open stays local to the layout.
|
|
38
38
|
*/
|
|
39
39
|
export function AppSidebarProvider({
|
|
@@ -48,11 +48,11 @@ export function AppSidebarProvider({
|
|
|
48
48
|
open={!collapsed}
|
|
49
49
|
onOpenChange={(open) => setCollapsed(!open)}
|
|
50
50
|
className='h-svh bg-background'
|
|
51
|
-
//
|
|
52
|
-
//
|
|
51
|
+
// shadcn's 16rem in spacing units, so a density preset that changes the unit scales the sidebar together with
|
|
52
|
+
// the items inside it (the provider's documented width override).
|
|
53
53
|
style={
|
|
54
54
|
{
|
|
55
|
-
'--sidebar-width': 'calc(var(--spacing) *
|
|
55
|
+
'--sidebar-width': 'calc(var(--spacing) * 64)',
|
|
56
56
|
'--sidebar-width-icon': 'calc(var(--spacing) * 12)',
|
|
57
57
|
} as CSSProperties
|
|
58
58
|
}
|
|
@@ -102,7 +102,7 @@ export function AppSidebar({
|
|
|
102
102
|
}, [isMobile, openMobile, setOpenMobile]);
|
|
103
103
|
|
|
104
104
|
const header = (
|
|
105
|
-
<SidebarHeader className='h-16 shrink-0 flex-row items-center justify-between border-b border-sidebar-border
|
|
105
|
+
<SidebarHeader className='h-16 shrink-0 flex-row items-center justify-between border-b border-sidebar-border px-5 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0'>
|
|
106
106
|
<AppBrand compact={state === 'collapsed' && !isMobile} />
|
|
107
107
|
{isMobile ? (
|
|
108
108
|
<Button
|
|
@@ -123,7 +123,7 @@ export function AppSidebar({
|
|
|
123
123
|
<>
|
|
124
124
|
{header}
|
|
125
125
|
{/* The primitive hides the icon mode's overflow; a long menu still scrolls there. */}
|
|
126
|
-
<SidebarContent className='app-navigation group-data-[collapsible=icon]:overflow-y-auto
|
|
126
|
+
<SidebarContent className='app-navigation group-data-[collapsible=icon]:overflow-y-auto'>
|
|
127
127
|
{children}
|
|
128
128
|
</SidebarContent>
|
|
129
129
|
{footer}
|
|
@@ -8,7 +8,7 @@ export function LayoutHeader({
|
|
|
8
8
|
return (
|
|
9
9
|
<header
|
|
10
10
|
className={cn(
|
|
11
|
-
'flex h-16 shrink-0 items-center border-b border-border
|
|
11
|
+
'flex h-16 shrink-0 items-center border-b border-border bg-background/85 px-3 backdrop-blur-xl md:px-4',
|
|
12
12
|
className,
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|