@m5kdev/web-ui 0.1.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/LICENSE +621 -0
- package/README.md +17 -0
- package/package.json +169 -0
- package/src/animations/card.motion.ts +9 -0
- package/src/components/AvatarUpload.tsx +133 -0
- package/src/components/Button.tsx +14 -0
- package/src/components/Calendar.css +684 -0
- package/src/components/Calendar.tsx +32 -0
- package/src/components/CardsSelect.tsx +155 -0
- package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
- package/src/components/ColorPicker.tsx +56 -0
- package/src/components/CopyButton.tsx +45 -0
- package/src/components/CropDialog.tsx +154 -0
- package/src/components/DialogProvider.tsx +105 -0
- package/src/components/ErrorFallback.tsx +17 -0
- package/src/components/FileDropzone.tsx +120 -0
- package/src/components/MultiSelectDropdown.tsx +233 -0
- package/src/components/Orb.tsx +288 -0
- package/src/components/PageAlert.tsx +121 -0
- package/src/components/SelectChips.tsx +40 -0
- package/src/components/SidebarItem.tsx +26 -0
- package/src/components/Steps.tsx +340 -0
- package/src/components/TablerIconPicker.tsx +4260 -0
- package/src/components/app-header.tsx +40 -0
- package/src/components/blur-card.tsx +132 -0
- package/src/components/features-section-demo-1.tsx +127 -0
- package/src/components/features-section-demo-2.tsx +102 -0
- package/src/components/features-section-demo-3.tsx +272 -0
- package/src/components/mode-toggle.tsx +31 -0
- package/src/components/nav-main.tsx +69 -0
- package/src/components/pricing-cards.tsx +133 -0
- package/src/components/shared/ButtonCopy.tsx +50 -0
- package/src/components/team-switcher.tsx +83 -0
- package/src/components/theme-provider.tsx +74 -0
- package/src/components/typewriter.tsx +90 -0
- package/src/components/ui/alert-dialog.tsx +133 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/bento-grid.tsx +54 -0
- package/src/components/ui/bento-grid2.tsx +66 -0
- package/src/components/ui/breadcrumb.tsx +101 -0
- package/src/components/ui/button.tsx +50 -0
- package/src/components/ui/card.tsx +55 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +119 -0
- package/src/components/ui/dropdown-menu.tsx +186 -0
- package/src/components/ui/floating-navbar.tsx +78 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/image.tsx +55 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/pagination.tsx +105 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/resizable-navbar.tsx +260 -0
- package/src/components/ui/segment-control.tsx +143 -0
- package/src/components/ui/select.tsx +153 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +121 -0
- package/src/components/ui/sidebar.tsx +736 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/spinner.tsx +45 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +90 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/timeline.tsx +95 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/ui/typewriter-effect.tsx +181 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/useDialog.ts +25 -0
- package/src/icons/GoogleIcon.tsx +32 -0
- package/src/icons/LinkedInIcon.tsx +30 -0
- package/src/icons/MicrosoftIcon.tsx +21 -0
- package/src/lib/chatwoot.ts +51 -0
- package/src/lib/utils.ts +6 -0
- package/src/modules/app/components/AppLoader.tsx +9 -0
- package/src/modules/app/components/AppShell.tsx +21 -0
- package/src/modules/app/components/AppSidebar.tsx +26 -0
- package/src/modules/app/components/AppSidebarContent.tsx +73 -0
- package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
- package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
- package/src/modules/app/components/AppSidebarUser.tsx +128 -0
- package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
- package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
- package/src/modules/auth/components/AuthLayout.tsx +13 -0
- package/src/modules/auth/components/AuthProviders.tsx +105 -0
- package/src/modules/auth/components/AuthRouter.tsx +29 -0
- package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
- package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
- package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
- package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/InviteFriends.tsx +273 -0
- package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
- package/src/modules/auth/components/LoginForm.tsx +104 -0
- package/src/modules/auth/components/LoginRoute.tsx +31 -0
- package/src/modules/auth/components/LogoutRoute.tsx +21 -0
- package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
- package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
- package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
- package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
- package/src/modules/auth/components/ProfileRoute.tsx +104 -0
- package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
- package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
- package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
- package/src/modules/auth/components/SignupRoute.tsx +53 -0
- package/src/modules/auth/components/UserPreferences.tsx +144 -0
- package/src/modules/auth/components/WaitlistCard.tsx +78 -0
- package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
- package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
- package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
- package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
- package/src/modules/billing/components/BillingRouter.tsx +20 -0
- package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
- package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
- package/src/modules/table/components/NuqsTable.tsx +396 -0
- package/src/modules/table/components/TableFiltering.tsx +520 -0
- package/src/modules/table/components/TablePagination.tsx +59 -0
- package/src/modules/table/components/table.types.ts +11 -0
- package/src/modules/table/filterTransformers.ts +323 -0
- package/src/types.ts +4 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DatePicker,
|
|
3
|
+
DateRangePicker,
|
|
4
|
+
type DateValue,
|
|
5
|
+
Input,
|
|
6
|
+
Select,
|
|
7
|
+
SelectItem,
|
|
8
|
+
type SharedSelection,
|
|
9
|
+
} from "@heroui/react";
|
|
10
|
+
import { getLocalTimeZone, today } from "@internationalized/date";
|
|
11
|
+
import type { QueryFilters } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
12
|
+
import type {
|
|
13
|
+
ColumnDataType,
|
|
14
|
+
ComponentForFilterMethod,
|
|
15
|
+
FilterMethod,
|
|
16
|
+
FilterMethods,
|
|
17
|
+
} from "@m5kdev/commons/modules/table/filter.types";
|
|
18
|
+
import { PlusIcon, XIcon } from "lucide-react";
|
|
19
|
+
import type { ReactNode } from "react";
|
|
20
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
21
|
+
import { Button } from "#components/ui/button";
|
|
22
|
+
import {
|
|
23
|
+
type FilterValue,
|
|
24
|
+
type HeroUIFilter,
|
|
25
|
+
transformFiltersFromHeroUI,
|
|
26
|
+
transformFiltersToHeroUI,
|
|
27
|
+
} from "../filterTransformers";
|
|
28
|
+
|
|
29
|
+
type ComponentRenderer = (
|
|
30
|
+
value: FilterValue,
|
|
31
|
+
onChange: (value: FilterValue) => void,
|
|
32
|
+
options?: { label: string; value: string }[]
|
|
33
|
+
) => ReactNode;
|
|
34
|
+
|
|
35
|
+
const componentForFilterMethod: Record<ComponentForFilterMethod, ComponentRenderer> = {
|
|
36
|
+
text: (value, onChange) => (
|
|
37
|
+
<Input
|
|
38
|
+
size="sm"
|
|
39
|
+
aria-label="Select Value"
|
|
40
|
+
className="flex-1 min-w-0 text-sm"
|
|
41
|
+
value={(value as string) ?? ""}
|
|
42
|
+
onChange={(e) => onChange(e.target.value)}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
number: (value, onChange) => (
|
|
46
|
+
<Input
|
|
47
|
+
size="sm"
|
|
48
|
+
aria-label="Select Value"
|
|
49
|
+
type="number"
|
|
50
|
+
className="flex-1 min-w-0 text-sm"
|
|
51
|
+
value={(value as number | null)?.toString() ?? ""}
|
|
52
|
+
onChange={(e) => onChange(Number.parseFloat(e.target.value) || 0)}
|
|
53
|
+
/>
|
|
54
|
+
),
|
|
55
|
+
date: (value, onChange) => (
|
|
56
|
+
<DatePicker
|
|
57
|
+
size="sm"
|
|
58
|
+
aria-label="Select Value"
|
|
59
|
+
className="flex-1 min-w-0 text-sm"
|
|
60
|
+
value={(value as any) ?? undefined}
|
|
61
|
+
onChange={(date) => date && onChange(date as FilterValue)}
|
|
62
|
+
maxValue={today(getLocalTimeZone()) as unknown as DateValue}
|
|
63
|
+
/>
|
|
64
|
+
),
|
|
65
|
+
range: (value, onChange) => (
|
|
66
|
+
<DateRangePicker
|
|
67
|
+
size="sm"
|
|
68
|
+
aria-label="Select Value"
|
|
69
|
+
className="flex-1 min-w-0 text-sm"
|
|
70
|
+
value={(value as any) ?? undefined}
|
|
71
|
+
onChange={(range) => range && onChange(range as FilterValue)}
|
|
72
|
+
maxValue={today(getLocalTimeZone()) as unknown as DateValue}
|
|
73
|
+
/>
|
|
74
|
+
),
|
|
75
|
+
radio: (value, onChange) => (
|
|
76
|
+
<Select
|
|
77
|
+
size="sm"
|
|
78
|
+
aria-label="Select Value"
|
|
79
|
+
className="flex-1 min-w-0 text-sm"
|
|
80
|
+
selectedKeys={(value as boolean | null) ? ["true"] : ["false"]}
|
|
81
|
+
onSelectionChange={(keys) => onChange(keys.currentKey === "true")}
|
|
82
|
+
>
|
|
83
|
+
<SelectItem key="true" className="text-sm">
|
|
84
|
+
True
|
|
85
|
+
</SelectItem>
|
|
86
|
+
<SelectItem key="false" className="text-sm">
|
|
87
|
+
False
|
|
88
|
+
</SelectItem>
|
|
89
|
+
</Select>
|
|
90
|
+
),
|
|
91
|
+
select: (value, onChange, options = []) => (
|
|
92
|
+
<Select
|
|
93
|
+
size="sm"
|
|
94
|
+
aria-label="Select Value"
|
|
95
|
+
className="flex-1 min-w-0 text-sm"
|
|
96
|
+
selectedKeys={(value as SharedSelection) ?? new Set()}
|
|
97
|
+
onSelectionChange={(keys) => keys && onChange(keys as FilterValue)}
|
|
98
|
+
>
|
|
99
|
+
{options.map((option) => (
|
|
100
|
+
<SelectItem key={option.value} className="text-sm">
|
|
101
|
+
{option.label}
|
|
102
|
+
</SelectItem>
|
|
103
|
+
))}
|
|
104
|
+
</Select>
|
|
105
|
+
),
|
|
106
|
+
multiSelect: (value, onChange, options = []) => (
|
|
107
|
+
<Select
|
|
108
|
+
size="sm"
|
|
109
|
+
aria-label="Select Value"
|
|
110
|
+
selectionMode="multiple"
|
|
111
|
+
className="flex-1 min-w-0 text-sm"
|
|
112
|
+
selectedKeys={value ? new Set(value as SharedSelection) : new Set()}
|
|
113
|
+
onSelectionChange={(keys) => onChange(keys as FilterValue)}
|
|
114
|
+
>
|
|
115
|
+
{options.map((option) => (
|
|
116
|
+
<SelectItem key={option.value} className="text-sm">
|
|
117
|
+
{option.label}
|
|
118
|
+
</SelectItem>
|
|
119
|
+
))}
|
|
120
|
+
</Select>
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const defaultFilterMethods: FilterMethods = {
|
|
125
|
+
string: [
|
|
126
|
+
{ value: "contains", label: "Contains", component: "text" },
|
|
127
|
+
{ value: "equals", label: "Equals", component: "text" },
|
|
128
|
+
{ value: "starts_with", label: "Starts With", component: "text" },
|
|
129
|
+
{ value: "ends_with", label: "Ends With", component: "text" },
|
|
130
|
+
],
|
|
131
|
+
number: [
|
|
132
|
+
{ value: "equals", label: "Equals", component: "number" },
|
|
133
|
+
{ value: "greater_than", label: "Greater Than", component: "number" },
|
|
134
|
+
{ value: "less_than", label: "Less Than", component: "number" },
|
|
135
|
+
],
|
|
136
|
+
date: [
|
|
137
|
+
{ value: "on", label: "On", component: "date" },
|
|
138
|
+
{ value: "between", label: "Between", component: "range" },
|
|
139
|
+
{ value: "before", label: "Before", component: "date" },
|
|
140
|
+
{ value: "after", label: "After", component: "date" },
|
|
141
|
+
{ value: "intersect", label: "During", component: "range" },
|
|
142
|
+
],
|
|
143
|
+
boolean: [{ value: "equals", label: "Equals", component: "radio" }],
|
|
144
|
+
enum: [
|
|
145
|
+
{ value: "oneOf", label: "One Of", component: "multiSelect" },
|
|
146
|
+
{ value: "equals", label: "Equals", component: "select" },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const SINGLE_FILTER_KEY = "single-filter";
|
|
151
|
+
|
|
152
|
+
type TableFilteringProps = {
|
|
153
|
+
columns: {
|
|
154
|
+
id: string;
|
|
155
|
+
label: string;
|
|
156
|
+
type?: ColumnDataType | null;
|
|
157
|
+
options?: { label: string; value: string }[] | null;
|
|
158
|
+
endColumnId?: string | null;
|
|
159
|
+
periodStartColumnId?: string | null;
|
|
160
|
+
periodEndColumnId?: string | null;
|
|
161
|
+
}[];
|
|
162
|
+
onFiltersChange: (filters: QueryFilters) => void;
|
|
163
|
+
filters: QueryFilters;
|
|
164
|
+
onClose?: () => void;
|
|
165
|
+
singleFilter?: boolean;
|
|
166
|
+
filterMethods?: Partial<FilterMethods>;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const mergeFilterMethods = (overrides?: Partial<FilterMethods>): FilterMethods => ({
|
|
170
|
+
string:
|
|
171
|
+
overrides?.string && overrides.string.length > 0
|
|
172
|
+
? overrides.string
|
|
173
|
+
: defaultFilterMethods.string,
|
|
174
|
+
number:
|
|
175
|
+
overrides?.number && overrides.number.length > 0
|
|
176
|
+
? overrides.number
|
|
177
|
+
: defaultFilterMethods.number,
|
|
178
|
+
date: overrides?.date && overrides.date.length > 0 ? overrides.date : defaultFilterMethods.date,
|
|
179
|
+
boolean:
|
|
180
|
+
overrides?.boolean && overrides.boolean.length > 0
|
|
181
|
+
? overrides.boolean
|
|
182
|
+
: defaultFilterMethods.boolean,
|
|
183
|
+
enum: overrides?.enum && overrides.enum.length > 0 ? overrides.enum : defaultFilterMethods.enum,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export const TableFiltering = ({
|
|
187
|
+
columns,
|
|
188
|
+
onFiltersChange,
|
|
189
|
+
filters: initialFilters = [],
|
|
190
|
+
onClose,
|
|
191
|
+
singleFilter = false,
|
|
192
|
+
filterMethods,
|
|
193
|
+
}: TableFilteringProps) => {
|
|
194
|
+
const [filters, setFilters] = useState<Record<string, HeroUIFilter>>({});
|
|
195
|
+
const effectiveFilterMethods = useMemo(() => mergeFilterMethods(filterMethods), [filterMethods]);
|
|
196
|
+
|
|
197
|
+
const createEmptyFilter = useCallback(
|
|
198
|
+
(): HeroUIFilter => ({
|
|
199
|
+
columnId: "",
|
|
200
|
+
type: null,
|
|
201
|
+
value: null,
|
|
202
|
+
method: null,
|
|
203
|
+
options: null,
|
|
204
|
+
endColumnId: null,
|
|
205
|
+
periodStartColumnId: null,
|
|
206
|
+
periodEndColumnId: null,
|
|
207
|
+
}),
|
|
208
|
+
[]
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const addFilter = useCallback(() => {
|
|
212
|
+
setFilters((prev) => {
|
|
213
|
+
if (!singleFilter) {
|
|
214
|
+
const filterId = crypto.randomUUID();
|
|
215
|
+
return {
|
|
216
|
+
...prev,
|
|
217
|
+
[filterId]: createEmptyFilter(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// single filter mode
|
|
222
|
+
return Object.keys(prev).length > 0 ? prev : { [SINGLE_FILTER_KEY]: createEmptyFilter() };
|
|
223
|
+
});
|
|
224
|
+
}, [createEmptyFilter, singleFilter]);
|
|
225
|
+
|
|
226
|
+
const removeFilter = useCallback(
|
|
227
|
+
(filterId: string) => {
|
|
228
|
+
setFilters((prev) => {
|
|
229
|
+
if (!singleFilter) {
|
|
230
|
+
const { [filterId]: _, ...rest } = prev;
|
|
231
|
+
return rest;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// single filter mode resets the lone filter
|
|
235
|
+
return { [SINGLE_FILTER_KEY]: createEmptyFilter() };
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
[createEmptyFilter, singleFilter]
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const columnsMap = useMemo(
|
|
242
|
+
() => new Map(columns.map((column) => [column.id, column])),
|
|
243
|
+
[columns]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
// Transform initialFilters from FiltersToApply format to HeroUIFilter format
|
|
248
|
+
const transformedFilters = transformFiltersToHeroUI(
|
|
249
|
+
initialFilters,
|
|
250
|
+
columnsMap,
|
|
251
|
+
effectiveFilterMethods
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (!singleFilter) {
|
|
255
|
+
setFilters(Object.fromEntries(transformedFilters.map((filter) => [filter.columnId, filter])));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const firstFilter = transformedFilters[0];
|
|
260
|
+
setFilters({
|
|
261
|
+
[SINGLE_FILTER_KEY]: firstFilter ?? createEmptyFilter(),
|
|
262
|
+
});
|
|
263
|
+
}, [createEmptyFilter, initialFilters, singleFilter, columnsMap, effectiveFilterMethods]);
|
|
264
|
+
|
|
265
|
+
const selectColumn = useCallback(
|
|
266
|
+
(filterId: string, columnId: string) => {
|
|
267
|
+
setFilters((prev) => {
|
|
268
|
+
const oldFilter = prev[filterId];
|
|
269
|
+
if (!oldFilter) return prev;
|
|
270
|
+
const column = columnsMap.get(columnId);
|
|
271
|
+
if (!column) return prev;
|
|
272
|
+
|
|
273
|
+
// If Period column, auto-set intersect method
|
|
274
|
+
const isPeriodColumn = columnId.endsWith("__period");
|
|
275
|
+
const intersectMethod = isPeriodColumn
|
|
276
|
+
? (effectiveFilterMethods.date.find((m) => m.value === "intersect") ?? null)
|
|
277
|
+
: null;
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
...prev,
|
|
281
|
+
[filterId]: {
|
|
282
|
+
...oldFilter,
|
|
283
|
+
columnId,
|
|
284
|
+
type: column.type ?? null,
|
|
285
|
+
options: column.options ?? null,
|
|
286
|
+
endColumnId: column.endColumnId ?? null,
|
|
287
|
+
periodStartColumnId: column.periodStartColumnId ?? null,
|
|
288
|
+
periodEndColumnId: column.periodEndColumnId ?? null,
|
|
289
|
+
method: intersectMethod,
|
|
290
|
+
value: null,
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
[columnsMap, effectiveFilterMethods]
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const selectMethod = useCallback((filterId: string, method: FilterMethod) => {
|
|
299
|
+
setFilters((prev) => {
|
|
300
|
+
const oldFilter = prev[filterId];
|
|
301
|
+
if (!oldFilter) return prev;
|
|
302
|
+
return {
|
|
303
|
+
...prev,
|
|
304
|
+
[filterId]: { ...oldFilter, method, value: null },
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}, []);
|
|
308
|
+
|
|
309
|
+
const selectValue = useCallback((filterId: string, value: FilterValue) => {
|
|
310
|
+
setFilters((prev) => {
|
|
311
|
+
const oldFilter = prev[filterId];
|
|
312
|
+
if (!oldFilter) return prev;
|
|
313
|
+
return {
|
|
314
|
+
...prev,
|
|
315
|
+
[filterId]: { ...oldFilter, value },
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
const filterEntries = useMemo(() => Object.entries(filters), [filters]);
|
|
321
|
+
|
|
322
|
+
const availableColumnsMap = useMemo(() => {
|
|
323
|
+
const map = new Map<string, typeof columns>();
|
|
324
|
+
filterEntries.forEach(([filterId]) => {
|
|
325
|
+
const columnsUsedByOtherFilters = new Set(
|
|
326
|
+
filterEntries
|
|
327
|
+
.filter(([id]) => id !== filterId)
|
|
328
|
+
.map(([_, f]) => f.columnId)
|
|
329
|
+
.filter((id) => id !== "")
|
|
330
|
+
);
|
|
331
|
+
map.set(
|
|
332
|
+
filterId,
|
|
333
|
+
columns.filter((column) => !columnsUsedByOtherFilters.has(column.id))
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
return map;
|
|
337
|
+
}, [filterEntries, columns]);
|
|
338
|
+
|
|
339
|
+
const applyFilters = useCallback(() => {
|
|
340
|
+
const heroUIFilters = Object.values(filters);
|
|
341
|
+
const filtersToApply = transformFiltersFromHeroUI(heroUIFilters);
|
|
342
|
+
onFiltersChange(filtersToApply);
|
|
343
|
+
onClose?.();
|
|
344
|
+
}, [filters, onFiltersChange, onClose]);
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div className="flex flex-col gap-2 p-1 min-w-[600px]">
|
|
348
|
+
{filterEntries.map(([filterId, filter]) => {
|
|
349
|
+
const availableColumns = availableColumnsMap.get(filterId) ?? columns;
|
|
350
|
+
return (
|
|
351
|
+
<TableFilteringItem
|
|
352
|
+
key={filterId}
|
|
353
|
+
id={filterId}
|
|
354
|
+
filter={filter}
|
|
355
|
+
columns={availableColumns}
|
|
356
|
+
selectColumn={selectColumn}
|
|
357
|
+
selectMethod={selectMethod}
|
|
358
|
+
removeFilter={removeFilter}
|
|
359
|
+
selectValue={selectValue}
|
|
360
|
+
filterMethods={effectiveFilterMethods}
|
|
361
|
+
/>
|
|
362
|
+
);
|
|
363
|
+
})}
|
|
364
|
+
{!singleFilter && (
|
|
365
|
+
<Button variant="outline" size="sm" onClick={addFilter}>
|
|
366
|
+
<PlusIcon className="h-4 w-4" />
|
|
367
|
+
Add Filter
|
|
368
|
+
</Button>
|
|
369
|
+
)}
|
|
370
|
+
<Button onClick={applyFilters}>{singleFilter ? "Apply Filter" : "Apply Filters"}</Button>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const TableFilteringItem = ({
|
|
376
|
+
id,
|
|
377
|
+
filter,
|
|
378
|
+
columns,
|
|
379
|
+
selectColumn,
|
|
380
|
+
selectMethod,
|
|
381
|
+
selectValue,
|
|
382
|
+
removeFilter,
|
|
383
|
+
filterMethods,
|
|
384
|
+
}: {
|
|
385
|
+
id: string;
|
|
386
|
+
filter: HeroUIFilter;
|
|
387
|
+
columns: {
|
|
388
|
+
id: string;
|
|
389
|
+
label: string;
|
|
390
|
+
type?: ColumnDataType | null;
|
|
391
|
+
options?: { label: string; value: string }[] | null;
|
|
392
|
+
endColumnId?: string | null;
|
|
393
|
+
periodStartColumnId?: string | null;
|
|
394
|
+
periodEndColumnId?: string | null;
|
|
395
|
+
}[];
|
|
396
|
+
selectColumn: (filterId: string, columnId: string) => void;
|
|
397
|
+
selectMethod: (filterId: string, method: FilterMethod) => void;
|
|
398
|
+
selectValue: (filterId: string, value: FilterValue) => void;
|
|
399
|
+
removeFilter: (filterId: string) => void;
|
|
400
|
+
filterMethods: FilterMethods;
|
|
401
|
+
}) => {
|
|
402
|
+
const handleColumnChange = useCallback(
|
|
403
|
+
(keys: any) => {
|
|
404
|
+
const columnId = String(keys.currentKey);
|
|
405
|
+
selectColumn(id, columnId);
|
|
406
|
+
},
|
|
407
|
+
[id, selectColumn]
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const methodsForType = useMemo(() => {
|
|
411
|
+
if (!filter.type) return [];
|
|
412
|
+
|
|
413
|
+
// Period columns only support intersect method
|
|
414
|
+
const isPeriodColumn = filter.columnId.endsWith("__period");
|
|
415
|
+
if (isPeriodColumn) {
|
|
416
|
+
const intersectMethod = filterMethods.date.find((m) => m.value === "intersect");
|
|
417
|
+
return intersectMethod ? [intersectMethod] : [];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const baseMethods = filterMethods[filter.type as ColumnDataType] ?? [];
|
|
421
|
+
const emptyMethods: FilterMethod[] = [
|
|
422
|
+
{ value: "isEmpty", label: "Is Empty", component: null },
|
|
423
|
+
{ value: "isNotEmpty", label: "Is Not Empty", component: null },
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
if (filter.type !== "boolean" && filter.type !== "date") {
|
|
427
|
+
return [...baseMethods, ...emptyMethods];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return baseMethods;
|
|
431
|
+
}, [filter.type, filter.columnId, filter.method?.value, filterMethods]);
|
|
432
|
+
|
|
433
|
+
const handleMethodChange = useCallback(
|
|
434
|
+
(keys: any) => {
|
|
435
|
+
if (!filter.type) return;
|
|
436
|
+
// Use methodsForType instead of filterMethods to include dynamically added methods
|
|
437
|
+
const method = methodsForType.find(
|
|
438
|
+
(currentMethod: FilterMethod) => currentMethod.value === String(keys.currentKey)
|
|
439
|
+
);
|
|
440
|
+
if (method) {
|
|
441
|
+
selectMethod(id, method);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
[id, filter.type, selectMethod, methodsForType]
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const handleValueChange = useCallback(
|
|
448
|
+
(value: FilterValue) => {
|
|
449
|
+
selectValue(id, value);
|
|
450
|
+
},
|
|
451
|
+
[id, selectValue]
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const methodSelect = useMemo(() => {
|
|
455
|
+
if (!filter.type) return null;
|
|
456
|
+
return (
|
|
457
|
+
<Select
|
|
458
|
+
size="sm"
|
|
459
|
+
aria-label="Select Method"
|
|
460
|
+
className="w-40 flex-shrink-0 text-sm"
|
|
461
|
+
selectedKeys={filter.method?.value ? [filter.method.value] : []}
|
|
462
|
+
onSelectionChange={handleMethodChange}
|
|
463
|
+
popoverProps={{
|
|
464
|
+
className: "w-auto min-w-max",
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
{methodsForType.map((method: FilterMethod) => (
|
|
468
|
+
<SelectItem key={method.value} className="text-sm">
|
|
469
|
+
{method.label}
|
|
470
|
+
</SelectItem>
|
|
471
|
+
))}
|
|
472
|
+
</Select>
|
|
473
|
+
);
|
|
474
|
+
}, [filter.type, filter.method?.value, methodsForType, handleMethodChange]);
|
|
475
|
+
|
|
476
|
+
const filterValueComponent = useMemo(() => {
|
|
477
|
+
if (!filter.method?.component) {
|
|
478
|
+
return <div className="flex-1 min-w-0" />;
|
|
479
|
+
}
|
|
480
|
+
const component = filter.method.component as ComponentForFilterMethod;
|
|
481
|
+
const ComponentFn =
|
|
482
|
+
componentForFilterMethod[component as keyof typeof componentForFilterMethod];
|
|
483
|
+
if (!ComponentFn) return <div className="flex-1 min-w-0" />;
|
|
484
|
+
return ComponentFn(filter.value as any, handleValueChange, filter.options ?? []);
|
|
485
|
+
}, [filter.method?.component, filter.value, filter.options, handleValueChange]);
|
|
486
|
+
|
|
487
|
+
const columnSelectItems = useMemo(
|
|
488
|
+
() =>
|
|
489
|
+
columns.map((column) => (
|
|
490
|
+
<SelectItem key={column.id} className="text-sm">
|
|
491
|
+
{String(column.label)}
|
|
492
|
+
</SelectItem>
|
|
493
|
+
)),
|
|
494
|
+
[columns]
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<div className="flex items-center gap-2 w-full">
|
|
499
|
+
<div className="flex flex-1 items-center gap-2 min-w-0">
|
|
500
|
+
<Select
|
|
501
|
+
size="sm"
|
|
502
|
+
aria-label="Select Column"
|
|
503
|
+
className="w-40 flex-shrink-0 text-sm"
|
|
504
|
+
selectedKeys={filter.columnId ? [filter.columnId] : []}
|
|
505
|
+
onSelectionChange={handleColumnChange}
|
|
506
|
+
popoverProps={{
|
|
507
|
+
className: "w-auto min-w-max",
|
|
508
|
+
}}
|
|
509
|
+
>
|
|
510
|
+
{columnSelectItems}
|
|
511
|
+
</Select>
|
|
512
|
+
{methodSelect}
|
|
513
|
+
{filterValueComponent}
|
|
514
|
+
</div>
|
|
515
|
+
<Button variant="outline" size="sm" onClick={() => removeFilter(id)}>
|
|
516
|
+
<XIcon className="h-4 w-4" />
|
|
517
|
+
</Button>
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { TableProps } from "@m5kdev/frontend/modules/table/hooks/useNuqsTable";
|
|
2
|
+
import { Input } from "#components/ui/input";
|
|
3
|
+
import {
|
|
4
|
+
Pagination,
|
|
5
|
+
PaginationContent,
|
|
6
|
+
PaginationNext,
|
|
7
|
+
PaginationPrevious,
|
|
8
|
+
} from "#components/ui/pagination";
|
|
9
|
+
|
|
10
|
+
interface TablePaginationProps {
|
|
11
|
+
pageCount: number;
|
|
12
|
+
page: TableProps["page"];
|
|
13
|
+
limit: TableProps["limit"];
|
|
14
|
+
setPagination: TableProps["setPagination"];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const TablePagination = ({
|
|
18
|
+
pageCount,
|
|
19
|
+
page = 1,
|
|
20
|
+
limit = 10,
|
|
21
|
+
setPagination,
|
|
22
|
+
}: TablePaginationProps) => {
|
|
23
|
+
const isFirstPage = page === 1;
|
|
24
|
+
const isLastPage = page >= pageCount;
|
|
25
|
+
return (
|
|
26
|
+
<Pagination>
|
|
27
|
+
<PaginationContent>
|
|
28
|
+
<PaginationPrevious
|
|
29
|
+
isActive={!isFirstPage}
|
|
30
|
+
onClick={() => {
|
|
31
|
+
if (!isFirstPage) {
|
|
32
|
+
setPagination?.({ pageIndex: page - 2, pageSize: limit });
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
<Input
|
|
37
|
+
type="number"
|
|
38
|
+
value={page}
|
|
39
|
+
min={1}
|
|
40
|
+
max={pageCount}
|
|
41
|
+
onChange={(e) => {
|
|
42
|
+
const newPage = e.target.valueAsNumber;
|
|
43
|
+
if (newPage >= 1 && newPage <= pageCount) {
|
|
44
|
+
setPagination?.({ pageIndex: newPage - 1, pageSize: limit });
|
|
45
|
+
}
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
<PaginationNext
|
|
49
|
+
isActive={!isLastPage}
|
|
50
|
+
onClick={() => {
|
|
51
|
+
if (!isLastPage) {
|
|
52
|
+
setPagination?.({ pageIndex: page, pageSize: limit });
|
|
53
|
+
}
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
</PaginationContent>
|
|
57
|
+
</Pagination>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ColumnDataType as CommonColumnDataType } from "@m5kdev/commons/modules/table/filter.types";
|
|
2
|
+
|
|
3
|
+
export type ColumnDataType = CommonColumnDataType;
|
|
4
|
+
|
|
5
|
+
export type ColumnItem = {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
visibility: boolean;
|
|
9
|
+
options?: { label: string; value: string }[];
|
|
10
|
+
type?: ColumnDataType;
|
|
11
|
+
};
|