@tuturuuu/ui 0.5.0 → 0.6.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 +29 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +50 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
- package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ShoppingCart } from '@tuturuuu/icons';
|
|
4
|
+
import type {
|
|
5
|
+
InventoryStorefront,
|
|
6
|
+
InventoryStorefrontListing,
|
|
7
|
+
} from '@tuturuuu/internal-api/inventory';
|
|
8
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
import { Badge } from '../badge';
|
|
11
|
+
import { StorefrontCartSummary } from './cart-summary';
|
|
12
|
+
import { StorefrontEmptyListings } from './empty-listings';
|
|
13
|
+
import { StorefrontHeroPanel } from './hero-panel';
|
|
14
|
+
import { StorefrontListingCard } from './listing-card';
|
|
15
|
+
import type {
|
|
16
|
+
StorefrontCartLine,
|
|
17
|
+
StorefrontSurfaceLabels,
|
|
18
|
+
StorefrontSurfaceMode,
|
|
19
|
+
} from './types';
|
|
20
|
+
import { mergeStorefrontSurfaceLabels } from './types';
|
|
21
|
+
import {
|
|
22
|
+
getAccentStyle,
|
|
23
|
+
getStorefrontListingLimit,
|
|
24
|
+
sanitizeStorefrontAccentColor,
|
|
25
|
+
storefrontRadiusClasses,
|
|
26
|
+
storefrontSurfaceClasses,
|
|
27
|
+
} from './utils';
|
|
28
|
+
|
|
29
|
+
export function StorefrontSurface({
|
|
30
|
+
cartLines = [],
|
|
31
|
+
checkoutHref,
|
|
32
|
+
className,
|
|
33
|
+
emptyAction,
|
|
34
|
+
headerActions,
|
|
35
|
+
isDemo = false,
|
|
36
|
+
isSubmitting = false,
|
|
37
|
+
labels: labelOverrides,
|
|
38
|
+
listings,
|
|
39
|
+
mode,
|
|
40
|
+
notice,
|
|
41
|
+
onCheckoutSubmit,
|
|
42
|
+
onDecrement,
|
|
43
|
+
onIncrement,
|
|
44
|
+
selectedListingId,
|
|
45
|
+
storefront,
|
|
46
|
+
}: {
|
|
47
|
+
cartLines?: StorefrontCartLine[];
|
|
48
|
+
checkoutHref?: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
emptyAction?: ReactNode;
|
|
51
|
+
headerActions?: ReactNode;
|
|
52
|
+
isDemo?: boolean;
|
|
53
|
+
isSubmitting?: boolean;
|
|
54
|
+
labels?: Partial<StorefrontSurfaceLabels>;
|
|
55
|
+
listings: InventoryStorefrontListing[];
|
|
56
|
+
mode: StorefrontSurfaceMode;
|
|
57
|
+
notice?: ReactNode;
|
|
58
|
+
onCheckoutSubmit?: (formData: FormData) => void;
|
|
59
|
+
onDecrement?: (listingId: string) => void;
|
|
60
|
+
onIncrement?: (listingId: string, maxQuantity: number) => void;
|
|
61
|
+
selectedListingId?: string;
|
|
62
|
+
storefront: InventoryStorefront;
|
|
63
|
+
}) {
|
|
64
|
+
const labels = mergeStorefrontSurfaceLabels(labelOverrides);
|
|
65
|
+
const accentColor = sanitizeStorefrontAccentColor(storefront.accentColor);
|
|
66
|
+
const radius = storefrontRadiusClasses[storefront.cornerStyle];
|
|
67
|
+
const cartEntries = cartLines.flatMap((line) => {
|
|
68
|
+
const listing = listings.find((item) => item.id === line.listingId);
|
|
69
|
+
return listing ? [{ line, listing }] : [];
|
|
70
|
+
});
|
|
71
|
+
const checkoutEntries = cartEntries.filter(({ line, listing }) => {
|
|
72
|
+
const quantity = Math.min(
|
|
73
|
+
line.quantity,
|
|
74
|
+
getStorefrontListingLimit(listing)
|
|
75
|
+
);
|
|
76
|
+
return quantity > 0;
|
|
77
|
+
});
|
|
78
|
+
const total = checkoutEntries.reduce((sum, { line, listing }) => {
|
|
79
|
+
const quantity = Math.min(
|
|
80
|
+
line.quantity,
|
|
81
|
+
getStorefrontListingLimit(listing)
|
|
82
|
+
);
|
|
83
|
+
return sum + listing.price * quantity;
|
|
84
|
+
}, 0);
|
|
85
|
+
const cartQuantity = cartLines.reduce((sum, line) => sum + line.quantity, 0);
|
|
86
|
+
const visibleListings =
|
|
87
|
+
mode === 'product' && selectedListingId
|
|
88
|
+
? listings.filter((listing) => listing.id === selectedListingId)
|
|
89
|
+
: listings;
|
|
90
|
+
const isCheckout = mode === 'checkout';
|
|
91
|
+
const isPreview = mode === 'preview';
|
|
92
|
+
const showCartListings = mode === 'cart' || isCheckout;
|
|
93
|
+
const listingRows = showCartListings
|
|
94
|
+
? cartEntries.map(({ listing }) => listing)
|
|
95
|
+
: visibleListings;
|
|
96
|
+
const currency = storefront.currency ?? 'USD';
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<main
|
|
100
|
+
className={cn('min-h-dvh bg-background text-foreground', className)}
|
|
101
|
+
style={getAccentStyle(accentColor)}
|
|
102
|
+
>
|
|
103
|
+
{notice ? (
|
|
104
|
+
<div className="border-border border-b bg-muted/35 px-4 py-2 text-center text-muted-foreground text-sm">
|
|
105
|
+
{notice}
|
|
106
|
+
</div>
|
|
107
|
+
) : null}
|
|
108
|
+
|
|
109
|
+
<header className="border-border border-b bg-background/90 backdrop-blur">
|
|
110
|
+
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-3 px-4 py-3">
|
|
111
|
+
<div className="min-w-0">
|
|
112
|
+
<div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
|
|
113
|
+
<span>
|
|
114
|
+
{storefront.visibility === 'private'
|
|
115
|
+
? labels.privateStore
|
|
116
|
+
: labels.publicStore}
|
|
117
|
+
</span>
|
|
118
|
+
{isDemo ? (
|
|
119
|
+
<Badge variant="secondary">{labels.demoBadge}</Badge>
|
|
120
|
+
) : null}
|
|
121
|
+
{isPreview ? (
|
|
122
|
+
<Badge
|
|
123
|
+
className="border-border bg-background"
|
|
124
|
+
variant="outline"
|
|
125
|
+
>
|
|
126
|
+
{labels.previewBadge}
|
|
127
|
+
</Badge>
|
|
128
|
+
) : null}
|
|
129
|
+
{storefront.checkoutMode === 'simulated' ? (
|
|
130
|
+
<Badge
|
|
131
|
+
className="border-border bg-background"
|
|
132
|
+
variant="outline"
|
|
133
|
+
>
|
|
134
|
+
{labels.simulatedBadge}
|
|
135
|
+
</Badge>
|
|
136
|
+
) : null}
|
|
137
|
+
{storefront.checkoutMode === 'disabled' ? (
|
|
138
|
+
<Badge
|
|
139
|
+
className="border-border bg-background"
|
|
140
|
+
variant="outline"
|
|
141
|
+
>
|
|
142
|
+
{labels.checkoutDisabledBadge}
|
|
143
|
+
</Badge>
|
|
144
|
+
) : null}
|
|
145
|
+
</div>
|
|
146
|
+
<h1 className="mt-0.5 truncate font-semibold text-xl">
|
|
147
|
+
{storefront.name}
|
|
148
|
+
</h1>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
{headerActions}
|
|
152
|
+
<span
|
|
153
|
+
className={cn(
|
|
154
|
+
'inline-flex h-9 min-w-12 items-center justify-center gap-2 border bg-card px-3 font-medium text-sm',
|
|
155
|
+
radius
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
<ShoppingCart className="h-4 w-4" />
|
|
159
|
+
{cartQuantity}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</header>
|
|
164
|
+
|
|
165
|
+
<section className="mx-auto grid max-w-7xl gap-4 px-4 py-5 lg:grid-cols-[minmax(0,1fr)_340px]">
|
|
166
|
+
<div className="min-w-0">
|
|
167
|
+
<StorefrontHeroPanel
|
|
168
|
+
currency={currency}
|
|
169
|
+
labels={labels}
|
|
170
|
+
listingsCount={listings.length}
|
|
171
|
+
radius={radius}
|
|
172
|
+
storefront={storefront}
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<div
|
|
176
|
+
className={cn(
|
|
177
|
+
'mt-4',
|
|
178
|
+
storefront.layoutStyle === 'list'
|
|
179
|
+
? 'grid gap-3'
|
|
180
|
+
: 'grid gap-3 sm:grid-cols-2 xl:grid-cols-3',
|
|
181
|
+
storefront.layoutStyle === 'feature' &&
|
|
182
|
+
'[&>article:first-child]:sm:col-span-2'
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
{listingRows.length === 0 ? (
|
|
186
|
+
<StorefrontEmptyListings
|
|
187
|
+
action={emptyAction}
|
|
188
|
+
labels={labels}
|
|
189
|
+
radius={radius}
|
|
190
|
+
/>
|
|
191
|
+
) : (
|
|
192
|
+
listingRows.map((listing) => {
|
|
193
|
+
const line = cartLines.find(
|
|
194
|
+
(item) => item.listingId === listing.id
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<StorefrontListingCard
|
|
199
|
+
currency={currency}
|
|
200
|
+
isList={storefront.layoutStyle === 'list'}
|
|
201
|
+
key={listing.id}
|
|
202
|
+
labels={labels}
|
|
203
|
+
listing={listing}
|
|
204
|
+
onDecrement={onDecrement}
|
|
205
|
+
onIncrement={onIncrement}
|
|
206
|
+
quantity={line?.quantity ?? 0}
|
|
207
|
+
radius={radius}
|
|
208
|
+
showInventoryBadges={storefront.showInventoryBadges}
|
|
209
|
+
surfaceClassName={
|
|
210
|
+
storefrontSurfaceClasses[storefront.surfaceStyle]
|
|
211
|
+
}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
})
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<StorefrontCartSummary
|
|
220
|
+
cartEntries={checkoutEntries}
|
|
221
|
+
checkoutHref={checkoutHref}
|
|
222
|
+
currency={currency}
|
|
223
|
+
isCheckout={isCheckout}
|
|
224
|
+
isPreview={isPreview}
|
|
225
|
+
isSubmitting={isSubmitting}
|
|
226
|
+
labels={labels}
|
|
227
|
+
onCheckoutSubmit={onCheckoutSubmit}
|
|
228
|
+
radius={radius}
|
|
229
|
+
storefront={storefront}
|
|
230
|
+
total={total}
|
|
231
|
+
/>
|
|
232
|
+
</section>
|
|
233
|
+
</main>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { InventoryStorefrontListing } from '@tuturuuu/internal-api/inventory';
|
|
2
|
+
|
|
3
|
+
export type StorefrontCartLine = {
|
|
4
|
+
listingId: string;
|
|
5
|
+
quantity: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type StorefrontCartEntry = {
|
|
9
|
+
line: StorefrontCartLine;
|
|
10
|
+
listing: InventoryStorefrontListing;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type StorefrontSurfaceMode =
|
|
14
|
+
| 'cart'
|
|
15
|
+
| 'checkout'
|
|
16
|
+
| 'preview'
|
|
17
|
+
| 'product'
|
|
18
|
+
| 'store';
|
|
19
|
+
|
|
20
|
+
export type StorefrontSurfaceLabels = {
|
|
21
|
+
add: string;
|
|
22
|
+
available: string;
|
|
23
|
+
browse: string;
|
|
24
|
+
bundle: string;
|
|
25
|
+
cart: string;
|
|
26
|
+
checkout: string;
|
|
27
|
+
checkoutDisabled: string;
|
|
28
|
+
checkoutDisabledBadge: string;
|
|
29
|
+
demoBadge: string;
|
|
30
|
+
emptyCart: string;
|
|
31
|
+
emptyListingsDescription: string;
|
|
32
|
+
emptyListingsTitle: string;
|
|
33
|
+
fallbackDescription: string;
|
|
34
|
+
form: {
|
|
35
|
+
email: string;
|
|
36
|
+
name: string;
|
|
37
|
+
note: string;
|
|
38
|
+
phone: string;
|
|
39
|
+
};
|
|
40
|
+
privateStore: string;
|
|
41
|
+
previewBadge: string;
|
|
42
|
+
product: string;
|
|
43
|
+
publicStore: string;
|
|
44
|
+
quantity: string;
|
|
45
|
+
reserve: string;
|
|
46
|
+
reserving: string;
|
|
47
|
+
reservedCopy: string;
|
|
48
|
+
simulatedBadge: string;
|
|
49
|
+
soldOut: string;
|
|
50
|
+
total: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
54
|
+
add: 'Add',
|
|
55
|
+
available: 'available',
|
|
56
|
+
browse: 'Browse',
|
|
57
|
+
bundle: 'Bundle',
|
|
58
|
+
cart: 'Cart',
|
|
59
|
+
checkout: 'Checkout',
|
|
60
|
+
checkoutDisabled: 'Checkout is disabled in preview',
|
|
61
|
+
checkoutDisabledBadge: 'Checkout disabled',
|
|
62
|
+
demoBadge: 'Demo',
|
|
63
|
+
emptyCart: 'Add a listing to start checkout.',
|
|
64
|
+
emptyListingsDescription:
|
|
65
|
+
'Publish a listing to make this storefront ready for buyers.',
|
|
66
|
+
emptyListingsTitle: 'No listings yet',
|
|
67
|
+
fallbackDescription: 'This listing is available for checkout.',
|
|
68
|
+
form: {
|
|
69
|
+
email: 'Email',
|
|
70
|
+
name: 'Name',
|
|
71
|
+
note: 'Order note',
|
|
72
|
+
phone: 'Phone',
|
|
73
|
+
},
|
|
74
|
+
privateStore: 'Private',
|
|
75
|
+
previewBadge: 'Preview',
|
|
76
|
+
product: 'Product',
|
|
77
|
+
publicStore: 'Public',
|
|
78
|
+
quantity: 'Qty',
|
|
79
|
+
reserve: 'Reserve with Polar',
|
|
80
|
+
reserving: 'Reserving...',
|
|
81
|
+
reservedCopy:
|
|
82
|
+
'Review your cart, then continue with the available checkout mode.',
|
|
83
|
+
simulatedBadge: 'Simulated checkout',
|
|
84
|
+
soldOut: 'Sold out',
|
|
85
|
+
total: 'Total',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function mergeStorefrontSurfaceLabels(
|
|
89
|
+
labels?: Partial<StorefrontSurfaceLabels>
|
|
90
|
+
) {
|
|
91
|
+
return {
|
|
92
|
+
...defaultStorefrontSurfaceLabels,
|
|
93
|
+
...labels,
|
|
94
|
+
form: {
|
|
95
|
+
...defaultStorefrontSurfaceLabels.form,
|
|
96
|
+
...labels?.form,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InventoryStorefront,
|
|
3
|
+
InventoryStorefrontListing,
|
|
4
|
+
} from '@tuturuuu/internal-api/inventory';
|
|
5
|
+
import type { CSSProperties } from 'react';
|
|
6
|
+
|
|
7
|
+
export const storefrontRadiusClasses: Record<
|
|
8
|
+
InventoryStorefront['cornerStyle'],
|
|
9
|
+
string
|
|
10
|
+
> = {
|
|
11
|
+
compact: 'rounded-md',
|
|
12
|
+
rounded: 'rounded-lg',
|
|
13
|
+
soft: 'rounded-2xl',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const storefrontSurfaceClasses: Record<
|
|
17
|
+
InventoryStorefront['surfaceStyle'],
|
|
18
|
+
string
|
|
19
|
+
> = {
|
|
20
|
+
glass:
|
|
21
|
+
'border border-border/70 bg-card/75 shadow-sm shadow-foreground/5 backdrop-blur',
|
|
22
|
+
soft: 'border border-border/70 bg-muted/35 shadow-sm shadow-foreground/5',
|
|
23
|
+
solid: 'border border-border bg-card shadow-sm shadow-foreground/5',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type StorefrontAccentStyle = CSSProperties & {
|
|
27
|
+
'--storefront-accent'?: string;
|
|
28
|
+
'--storefront-accent-foreground'?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function sanitizeStorefrontAccentColor(value?: string | null) {
|
|
32
|
+
const normalized = value?.trim();
|
|
33
|
+
if (!normalized) return null;
|
|
34
|
+
|
|
35
|
+
if (/^#[0-9a-f]{3}$/i.test(normalized)) {
|
|
36
|
+
const [, r, g, b] = normalized;
|
|
37
|
+
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (/^#[0-9a-f]{6}$/i.test(normalized)) {
|
|
41
|
+
return normalized.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getStorefrontListingLimit(listing: InventoryStorefrontListing) {
|
|
48
|
+
const available =
|
|
49
|
+
typeof listing.availableQuantity === 'number'
|
|
50
|
+
? listing.availableQuantity
|
|
51
|
+
: Number.POSITIVE_INFINITY;
|
|
52
|
+
|
|
53
|
+
return Math.max(0, Math.min(listing.maxPerOrder, available));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatStorefrontPrice(value: number, currency: string) {
|
|
57
|
+
return new Intl.NumberFormat(undefined, {
|
|
58
|
+
currency,
|
|
59
|
+
maximumFractionDigits: 0,
|
|
60
|
+
style: 'currency',
|
|
61
|
+
}).format(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getListingInitials(title: string) {
|
|
65
|
+
return title
|
|
66
|
+
.split(/\s+/)
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.slice(0, 2)
|
|
69
|
+
.map((part) => part[0]?.toUpperCase())
|
|
70
|
+
.join('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getAccentStyle(
|
|
74
|
+
accentColor: string | null
|
|
75
|
+
): StorefrontAccentStyle {
|
|
76
|
+
if (!accentColor) return {};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
'--storefront-accent': accentColor,
|
|
80
|
+
'--storefront-accent-foreground': getAccentForeground(accentColor),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getAccentForeground(hexColor: string) {
|
|
85
|
+
const red = Number.parseInt(hexColor.slice(1, 3), 16);
|
|
86
|
+
const green = Number.parseInt(hexColor.slice(3, 5), 16);
|
|
87
|
+
const blue = Number.parseInt(hexColor.slice(5, 7), 16);
|
|
88
|
+
const luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255;
|
|
89
|
+
return luminance > 0.64 ? '#111111' : '#ffffff';
|
|
90
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
getTaskCardHydratingOpenOptions,
|
|
6
|
+
isExternalTaskSnapshot,
|
|
7
|
+
} from './task-card-open-options';
|
|
8
|
+
|
|
9
|
+
const task = {
|
|
10
|
+
id: 'task-1',
|
|
11
|
+
name: 'Visible task',
|
|
12
|
+
description: '',
|
|
13
|
+
list_id: 'list-1',
|
|
14
|
+
display_number: 7,
|
|
15
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
16
|
+
end_date: null,
|
|
17
|
+
priority: 'normal',
|
|
18
|
+
} satisfies Task;
|
|
19
|
+
|
|
20
|
+
const list = {
|
|
21
|
+
id: 'list-1',
|
|
22
|
+
name: 'To Do',
|
|
23
|
+
board_id: 'board-1',
|
|
24
|
+
position: 0,
|
|
25
|
+
status: 'not_started',
|
|
26
|
+
color: 'BLUE',
|
|
27
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
28
|
+
creator_id: 'user-1',
|
|
29
|
+
archived: false,
|
|
30
|
+
deleted: false,
|
|
31
|
+
} satisfies TaskList;
|
|
32
|
+
|
|
33
|
+
describe('getTaskCardHydratingOpenOptions', () => {
|
|
34
|
+
it('opens external task cards from the visible snapshot while hydrating source details', () => {
|
|
35
|
+
const externalTask = {
|
|
36
|
+
...task,
|
|
37
|
+
list_id: 'personal-list',
|
|
38
|
+
source_workspace_id: 'source-workspace',
|
|
39
|
+
source_workspace_name: 'Source workspace',
|
|
40
|
+
source_board_id: 'source-board',
|
|
41
|
+
source_board_name: 'Source board',
|
|
42
|
+
source_list_id: 'source-list',
|
|
43
|
+
source_list_name: 'Doing',
|
|
44
|
+
source_list_status: 'active',
|
|
45
|
+
ticket_prefix: 'SRC',
|
|
46
|
+
} satisfies Task & { ticket_prefix: string };
|
|
47
|
+
|
|
48
|
+
expect(
|
|
49
|
+
getTaskCardHydratingOpenOptions({
|
|
50
|
+
task: externalTask,
|
|
51
|
+
boardId: 'personal-board',
|
|
52
|
+
availableLists: [list],
|
|
53
|
+
effectiveWorkspaceId: 'personal-workspace',
|
|
54
|
+
isPersonalWorkspace: true,
|
|
55
|
+
})
|
|
56
|
+
).toEqual({
|
|
57
|
+
initialTask: {
|
|
58
|
+
...externalTask,
|
|
59
|
+
list_id: 'source-list',
|
|
60
|
+
},
|
|
61
|
+
boardId: 'source-board',
|
|
62
|
+
availableLists: [
|
|
63
|
+
{
|
|
64
|
+
id: 'source-list',
|
|
65
|
+
name: 'Doing',
|
|
66
|
+
board_id: 'source-board',
|
|
67
|
+
position: 0,
|
|
68
|
+
status: 'active',
|
|
69
|
+
color: 'GRAY',
|
|
70
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
71
|
+
creator_id: '',
|
|
72
|
+
archived: false,
|
|
73
|
+
deleted: false,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
taskWsId: 'source-workspace',
|
|
77
|
+
taskWorkspacePersonal: false,
|
|
78
|
+
initialSharedContext: {
|
|
79
|
+
boardConfig: {
|
|
80
|
+
id: 'source-board',
|
|
81
|
+
name: 'Source board',
|
|
82
|
+
ws_id: 'source-workspace',
|
|
83
|
+
ticket_prefix: 'SRC',
|
|
84
|
+
},
|
|
85
|
+
availableLists: [
|
|
86
|
+
{
|
|
87
|
+
id: 'source-list',
|
|
88
|
+
name: 'Doing',
|
|
89
|
+
board_id: 'source-board',
|
|
90
|
+
position: 0,
|
|
91
|
+
status: 'active',
|
|
92
|
+
color: 'GRAY',
|
|
93
|
+
created_at: '2026-06-12T00:00:00.000Z',
|
|
94
|
+
creator_id: '',
|
|
95
|
+
archived: false,
|
|
96
|
+
deleted: false,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
workspaceLabels: [],
|
|
100
|
+
workspaceMembers: [],
|
|
101
|
+
workspaceProjects: [],
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('keeps local board metadata for non-source personal tasks', () => {
|
|
107
|
+
expect(
|
|
108
|
+
getTaskCardHydratingOpenOptions({
|
|
109
|
+
task,
|
|
110
|
+
boardId: 'board-1',
|
|
111
|
+
availableLists: [list],
|
|
112
|
+
effectiveWorkspaceId: 'workspace-1',
|
|
113
|
+
isPersonalWorkspace: true,
|
|
114
|
+
})
|
|
115
|
+
).toEqual({
|
|
116
|
+
initialTask: task,
|
|
117
|
+
boardId: 'board-1',
|
|
118
|
+
availableLists: [list],
|
|
119
|
+
taskWsId: 'workspace-1',
|
|
120
|
+
taskWorkspacePersonal: true,
|
|
121
|
+
initialSharedContext: undefined,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('treats source metadata as an external task snapshot', () => {
|
|
126
|
+
expect(
|
|
127
|
+
isExternalTaskSnapshot({
|
|
128
|
+
...task,
|
|
129
|
+
source_workspace_id: 'source-workspace',
|
|
130
|
+
})
|
|
131
|
+
).toBe(true);
|
|
132
|
+
expect(isExternalTaskSnapshot(task)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
3
|
+
import type { SharedTaskContext } from '../../../shared/task-edit-dialog/hooks/use-task-data';
|
|
4
|
+
|
|
5
|
+
interface TaskCardOpenOptionsInput {
|
|
6
|
+
task: Task;
|
|
7
|
+
boardId: string;
|
|
8
|
+
availableLists?: TaskList[];
|
|
9
|
+
effectiveWorkspaceId?: string;
|
|
10
|
+
isPersonalWorkspace: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isExternalTaskSnapshot(task: Task) {
|
|
14
|
+
return (
|
|
15
|
+
task.is_personal_external === true ||
|
|
16
|
+
Boolean(task.personal_board_id) ||
|
|
17
|
+
Boolean(task.source_workspace_id) ||
|
|
18
|
+
Boolean(task.source_board_id)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeSourceListStatus(status?: string | null): TaskList['status'] {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'documents':
|
|
25
|
+
case 'not_started':
|
|
26
|
+
case 'active':
|
|
27
|
+
case 'review':
|
|
28
|
+
case 'done':
|
|
29
|
+
case 'closed':
|
|
30
|
+
return status;
|
|
31
|
+
default:
|
|
32
|
+
return 'not_started';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getTaskTicketPrefix(task: Task) {
|
|
37
|
+
return 'ticket_prefix' in task && typeof task.ticket_prefix === 'string'
|
|
38
|
+
? task.ticket_prefix
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildInitialSourceList(
|
|
43
|
+
task: Task,
|
|
44
|
+
sourceBoardId: string
|
|
45
|
+
): TaskList | undefined {
|
|
46
|
+
if (!task.source_list_id) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: task.source_list_id,
|
|
52
|
+
name: task.source_list_name ?? task.source_list_id,
|
|
53
|
+
archived: false,
|
|
54
|
+
deleted: false,
|
|
55
|
+
created_at: task.created_at,
|
|
56
|
+
board_id: sourceBoardId,
|
|
57
|
+
creator_id: '',
|
|
58
|
+
status: normalizeSourceListStatus(task.source_list_status),
|
|
59
|
+
color: 'GRAY',
|
|
60
|
+
position: 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildInitialSourceContext(
|
|
65
|
+
task: Task,
|
|
66
|
+
sourceWorkspaceId?: string,
|
|
67
|
+
sourceBoardId?: string
|
|
68
|
+
): SharedTaskContext | undefined {
|
|
69
|
+
if (!sourceBoardId) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sourceList = buildInitialSourceList(task, sourceBoardId);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
boardConfig: {
|
|
77
|
+
id: sourceBoardId,
|
|
78
|
+
name: task.source_board_name ?? sourceBoardId,
|
|
79
|
+
ws_id: sourceWorkspaceId,
|
|
80
|
+
ticket_prefix: getTaskTicketPrefix(task),
|
|
81
|
+
},
|
|
82
|
+
availableLists: sourceList ? [sourceList] : undefined,
|
|
83
|
+
workspaceLabels: task.labels ?? [],
|
|
84
|
+
workspaceMembers:
|
|
85
|
+
task.assignees?.map((assignee) => ({
|
|
86
|
+
id: assignee.id,
|
|
87
|
+
user_id: assignee.id,
|
|
88
|
+
display_name: assignee.display_name ?? assignee.email ?? assignee.id,
|
|
89
|
+
avatar_url: assignee.avatar_url ?? null,
|
|
90
|
+
})) ?? [],
|
|
91
|
+
workspaceProjects: task.projects ?? [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getTaskCardHydratingOpenOptions({
|
|
96
|
+
task,
|
|
97
|
+
boardId,
|
|
98
|
+
availableLists,
|
|
99
|
+
effectiveWorkspaceId,
|
|
100
|
+
isPersonalWorkspace,
|
|
101
|
+
}: TaskCardOpenOptionsInput) {
|
|
102
|
+
const sourceWorkspaceId = task.source_workspace_id;
|
|
103
|
+
const sourceBoardId = task.source_board_id;
|
|
104
|
+
const initialSharedContext = buildInitialSourceContext(
|
|
105
|
+
task,
|
|
106
|
+
sourceWorkspaceId ?? undefined,
|
|
107
|
+
sourceBoardId ?? undefined
|
|
108
|
+
);
|
|
109
|
+
const initialTask = {
|
|
110
|
+
...task,
|
|
111
|
+
list_id: task.source_list_id ?? task.list_id,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
initialTask,
|
|
116
|
+
boardId: sourceBoardId ?? boardId,
|
|
117
|
+
availableLists:
|
|
118
|
+
sourceBoardId && initialSharedContext?.availableLists
|
|
119
|
+
? initialSharedContext.availableLists
|
|
120
|
+
: sourceBoardId
|
|
121
|
+
? undefined
|
|
122
|
+
: availableLists,
|
|
123
|
+
taskWsId: sourceWorkspaceId ?? effectiveWorkspaceId,
|
|
124
|
+
taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
|
|
125
|
+
initialSharedContext,
|
|
126
|
+
};
|
|
127
|
+
}
|