@tuturuuu/ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/package.json +6 -5
- package/src/components/ui/storefront/browse-panel.tsx +161 -0
- package/src/components/ui/storefront/bundle-selection-dialog.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +1 -1
- package/src/components/ui/storefront/hero-panel.tsx +35 -30
- package/src/components/ui/storefront/listing-card.tsx +10 -5
- package/src/components/ui/storefront/merch-sections.tsx +7 -5
- package/src/components/ui/storefront/product-detail.tsx +79 -6
- package/src/components/ui/storefront/product-dialog.tsx +5 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.tsx +67 -63
- package/src/components/ui/storefront/types.ts +28 -0
- package/src/components/ui/storefront/utils.ts +4 -5
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +59 -1
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +37 -8
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +42 -54
- package/src/globals.css +1 -0
|
@@ -213,6 +213,78 @@ describe('StorefrontSurface', () => {
|
|
|
213
213
|
expect(screen.getAllByText('1M')).toHaveLength(1);
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
+
it('filters and searches the buyer catalog without losing the store context', () => {
|
|
217
|
+
render(
|
|
218
|
+
<StorefrontSurface
|
|
219
|
+
bundles={[categoryBundle]}
|
|
220
|
+
listings={[listing, bundleListing]}
|
|
221
|
+
mode="store"
|
|
222
|
+
storefront={storefront}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(screen.getByText('1:1 Mentoring')).toBeInTheDocument();
|
|
227
|
+
expect(screen.getByText('Buy 2 Get 1 Keychains')).toBeInTheDocument();
|
|
228
|
+
|
|
229
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Bundles' }));
|
|
230
|
+
expect(screen.queryByText('1:1 Mentoring')).not.toBeInTheDocument();
|
|
231
|
+
expect(screen.getByText('Buy 2 Get 1 Keychains')).toBeInTheDocument();
|
|
232
|
+
|
|
233
|
+
fireEvent.click(screen.getByRole('tab', { name: 'All' }));
|
|
234
|
+
fireEvent.change(
|
|
235
|
+
screen.getByRole('searchbox', { name: 'Search this store' }),
|
|
236
|
+
{
|
|
237
|
+
target: { value: '1:1' },
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
expect(screen.getByText('1:1 Mentoring')).toBeInTheDocument();
|
|
241
|
+
expect(screen.queryByText('Buy 2 Get 1 Keychains')).not.toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('opens a wide, readable Square Terminal product dialog', () => {
|
|
245
|
+
const longListing = {
|
|
246
|
+
...listing,
|
|
247
|
+
title: 'Shikishi Board (8.8 x 10 inches, 253 mm x 225 mm)',
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
render(
|
|
251
|
+
<StorefrontSurface
|
|
252
|
+
detailListingId={longListing.id}
|
|
253
|
+
labels={{
|
|
254
|
+
buyNow: 'Pay at terminal',
|
|
255
|
+
squareTerminal: 'Square Terminal',
|
|
256
|
+
squareTerminalDescription: 'Finish payment on the counter device.',
|
|
257
|
+
}}
|
|
258
|
+
listings={[longListing]}
|
|
259
|
+
mode="store"
|
|
260
|
+
onBuyNow={() => undefined}
|
|
261
|
+
onDetailListingChange={() => undefined}
|
|
262
|
+
storefront={{ ...storefront, checkoutMode: 'square_terminal' }}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
expect(screen.getByRole('dialog')).toHaveClass(
|
|
267
|
+
'sm:max-w-6xl',
|
|
268
|
+
'overflow-hidden',
|
|
269
|
+
'p-0'
|
|
270
|
+
);
|
|
271
|
+
const visibleHeading = screen
|
|
272
|
+
.getAllByRole('heading', { name: longListing.title })
|
|
273
|
+
.find((heading) => !heading.classList.contains('sr-only'));
|
|
274
|
+
expect(visibleHeading).toHaveClass(
|
|
275
|
+
'break-words',
|
|
276
|
+
'text-2xl',
|
|
277
|
+
'sm:text-3xl'
|
|
278
|
+
);
|
|
279
|
+
expect(screen.getByText('Square Terminal')).toBeInTheDocument();
|
|
280
|
+
expect(
|
|
281
|
+
screen.getByText('Finish payment on the counter device.')
|
|
282
|
+
).toBeInTheDocument();
|
|
283
|
+
expect(
|
|
284
|
+
screen.getByRole('button', { name: 'Pay at terminal' })
|
|
285
|
+
).toBeEnabled();
|
|
286
|
+
});
|
|
287
|
+
|
|
216
288
|
it('keeps the compatibility cart page as a full cart review', () => {
|
|
217
289
|
render(
|
|
218
290
|
<StorefrontSurface
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DialogHeader,
|
|
16
16
|
DialogTitle,
|
|
17
17
|
} from '../dialog';
|
|
18
|
+
import { StorefrontBrowsePanel } from './browse-panel';
|
|
18
19
|
import { StorefrontBundleSelectionDialog } from './bundle-selection-dialog';
|
|
19
20
|
import { StorefrontCartPopover } from './cart-popover';
|
|
20
21
|
import { StorefrontCartSummary } from './cart-summary';
|
|
@@ -173,7 +174,6 @@ export function StorefrontSurface({
|
|
|
173
174
|
const isCheckoutDialogOpen = checkoutOpen ?? isCheckout;
|
|
174
175
|
const isPreview = mode === 'preview';
|
|
175
176
|
const isCartPage = mode === 'cart';
|
|
176
|
-
const listingRows = visibleListings;
|
|
177
177
|
const bundleSelectionListing = bundleSelectionListingId
|
|
178
178
|
? listings.find((listing) => listing.id === bundleSelectionListingId)
|
|
179
179
|
: null;
|
|
@@ -256,27 +256,36 @@ export function StorefrontSurface({
|
|
|
256
256
|
</div>
|
|
257
257
|
) : null}
|
|
258
258
|
|
|
259
|
-
<header className="sticky top-0 z-30 border-border border-b bg-background/
|
|
260
|
-
<div className="mx-auto flex max-w-7xl
|
|
259
|
+
<header className="sticky top-0 z-30 border-border border-b bg-background/95 backdrop-blur-md supports-[backdrop-filter]:bg-background/85">
|
|
260
|
+
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-5 py-3 sm:px-6">
|
|
261
261
|
<div className="min-w-0">
|
|
262
|
-
<h1 className="truncate font-semibold text-
|
|
262
|
+
<h1 className="truncate font-semibold text-base tracking-tight">
|
|
263
263
|
{storefrontHref ? (
|
|
264
264
|
<a
|
|
265
|
-
className="
|
|
265
|
+
className="flex min-w-0 items-center gap-3 rounded-sm transition hover:text-[var(--storefront-accent-text,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
266
266
|
href={storefrontHref}
|
|
267
267
|
title={storefront.name}
|
|
268
268
|
>
|
|
269
|
-
|
|
269
|
+
<span
|
|
270
|
+
aria-hidden
|
|
271
|
+
className="grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted/35 font-mono text-xs uppercase"
|
|
272
|
+
>
|
|
273
|
+
{storefront.name.slice(0, 1)}
|
|
274
|
+
</span>
|
|
275
|
+
<span className="truncate">{storefront.name}</span>
|
|
270
276
|
</a>
|
|
271
277
|
) : (
|
|
272
|
-
|
|
278
|
+
<span className="flex min-w-0 items-center gap-3">
|
|
279
|
+
<span
|
|
280
|
+
aria-hidden
|
|
281
|
+
className="grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted/35 font-mono text-xs uppercase"
|
|
282
|
+
>
|
|
283
|
+
{storefront.name.slice(0, 1)}
|
|
284
|
+
</span>
|
|
285
|
+
<span className="truncate">{storefront.name}</span>
|
|
286
|
+
</span>
|
|
273
287
|
)}
|
|
274
288
|
</h1>
|
|
275
|
-
{storefront.description ? (
|
|
276
|
-
<p className="mt-0.5 line-clamp-1 text-muted-foreground text-sm">
|
|
277
|
-
{storefront.description}
|
|
278
|
-
</p>
|
|
279
|
-
) : null}
|
|
280
289
|
</div>
|
|
281
290
|
<div className="flex items-center gap-2">
|
|
282
291
|
{headerActions}
|
|
@@ -295,7 +304,7 @@ export function StorefrontSurface({
|
|
|
295
304
|
|
|
296
305
|
<section
|
|
297
306
|
className={cn(
|
|
298
|
-
'mx-auto max-w-7xl px-
|
|
307
|
+
'mx-auto max-w-7xl px-5 py-8 sm:px-6 sm:py-10',
|
|
299
308
|
compactLayout ? 'max-w-5xl' : null
|
|
300
309
|
)}
|
|
301
310
|
>
|
|
@@ -317,6 +326,7 @@ export function StorefrontSurface({
|
|
|
317
326
|
<StorefrontProductDetail
|
|
318
327
|
cartHref={cartHref}
|
|
319
328
|
cartLines={cartLines}
|
|
329
|
+
checkoutMode={storefront.checkoutMode}
|
|
320
330
|
currency={currency}
|
|
321
331
|
isSubmitting={isSubmitting}
|
|
322
332
|
labels={labels}
|
|
@@ -351,61 +361,54 @@ export function StorefrontSurface({
|
|
|
351
361
|
sections={storefront.sections ?? []}
|
|
352
362
|
/>
|
|
353
363
|
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
compactLayout
|
|
358
|
-
? 'sm:grid-cols-2'
|
|
359
|
-
: 'sm:grid-cols-2 xl:grid-cols-3'
|
|
360
|
-
)}
|
|
361
|
-
>
|
|
362
|
-
{listingRows.length === 0 ? (
|
|
364
|
+
<StorefrontBrowsePanel
|
|
365
|
+
compactLayout={compactLayout}
|
|
366
|
+
emptyListings={
|
|
363
367
|
<StorefrontEmptyListings
|
|
364
368
|
action={emptyAction}
|
|
365
369
|
labels={labels}
|
|
366
370
|
radius={radius}
|
|
367
371
|
/>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
372
|
+
}
|
|
373
|
+
labels={labels}
|
|
374
|
+
listings={visibleListings}
|
|
375
|
+
renderListing={(listing) => {
|
|
376
|
+
const listingBundle = listing.bundleId
|
|
377
|
+
? bundles.find((bundle) => bundle.id === listing.bundleId)
|
|
378
|
+
: undefined;
|
|
379
|
+
const quantity = cartLines
|
|
380
|
+
.filter((item) => item.listingId === listing.id)
|
|
381
|
+
.reduce((sum, item) => sum + item.quantity, 0);
|
|
378
382
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
</div>
|
|
383
|
+
return (
|
|
384
|
+
<StorefrontListingCard
|
|
385
|
+
currency={currency}
|
|
386
|
+
isList={false}
|
|
387
|
+
key={listing.id}
|
|
388
|
+
labels={labels}
|
|
389
|
+
listing={listing}
|
|
390
|
+
onDecrement={onDecrement}
|
|
391
|
+
onConfigureBundle={
|
|
392
|
+
listingBundle?.categoryComponents.length
|
|
393
|
+
? () => setBundleSelectionListingId(listing.id)
|
|
394
|
+
: undefined
|
|
395
|
+
}
|
|
396
|
+
onIncrement={onIncrement}
|
|
397
|
+
onOpenDetail={
|
|
398
|
+
onDetailListingChange
|
|
399
|
+
? (id) => onDetailListingChange(id)
|
|
400
|
+
: undefined
|
|
401
|
+
}
|
|
402
|
+
quantity={quantity}
|
|
403
|
+
radius={radius}
|
|
404
|
+
showInventoryBadges={storefront.showInventoryBadges}
|
|
405
|
+
surfaceClassName={
|
|
406
|
+
storefrontSurfaceClasses[storefront.surfaceStyle]
|
|
407
|
+
}
|
|
408
|
+
/>
|
|
409
|
+
);
|
|
410
|
+
}}
|
|
411
|
+
/>
|
|
409
412
|
</>
|
|
410
413
|
)}
|
|
411
414
|
</div>
|
|
@@ -415,6 +418,7 @@ export function StorefrontSurface({
|
|
|
415
418
|
<StorefrontProductDialog
|
|
416
419
|
cartHref={cartHref}
|
|
417
420
|
cartLines={cartLines}
|
|
421
|
+
checkoutMode={storefront.checkoutMode}
|
|
418
422
|
currency={currency}
|
|
419
423
|
isSubmitting={isSubmitting}
|
|
420
424
|
labels={labels}
|
|
@@ -453,7 +457,7 @@ export function StorefrontSurface({
|
|
|
453
457
|
onOpenChange={(open) => onCheckoutOpenChange?.(open)}
|
|
454
458
|
open={isCheckoutDialogOpen}
|
|
455
459
|
>
|
|
456
|
-
<DialogContent className="grid max-h-[92dvh] max-w-[min(42rem,calc(100vw-1rem))] grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden border-border
|
|
460
|
+
<DialogContent className="grid max-h-[92dvh] max-w-[min(42rem,calc(100vw-1rem))] grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden border-border p-0 shadow-sm sm:rounded-xl">
|
|
457
461
|
<DialogHeader className="px-5 pt-5 pb-2 text-left sm:px-6">
|
|
458
462
|
<DialogTitle>{labels.checkout}</DialogTitle>
|
|
459
463
|
<DialogDescription>{labels.reservedCopy}</DialogDescription>
|
|
@@ -34,9 +34,11 @@ export type StorefrontSurfaceMode =
|
|
|
34
34
|
|
|
35
35
|
export type StorefrontSurfaceLabels = {
|
|
36
36
|
add: string;
|
|
37
|
+
allItems: string;
|
|
37
38
|
available: string;
|
|
38
39
|
browse: string;
|
|
39
40
|
bundle: string;
|
|
41
|
+
bundles: string;
|
|
40
42
|
bundleSelectionTitle: string;
|
|
41
43
|
buyNow: string;
|
|
42
44
|
cart: string;
|
|
@@ -44,6 +46,7 @@ export type StorefrontSurfaceLabels = {
|
|
|
44
46
|
checkout: string;
|
|
45
47
|
checkoutDisabled: string;
|
|
46
48
|
checkoutDisabledBadge: string;
|
|
49
|
+
clearFilters: string;
|
|
47
50
|
contactDetails: string;
|
|
48
51
|
couponNote: string;
|
|
49
52
|
demoBadge: string;
|
|
@@ -51,15 +54,20 @@ export type StorefrontSurfaceLabels = {
|
|
|
51
54
|
fromPrice: string;
|
|
52
55
|
instantCheckout: string;
|
|
53
56
|
orderSummary: string;
|
|
57
|
+
onlineCheckout: string;
|
|
58
|
+
onlineCheckoutDescription: string;
|
|
54
59
|
redirectingToCheckout: string;
|
|
55
60
|
requiredItems: string;
|
|
56
61
|
selectOptions: string;
|
|
57
62
|
searchBundleItems: string;
|
|
63
|
+
searchStore: string;
|
|
58
64
|
selectedItems: string;
|
|
59
65
|
viewDetails: string;
|
|
60
66
|
emptyListingsDescription: string;
|
|
61
67
|
emptyListingsTitle: string;
|
|
62
68
|
fallbackDescription: string;
|
|
69
|
+
noResultsDescription: string;
|
|
70
|
+
noResultsTitle: string;
|
|
63
71
|
form: {
|
|
64
72
|
email: string;
|
|
65
73
|
name: string;
|
|
@@ -69,6 +77,7 @@ export type StorefrontSurfaceLabels = {
|
|
|
69
77
|
privateStore: string;
|
|
70
78
|
previewBadge: string;
|
|
71
79
|
product: string;
|
|
80
|
+
products: string;
|
|
72
81
|
publicStore: string;
|
|
73
82
|
quantity: string;
|
|
74
83
|
reserve: string;
|
|
@@ -76,14 +85,20 @@ export type StorefrontSurfaceLabels = {
|
|
|
76
85
|
reservedCopy: string;
|
|
77
86
|
simulatedBadge: string;
|
|
78
87
|
soldOut: string;
|
|
88
|
+
squareTerminal: string;
|
|
89
|
+
squareTerminalDescription: string;
|
|
90
|
+
shopTitle: string;
|
|
79
91
|
total: string;
|
|
92
|
+
visibleItems: string;
|
|
80
93
|
};
|
|
81
94
|
|
|
82
95
|
export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
83
96
|
add: 'Add',
|
|
97
|
+
allItems: 'All',
|
|
84
98
|
available: 'available',
|
|
85
99
|
browse: 'Browse',
|
|
86
100
|
bundle: 'Bundle',
|
|
101
|
+
bundles: 'Bundles',
|
|
87
102
|
bundleSelectionTitle: 'Build bundle',
|
|
88
103
|
buyNow: 'Buy now',
|
|
89
104
|
cart: 'Cart',
|
|
@@ -91,6 +106,7 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
|
91
106
|
checkout: 'Checkout',
|
|
92
107
|
checkoutDisabled: 'Checkout is disabled in preview',
|
|
93
108
|
checkoutDisabledBadge: 'Checkout disabled',
|
|
109
|
+
clearFilters: 'Clear filters',
|
|
94
110
|
contactDetails: 'Contact details',
|
|
95
111
|
couponNote: 'Have a coupon? You can apply it at checkout.',
|
|
96
112
|
demoBadge: 'Demo',
|
|
@@ -98,16 +114,22 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
|
98
114
|
fromPrice: 'From',
|
|
99
115
|
instantCheckout: 'Instant checkout',
|
|
100
116
|
orderSummary: 'Order summary',
|
|
117
|
+
onlineCheckout: 'Secure online checkout',
|
|
118
|
+
onlineCheckoutDescription:
|
|
119
|
+
'Continue to Polar to complete payment securely online.',
|
|
101
120
|
redirectingToCheckout: 'Taking you to secure checkout…',
|
|
102
121
|
requiredItems: 'Select {count} items',
|
|
103
122
|
selectOptions: 'Select options',
|
|
104
123
|
searchBundleItems: 'Search items',
|
|
124
|
+
searchStore: 'Search this store',
|
|
105
125
|
selectedItems: '{selected} of {required} selected',
|
|
106
126
|
viewDetails: 'View details',
|
|
107
127
|
emptyListingsDescription:
|
|
108
128
|
'Publish a listing to make this storefront ready for buyers.',
|
|
109
129
|
emptyListingsTitle: 'No listings yet',
|
|
110
130
|
fallbackDescription: 'This listing is available for checkout.',
|
|
131
|
+
noResultsDescription: 'Try another search or show every item.',
|
|
132
|
+
noResultsTitle: 'No matching items',
|
|
111
133
|
form: {
|
|
112
134
|
email: 'Email',
|
|
113
135
|
name: 'Name',
|
|
@@ -117,6 +139,7 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
|
117
139
|
privateStore: 'Private',
|
|
118
140
|
previewBadge: 'Preview',
|
|
119
141
|
product: 'Product',
|
|
142
|
+
products: 'Products',
|
|
120
143
|
publicStore: 'Public',
|
|
121
144
|
quantity: 'Qty',
|
|
122
145
|
reserve: 'Reserve with Polar',
|
|
@@ -125,7 +148,12 @@ export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
|
125
148
|
'Review your cart, then continue with the available checkout mode.',
|
|
126
149
|
simulatedBadge: 'Simulated checkout',
|
|
127
150
|
soldOut: 'Sold out',
|
|
151
|
+
squareTerminal: 'Square Terminal',
|
|
152
|
+
squareTerminalDescription:
|
|
153
|
+
'Complete payment in person on the paired terminal at the counter.',
|
|
154
|
+
shopTitle: 'Shop',
|
|
128
155
|
total: 'Total',
|
|
156
|
+
visibleItems: '{count} items',
|
|
129
157
|
};
|
|
130
158
|
|
|
131
159
|
export function mergeStorefrontSurfaceLabels(
|
|
@@ -15,8 +15,8 @@ import type { StorefrontCartLine } from './types';
|
|
|
15
15
|
// in the data model for backwards compatibility, but every value resolves to the
|
|
16
16
|
// same refined look so the experience is consistent across all storefronts.
|
|
17
17
|
|
|
18
|
-
/** One
|
|
19
|
-
export const STOREFRONT_RADIUS = 'rounded-
|
|
18
|
+
/** One restrained corner radius for every storefront surface. */
|
|
19
|
+
export const STOREFRONT_RADIUS = 'rounded-xl';
|
|
20
20
|
|
|
21
21
|
export const storefrontRadiusClasses: Record<
|
|
22
22
|
InventoryStorefront['cornerStyle'],
|
|
@@ -27,9 +27,8 @@ export const storefrontRadiusClasses: Record<
|
|
|
27
27
|
soft: STOREFRONT_RADIUS,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
/** One
|
|
31
|
-
export const STOREFRONT_SURFACE =
|
|
32
|
-
'border border-border/60 bg-card shadow-sm shadow-foreground/5';
|
|
30
|
+
/** One quiet, border-led card surface for every storefront. */
|
|
31
|
+
export const STOREFRONT_SURFACE = 'border border-border bg-card';
|
|
33
32
|
|
|
34
33
|
export const storefrontSurfaceClasses: Record<
|
|
35
34
|
InventoryStorefront['surfaceStyle'],
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
* @vitest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
act,
|
|
7
|
+
fireEvent,
|
|
8
|
+
render,
|
|
9
|
+
screen,
|
|
10
|
+
waitFor,
|
|
11
|
+
} from '@testing-library/react';
|
|
12
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
6
13
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
7
14
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
15
|
import type { ListPaginationState } from '../../shared/progressive-loader-context';
|
|
@@ -82,6 +89,19 @@ const loadedExternalState: ListPaginationState = {
|
|
|
82
89
|
totalCount: 0,
|
|
83
90
|
};
|
|
84
91
|
|
|
92
|
+
const regularColumn: TaskList = {
|
|
93
|
+
...externalColumn,
|
|
94
|
+
color: 'BLUE',
|
|
95
|
+
id: 'list-1',
|
|
96
|
+
is_external_staging: false,
|
|
97
|
+
name: 'In progress',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const regularTasks = [
|
|
101
|
+
{ id: 'task-1', list_id: regularColumn.id, name: 'First task' },
|
|
102
|
+
{ id: 'task-2', list_id: regularColumn.id, name: 'Second task' },
|
|
103
|
+
] as Task[];
|
|
104
|
+
|
|
85
105
|
function renderExternalColumn() {
|
|
86
106
|
return (
|
|
87
107
|
<BoardColumn
|
|
@@ -124,4 +144,42 @@ describe('BoardColumn external lane retry behavior', () => {
|
|
|
124
144
|
externalSortBy: 'created-desc',
|
|
125
145
|
});
|
|
126
146
|
});
|
|
147
|
+
|
|
148
|
+
it('replaces the status icon with a stable select-all checkbox in bulk mode', () => {
|
|
149
|
+
mocks.pagination = {
|
|
150
|
+
[regularColumn.id]: {
|
|
151
|
+
...loadedExternalState,
|
|
152
|
+
hasMore: false,
|
|
153
|
+
totalCount: regularTasks.length,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const onTaskSelect = vi.fn();
|
|
157
|
+
|
|
158
|
+
render(
|
|
159
|
+
<BoardColumn
|
|
160
|
+
boardId="board-1"
|
|
161
|
+
column={regularColumn}
|
|
162
|
+
isMultiSelectMode
|
|
163
|
+
onTaskSelect={onTaskSelect}
|
|
164
|
+
selectedTasks={new Set(['task-1'])}
|
|
165
|
+
setIsMultiSelectMode={vi.fn()}
|
|
166
|
+
tasks={regularTasks}
|
|
167
|
+
wsId="workspace-1"
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const checkbox = screen.getByRole('checkbox', {
|
|
172
|
+
name: 'select_all_tasks',
|
|
173
|
+
});
|
|
174
|
+
expect(checkbox).toHaveAttribute('data-state', 'indeterminate');
|
|
175
|
+
expect(checkbox).toHaveClass('size-4');
|
|
176
|
+
|
|
177
|
+
fireEvent.click(checkbox);
|
|
178
|
+
|
|
179
|
+
expect(onTaskSelect).toHaveBeenCalledTimes(1);
|
|
180
|
+
expect(onTaskSelect).toHaveBeenCalledWith(
|
|
181
|
+
'task-2',
|
|
182
|
+
expect.objectContaining({ shiftKey: false })
|
|
183
|
+
);
|
|
184
|
+
});
|
|
127
185
|
});
|
|
@@ -22,6 +22,7 @@ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
|
22
22
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
23
23
|
import { Button } from '@tuturuuu/ui/button';
|
|
24
24
|
import { Card } from '@tuturuuu/ui/card';
|
|
25
|
+
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
25
26
|
import {
|
|
26
27
|
DropdownMenu,
|
|
27
28
|
DropdownMenuCheckboxItem,
|
|
@@ -469,6 +470,14 @@ export function BoardColumn({
|
|
|
469
470
|
? Math.max(listState.totalCount, visibleTasks.length)
|
|
470
471
|
: visibleTasks.length
|
|
471
472
|
: visibleTasks.length;
|
|
473
|
+
const selectedVisibleTaskCount = visibleTasks.reduce(
|
|
474
|
+
(count, task) => count + (selectedTasks?.has(task.id) ? 1 : 0),
|
|
475
|
+
0
|
|
476
|
+
);
|
|
477
|
+
const allVisibleTasksSelected =
|
|
478
|
+
visibleTasks.length > 0 && selectedVisibleTaskCount === visibleTasks.length;
|
|
479
|
+
const someVisibleTasksSelected =
|
|
480
|
+
selectedVisibleTaskCount > 0 && !allVisibleTasksSelected;
|
|
472
481
|
const externalFilterCount =
|
|
473
482
|
(externalIncludeDocuments ? 1 : 0) + (externalIncludeDoneClosed ? 1 : 0);
|
|
474
483
|
const pinListLabel = specialPinned
|
|
@@ -496,13 +505,15 @@ export function BoardColumn({
|
|
|
496
505
|
[attributes, listeners, isDragging, isOverlay, t]
|
|
497
506
|
);
|
|
498
507
|
|
|
499
|
-
const
|
|
500
|
-
if (!onTaskSelect || !setIsMultiSelectMode ||
|
|
508
|
+
const handleToggleAll = () => {
|
|
509
|
+
if (!onTaskSelect || !setIsMultiSelectMode || visibleTasks.length === 0)
|
|
510
|
+
return;
|
|
511
|
+
|
|
512
|
+
if (!allVisibleTasksSelected) setIsMultiSelectMode(true);
|
|
501
513
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (selectedTasks?.has(task.id)) return;
|
|
514
|
+
visibleTasks.forEach((task) => {
|
|
515
|
+
const isSelected = selectedTasks?.has(task.id) ?? false;
|
|
516
|
+
if (allVisibleTasksSelected ? !isSelected : isSelected) return;
|
|
506
517
|
|
|
507
518
|
const fakeEvent = {
|
|
508
519
|
shiftKey: false,
|
|
@@ -606,7 +617,25 @@ export function BoardColumn({
|
|
|
606
617
|
<div className="flex items-center gap-2 rounded-t-xl border-b p-3">
|
|
607
618
|
{!readOnly && !isExternalStaging && DragHandle}
|
|
608
619
|
<div className="flex flex-1 items-center gap-2">
|
|
609
|
-
|
|
620
|
+
{isMultiSelectMode && !readOnly && visibleTasks.length > 0 ? (
|
|
621
|
+
<Checkbox
|
|
622
|
+
aria-label={t('select_all_tasks')}
|
|
623
|
+
checked={
|
|
624
|
+
allVisibleTasksSelected
|
|
625
|
+
? true
|
|
626
|
+
: someVisibleTasksSelected
|
|
627
|
+
? 'indeterminate'
|
|
628
|
+
: false
|
|
629
|
+
}
|
|
630
|
+
className="size-4"
|
|
631
|
+
onCheckedChange={handleToggleAll}
|
|
632
|
+
title={t('select_all_tasks')}
|
|
633
|
+
/>
|
|
634
|
+
) : (
|
|
635
|
+
<span className="inline-flex size-4 items-center justify-center text-sm">
|
|
636
|
+
{statusIcon}
|
|
637
|
+
</span>
|
|
638
|
+
)}
|
|
610
639
|
<h3
|
|
611
640
|
className={cn(
|
|
612
641
|
'font-semibold text-foreground/90 text-sm',
|
|
@@ -830,7 +859,7 @@ export function BoardColumn({
|
|
|
830
859
|
boardId={boardId}
|
|
831
860
|
wsId={wsId}
|
|
832
861
|
onUpdate={handleUpdate}
|
|
833
|
-
onSelectAll={
|
|
862
|
+
onSelectAll={handleToggleAll}
|
|
834
863
|
isEditOpen={isEditOpen}
|
|
835
864
|
onEditOpenChange={setIsEditOpen}
|
|
836
865
|
/>
|