@arqel-dev/ui 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pages.d.ts CHANGED
@@ -29,6 +29,19 @@ declare function ArqelEditPage<TRecord extends RecordType = RecordType>(): JSX.E
29
29
  * `<ResourceIndex>`. Apps can override per-resource by registering
30
30
  * their own page component at `Pages/Arqel/{Slug}/Index.tsx` (the
31
31
  * lookup falls through to user pages first inside `createArqelApp`).
32
+ *
33
+ * Wires the standard table interactions — search / filter / sort /
34
+ * pagination — into Inertia partial `router.get` visits so the
35
+ * server can re-run TableQueryBuilder with the new query string.
36
+ *
37
+ * Query-string contract (matches `Arqel\Table\TableQueryBuilder`):
38
+ * - `?search=foo` global search
39
+ * - `?sort=column&direction=asc|desc`
40
+ * - `?page=N&per_page=M`
41
+ * - `?filter[name]=value` one entry per filter
42
+ *
43
+ * `search` is debounced (300ms) so typing doesn't hammer the
44
+ * backend; the rest fire immediately.
32
45
  */
33
46
 
34
47
  declare function ArqelIndexPage<TRecord extends RecordType = RecordType>(): JSX.Element;
package/dist/pages.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
- import { useRef, useMemo, useCallback, useId, useState } from 'react';
3
+ import { useMemo, useState, useRef, useEffect, useCallback, useId } from 'react';
4
4
  import { cva } from 'class-variance-authority';
5
- import { Slot } from 'radix-ui';
5
+ import { Slot, DropdownMenu as DropdownMenu$1, Dialog as Dialog$1 } from 'radix-ui';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
  import { isFieldEntry, isLayoutEntry } from '@arqel-dev/types/forms';
8
8
  import { useArqelForm } from '@arqel-dev/hooks';
9
9
  import { usePage, router } from '@inertiajs/react';
10
+ import { XIcon } from 'lucide-react';
10
11
  import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
11
12
 
12
13
  var __defProp = Object.defineProperty;
@@ -865,6 +866,477 @@ var init_ArqelEditPage = __esm({
865
866
  };
866
867
  }
867
868
  });
869
+ function DropdownMenu({
870
+ ...props
871
+ }) {
872
+ return /* @__PURE__ */ jsx(DropdownMenu$1.Root, { "data-slot": "dropdown-menu", ...props });
873
+ }
874
+ function DropdownMenuTrigger({
875
+ ...props
876
+ }) {
877
+ return /* @__PURE__ */ jsx(
878
+ DropdownMenu$1.Trigger,
879
+ {
880
+ "data-slot": "dropdown-menu-trigger",
881
+ ...props
882
+ }
883
+ );
884
+ }
885
+ function DropdownMenuContent({
886
+ className,
887
+ sideOffset = 4,
888
+ ...props
889
+ }) {
890
+ return /* @__PURE__ */ jsx(DropdownMenu$1.Portal, { children: /* @__PURE__ */ jsx(
891
+ DropdownMenu$1.Content,
892
+ {
893
+ "data-slot": "dropdown-menu-content",
894
+ sideOffset,
895
+ className: cn(
896
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
897
+ className
898
+ ),
899
+ ...props
900
+ }
901
+ ) });
902
+ }
903
+ function DropdownMenuItem({
904
+ className,
905
+ inset,
906
+ variant = "default",
907
+ ...props
908
+ }) {
909
+ return /* @__PURE__ */ jsx(
910
+ DropdownMenu$1.Item,
911
+ {
912
+ "data-slot": "dropdown-menu-item",
913
+ "data-inset": inset,
914
+ "data-variant": variant,
915
+ className: cn(
916
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
917
+ className
918
+ ),
919
+ ...props
920
+ }
921
+ );
922
+ }
923
+ var init_dropdown_menu = __esm({
924
+ "src/shadcn/ui/dropdown-menu.tsx"() {
925
+ "use client";
926
+ init_cn();
927
+ }
928
+ });
929
+ function Dialog({
930
+ ...props
931
+ }) {
932
+ return /* @__PURE__ */ jsx(Dialog$1.Root, { "data-slot": "dialog", ...props });
933
+ }
934
+ function DialogPortal({
935
+ ...props
936
+ }) {
937
+ return /* @__PURE__ */ jsx(Dialog$1.Portal, { "data-slot": "dialog-portal", ...props });
938
+ }
939
+ function DialogClose({
940
+ ...props
941
+ }) {
942
+ return /* @__PURE__ */ jsx(Dialog$1.Close, { "data-slot": "dialog-close", ...props });
943
+ }
944
+ function DialogOverlay({
945
+ className,
946
+ ...props
947
+ }) {
948
+ return /* @__PURE__ */ jsx(
949
+ Dialog$1.Overlay,
950
+ {
951
+ "data-slot": "dialog-overlay",
952
+ className: cn(
953
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
954
+ className
955
+ ),
956
+ ...props
957
+ }
958
+ );
959
+ }
960
+ function DialogContent({
961
+ className,
962
+ children,
963
+ showCloseButton = true,
964
+ ...props
965
+ }) {
966
+ return /* @__PURE__ */ jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [
967
+ /* @__PURE__ */ jsx(DialogOverlay, {}),
968
+ /* @__PURE__ */ jsxs(
969
+ Dialog$1.Content,
970
+ {
971
+ "data-slot": "dialog-content",
972
+ className: cn(
973
+ "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
974
+ className
975
+ ),
976
+ ...props,
977
+ children: [
978
+ children,
979
+ showCloseButton && /* @__PURE__ */ jsxs(
980
+ Dialog$1.Close,
981
+ {
982
+ "data-slot": "dialog-close",
983
+ className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
984
+ children: [
985
+ /* @__PURE__ */ jsx(XIcon, {}),
986
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
987
+ ]
988
+ }
989
+ )
990
+ ]
991
+ }
992
+ )
993
+ ] });
994
+ }
995
+ function DialogHeader({ className, ...props }) {
996
+ return /* @__PURE__ */ jsx(
997
+ "div",
998
+ {
999
+ "data-slot": "dialog-header",
1000
+ className: cn("flex flex-col gap-2 text-center sm:text-left", className),
1001
+ ...props
1002
+ }
1003
+ );
1004
+ }
1005
+ function DialogFooter({
1006
+ className,
1007
+ showCloseButton = false,
1008
+ children,
1009
+ ...props
1010
+ }) {
1011
+ return /* @__PURE__ */ jsxs(
1012
+ "div",
1013
+ {
1014
+ "data-slot": "dialog-footer",
1015
+ className: cn(
1016
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1017
+ className
1018
+ ),
1019
+ ...props,
1020
+ children: [
1021
+ children,
1022
+ showCloseButton && /* @__PURE__ */ jsx(Dialog$1.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", children: "Close" }) })
1023
+ ]
1024
+ }
1025
+ );
1026
+ }
1027
+ function DialogTitle({
1028
+ className,
1029
+ ...props
1030
+ }) {
1031
+ return /* @__PURE__ */ jsx(
1032
+ Dialog$1.Title,
1033
+ {
1034
+ "data-slot": "dialog-title",
1035
+ className: cn("text-lg leading-none font-semibold", className),
1036
+ ...props
1037
+ }
1038
+ );
1039
+ }
1040
+ function DialogDescription({
1041
+ className,
1042
+ ...props
1043
+ }) {
1044
+ return /* @__PURE__ */ jsx(
1045
+ Dialog$1.Description,
1046
+ {
1047
+ "data-slot": "dialog-description",
1048
+ className: cn("text-sm text-muted-foreground", className),
1049
+ ...props
1050
+ }
1051
+ );
1052
+ }
1053
+ var init_dialog = __esm({
1054
+ "src/shadcn/ui/dialog.tsx"() {
1055
+ init_cn();
1056
+ init_button();
1057
+ }
1058
+ });
1059
+ function ActionFormModal({
1060
+ open,
1061
+ onOpenChange,
1062
+ action,
1063
+ fields,
1064
+ onSubmit,
1065
+ processing = false,
1066
+ errors = {},
1067
+ initialValues = {}
1068
+ }) {
1069
+ const [values, setValues] = useState(initialValues);
1070
+ const size = action.modalSize ?? "md";
1071
+ const schema = useMemo(
1072
+ () => ({
1073
+ schema: (action.form ?? []).map((entry) => ({
1074
+ kind: "field",
1075
+ name: entry.name,
1076
+ type: entry.type
1077
+ })),
1078
+ columns: 1,
1079
+ model: null,
1080
+ inline: false,
1081
+ disabled: false
1082
+ }),
1083
+ [action.form]
1084
+ );
1085
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (next) => onOpenChange(next), children: /* @__PURE__ */ jsxs(DialogContent, { className: cn("overflow-auto", SIZE_CLASSES[size]), children: [
1086
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: action.label }) }),
1087
+ /* @__PURE__ */ jsxs(
1088
+ "form",
1089
+ {
1090
+ className: "flex flex-col gap-4",
1091
+ onSubmit: (e) => {
1092
+ e.preventDefault();
1093
+ onSubmit(values);
1094
+ },
1095
+ children: [
1096
+ /* @__PURE__ */ jsx(
1097
+ FormRenderer,
1098
+ {
1099
+ schema,
1100
+ fields,
1101
+ values,
1102
+ errors,
1103
+ onChange: (name, value) => setValues((prev) => ({ ...prev, [name]: value })),
1104
+ disabled: processing
1105
+ }
1106
+ ),
1107
+ /* @__PURE__ */ jsx(
1108
+ FormActions,
1109
+ {
1110
+ processing,
1111
+ submitLabel: action.label,
1112
+ extra: /* @__PURE__ */ jsx(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", disabled: processing, children: "Cancel" }) })
1113
+ }
1114
+ )
1115
+ ]
1116
+ }
1117
+ )
1118
+ ] }) });
1119
+ }
1120
+ var SIZE_CLASSES;
1121
+ var init_ActionFormModal = __esm({
1122
+ "src/action/ActionFormModal.tsx"() {
1123
+ init_FormActions();
1124
+ init_FormRenderer();
1125
+ init_dialog();
1126
+ init_cn();
1127
+ init_Button();
1128
+ SIZE_CLASSES = {
1129
+ sm: "sm:max-w-[24rem]",
1130
+ md: "sm:max-w-[32rem]",
1131
+ lg: "sm:max-w-[44rem]",
1132
+ xl: "sm:max-w-[56rem]",
1133
+ full: "sm:max-w-[90vw] h-[90vh]"
1134
+ };
1135
+ }
1136
+ });
1137
+ function ConfirmDialog({
1138
+ open,
1139
+ onOpenChange,
1140
+ config,
1141
+ onConfirm,
1142
+ processing = false
1143
+ }) {
1144
+ const [typed, setTyped] = useState("");
1145
+ useEffect(() => {
1146
+ if (!open) setTyped("");
1147
+ }, [open]);
1148
+ const requiresText = config?.requiresText;
1149
+ const submitDisabled = processing || requiresText !== void 0 && typed !== requiresText;
1150
+ const variant = COLOR_VARIANT[config?.color ?? "destructive"];
1151
+ const submitLabel = config?.submitLabel ?? (config?.color === "info" ? "Confirm" : "Delete");
1152
+ const cancelLabel = config?.cancelLabel ?? "Cancel";
1153
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (next) => onOpenChange(next), children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[28rem]", showCloseButton: false, children: [
1154
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
1155
+ /* @__PURE__ */ jsx(DialogTitle, { children: config?.heading ?? "Are you sure?" }),
1156
+ config?.description && /* @__PURE__ */ jsx(DialogDescription, { children: config.description })
1157
+ ] }),
1158
+ requiresText !== void 0 && /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-xs text-muted-foreground", children: [
1159
+ /* @__PURE__ */ jsxs("span", { children: [
1160
+ "Type ",
1161
+ /* @__PURE__ */ jsx("code", { className: "font-mono text-foreground", children: requiresText }),
1162
+ " to confirm"
1163
+ ] }),
1164
+ /* @__PURE__ */ jsx(
1165
+ "input",
1166
+ {
1167
+ type: "text",
1168
+ ref: (el) => el?.focus(),
1169
+ value: typed,
1170
+ onChange: (e) => setTyped(e.target.value),
1171
+ onKeyDown: (e) => {
1172
+ if (e.key === "Enter" && !submitDisabled) {
1173
+ e.preventDefault();
1174
+ onConfirm();
1175
+ }
1176
+ },
1177
+ className: "h-9 rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1178
+ }
1179
+ )
1180
+ ] }),
1181
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
1182
+ /* @__PURE__ */ jsx(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", disabled: processing, children: cancelLabel }) }),
1183
+ /* @__PURE__ */ jsx(Button, { type: "button", variant, disabled: submitDisabled, onClick: onConfirm, children: processing ? "Working\u2026" : submitLabel })
1184
+ ] })
1185
+ ] }) });
1186
+ }
1187
+ var COLOR_VARIANT;
1188
+ var init_ConfirmDialog = __esm({
1189
+ "src/action/ConfirmDialog.tsx"() {
1190
+ init_dialog();
1191
+ init_Button();
1192
+ COLOR_VARIANT = {
1193
+ destructive: "destructive",
1194
+ warning: "default",
1195
+ info: "default"
1196
+ };
1197
+ }
1198
+ });
1199
+ function ActionButton({
1200
+ action,
1201
+ formFields = [],
1202
+ onInvoke,
1203
+ processing = false,
1204
+ errors,
1205
+ size = "default",
1206
+ className
1207
+ }) {
1208
+ const [confirmOpen, setConfirmOpen] = useState(false);
1209
+ const [formOpen, setFormOpen] = useState(false);
1210
+ const variant = VARIANT_OVERRIDE[action.variant] ?? COLOR_TO_VARIANT[action.color];
1211
+ const handleClick = () => {
1212
+ if (action.disabled) return;
1213
+ if (action.requiresConfirmation) {
1214
+ setConfirmOpen(true);
1215
+ } else if (action.form && action.form.length > 0) {
1216
+ setFormOpen(true);
1217
+ } else {
1218
+ onInvoke();
1219
+ }
1220
+ };
1221
+ const handleConfirm = () => {
1222
+ setConfirmOpen(false);
1223
+ if (action.form && action.form.length > 0) {
1224
+ setFormOpen(true);
1225
+ } else {
1226
+ onInvoke();
1227
+ }
1228
+ };
1229
+ const handleFormSubmit = (values) => {
1230
+ onInvoke(values);
1231
+ setFormOpen(false);
1232
+ };
1233
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1234
+ /* @__PURE__ */ jsx(
1235
+ Button,
1236
+ {
1237
+ variant,
1238
+ size,
1239
+ disabled: action.disabled === true || processing,
1240
+ title: action.tooltip,
1241
+ onClick: handleClick,
1242
+ className,
1243
+ children: action.label
1244
+ }
1245
+ ),
1246
+ action.requiresConfirmation && /* @__PURE__ */ jsx(
1247
+ ConfirmDialog,
1248
+ {
1249
+ open: confirmOpen,
1250
+ onOpenChange: setConfirmOpen,
1251
+ config: action.confirmation,
1252
+ onConfirm: handleConfirm,
1253
+ processing
1254
+ }
1255
+ ),
1256
+ action.form && action.form.length > 0 && /* @__PURE__ */ jsx(
1257
+ ActionFormModal,
1258
+ {
1259
+ open: formOpen,
1260
+ onOpenChange: setFormOpen,
1261
+ action,
1262
+ fields: formFields,
1263
+ onSubmit: handleFormSubmit,
1264
+ processing,
1265
+ errors
1266
+ }
1267
+ )
1268
+ ] });
1269
+ }
1270
+ var COLOR_TO_VARIANT, VARIANT_OVERRIDE;
1271
+ var init_ActionButton = __esm({
1272
+ "src/action/ActionButton.tsx"() {
1273
+ init_ActionFormModal();
1274
+ init_Button();
1275
+ init_ConfirmDialog();
1276
+ COLOR_TO_VARIANT = {
1277
+ primary: "default",
1278
+ secondary: "outline",
1279
+ destructive: "destructive",
1280
+ success: "default",
1281
+ warning: "default",
1282
+ info: "default"
1283
+ };
1284
+ VARIANT_OVERRIDE = {
1285
+ default: "default",
1286
+ outline: "outline",
1287
+ ghost: "ghost",
1288
+ destructive: "destructive"
1289
+ };
1290
+ }
1291
+ });
1292
+ function ActionMenu({
1293
+ actions,
1294
+ formFieldsByAction = {},
1295
+ onInvoke,
1296
+ inlineThreshold = 3,
1297
+ processing = false,
1298
+ trigger,
1299
+ className
1300
+ }) {
1301
+ if (actions.length === 0) return null;
1302
+ if (actions.length <= inlineThreshold) {
1303
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap items-center gap-1", className), children: actions.map((action) => /* @__PURE__ */ jsx(
1304
+ ActionButton,
1305
+ {
1306
+ action,
1307
+ formFields: formFieldsByAction[action.name] ?? [],
1308
+ onInvoke: (values) => onInvoke(action, values),
1309
+ processing,
1310
+ size: "sm"
1311
+ },
1312
+ action.name
1313
+ )) });
1314
+ }
1315
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1316
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: trigger ?? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Actions", children: "\u22EF" }) }),
1317
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", sideOffset: 6, className: cn("min-w-[12rem]", className), children: actions.map((action) => /* @__PURE__ */ jsx(
1318
+ DropdownMenuItem,
1319
+ {
1320
+ disabled: action.disabled === true,
1321
+ variant: action.color === "destructive" ? "destructive" : "default",
1322
+ onSelect: () => {
1323
+ if (action.disabled) return;
1324
+ onInvoke(action);
1325
+ },
1326
+ children: action.label
1327
+ },
1328
+ action.name
1329
+ )) })
1330
+ ] });
1331
+ }
1332
+ var init_ActionMenu = __esm({
1333
+ "src/action/ActionMenu.tsx"() {
1334
+ init_dropdown_menu();
1335
+ init_cn();
1336
+ init_ActionButton();
1337
+ init_Button();
1338
+ }
1339
+ });
868
1340
  function TableCell({ column, value }) {
869
1341
  switch (column.type) {
870
1342
  case "text":
@@ -1566,13 +2038,176 @@ var ArqelIndexPage_exports = {};
1566
2038
  __export(ArqelIndexPage_exports, {
1567
2039
  default: () => ArqelIndexPage
1568
2040
  });
2041
+ function pruneEmptyFilters(values) {
2042
+ const out = {};
2043
+ for (const [k, v] of Object.entries(values)) {
2044
+ if (v === void 0 || v === null || v === "") continue;
2045
+ if (Array.isArray(v) && v.length === 0) continue;
2046
+ out[k] = v;
2047
+ }
2048
+ return out;
2049
+ }
2050
+ function invokeAction(action, record, formValues) {
2051
+ const recordId = record?.id;
2052
+ const url = action.url ? action.url.replace("{id}", String(recordId ?? "")) : `/arqel-dev/actions/${action.name}`;
2053
+ const method = action.method.toLowerCase();
2054
+ const data = { ...formValues ?? {} };
2055
+ if (recordId !== void 0) data["record_id"] = recordId;
2056
+ router.visit(url, {
2057
+ method,
2058
+ data,
2059
+ preserveScroll: true
2060
+ });
2061
+ }
2062
+ function buildQuery(params) {
2063
+ const data = {};
2064
+ const filters = pruneEmptyFilters(params.filters);
2065
+ if (Object.keys(filters).length > 0) data["filter"] = filters;
2066
+ if (params.search !== void 0) data["search"] = params.search;
2067
+ if (params.sort?.column) {
2068
+ data["sort"] = params.sort.column;
2069
+ if (params.sort.direction) data["direction"] = params.sort.direction;
2070
+ }
2071
+ if (params.page && params.page > 1) data["page"] = params.page;
2072
+ if (params.perPage) data["per_page"] = params.perPage;
2073
+ return data;
2074
+ }
1569
2075
  function ArqelIndexPage() {
1570
2076
  const page = usePage();
1571
2077
  const props = page.props;
1572
- return /* @__PURE__ */ jsx(ResourceIndex, { ...props });
2078
+ const initialFilters = useMemo(() => {
2079
+ if (typeof window === "undefined") return {};
2080
+ const sp = new URLSearchParams(window.location.search);
2081
+ const fromUrl = {};
2082
+ for (const [key, value] of sp.entries()) {
2083
+ const m = key.match(/^filter\[([^\]]+)\]$/);
2084
+ if (m?.[1]) fromUrl[m[1]] = value;
2085
+ }
2086
+ return fromUrl;
2087
+ }, []);
2088
+ const [filterValues, setFilterValues] = useState(initialFilters);
2089
+ const [searchValue, setSearchValue] = useState(props.search ?? "");
2090
+ const [perPage, setPerPage] = useState(props.pagination?.perPage);
2091
+ const [selectedIds, setSelectedIds] = useState([]);
2092
+ const searchDebounceRef = useRef(null);
2093
+ const visit = (data) => {
2094
+ const url = typeof window !== "undefined" ? window.location.pathname : page.url.split("?")[0];
2095
+ router.get(url ?? "/", data, {
2096
+ preserveState: true,
2097
+ preserveScroll: true,
2098
+ replace: true
2099
+ });
2100
+ };
2101
+ const currentSort = props.sort ?? null;
2102
+ const handleFilterChange = (name, value) => {
2103
+ const nextFilters = { ...filterValues };
2104
+ if (value === void 0 || value === null || value === "") {
2105
+ delete nextFilters[name];
2106
+ } else {
2107
+ nextFilters[name] = value;
2108
+ }
2109
+ setFilterValues(nextFilters);
2110
+ visit(buildQuery({ filters: nextFilters, search: searchValue, sort: currentSort, perPage }));
2111
+ };
2112
+ const handleClearFilters = () => {
2113
+ setFilterValues({});
2114
+ visit(buildQuery({ filters: {}, search: searchValue, sort: currentSort, perPage }));
2115
+ };
2116
+ const handleSearchChange = (value) => {
2117
+ setSearchValue(value);
2118
+ if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);
2119
+ searchDebounceRef.current = setTimeout(() => {
2120
+ visit(buildQuery({ filters: filterValues, search: value, sort: currentSort, perPage }));
2121
+ }, 300);
2122
+ };
2123
+ useEffect(
2124
+ () => () => {
2125
+ if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);
2126
+ },
2127
+ []
2128
+ );
2129
+ const handleSortChange = (column, direction) => {
2130
+ visit(
2131
+ buildQuery({
2132
+ filters: filterValues,
2133
+ search: searchValue,
2134
+ sort: { column, direction },
2135
+ perPage
2136
+ })
2137
+ );
2138
+ };
2139
+ const handlePageChange = (pageNum) => {
2140
+ visit(
2141
+ buildQuery({
2142
+ filters: filterValues,
2143
+ search: searchValue,
2144
+ sort: currentSort,
2145
+ page: pageNum,
2146
+ perPage
2147
+ })
2148
+ );
2149
+ };
2150
+ const handlePerPageChange = (newPerPage) => {
2151
+ setPerPage(newPerPage);
2152
+ visit(
2153
+ buildQuery({
2154
+ filters: filterValues,
2155
+ search: searchValue,
2156
+ sort: currentSort,
2157
+ perPage: newPerPage
2158
+ })
2159
+ );
2160
+ };
2161
+ const bulkActionsList = props.actions?.bulk ?? [];
2162
+ const bulkActions = bulkActionsList.length > 0 && selectedIds.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: bulkActionsList.map((action) => /* @__PURE__ */ jsx(
2163
+ "button",
2164
+ {
2165
+ type: "button",
2166
+ onClick: () => {
2167
+ const url = action.url ?? `/arqel-dev/actions/${action.name}`;
2168
+ const method = action.method.toLowerCase();
2169
+ router.visit(url, {
2170
+ method,
2171
+ data: { record_ids: selectedIds },
2172
+ preserveScroll: true,
2173
+ onSuccess: () => setSelectedIds([])
2174
+ });
2175
+ },
2176
+ className: "inline-flex h-8 items-center gap-1 rounded-sm border border-destructive/40 bg-destructive/10 px-3 text-xs font-medium text-destructive hover:bg-destructive/20",
2177
+ children: action.label
2178
+ },
2179
+ action.name
2180
+ )) }) : void 0;
2181
+ const rowActionsList = props.actions?.row ?? [];
2182
+ const rowActions = rowActionsList.length > 0 ? (record) => /* @__PURE__ */ jsx(
2183
+ ActionMenu,
2184
+ {
2185
+ actions: rowActionsList,
2186
+ onInvoke: (action, formValues) => invokeAction(action, record, formValues)
2187
+ }
2188
+ ) : void 0;
2189
+ return /* @__PURE__ */ jsx(
2190
+ ResourceIndex,
2191
+ {
2192
+ ...props,
2193
+ filterValues,
2194
+ onFilterChange: handleFilterChange,
2195
+ onClearFilters: handleClearFilters,
2196
+ onSearchChange: handleSearchChange,
2197
+ onSortChange: handleSortChange,
2198
+ onPageChange: handlePageChange,
2199
+ onPerPageChange: handlePerPageChange,
2200
+ search: searchValue,
2201
+ selectedIds,
2202
+ onSelectionChange: (ids) => setSelectedIds(ids),
2203
+ ...rowActions ? { rowActions } : {},
2204
+ ...bulkActions ? { bulkActions } : {}
2205
+ }
2206
+ );
1573
2207
  }
1574
2208
  var init_ArqelIndexPage = __esm({
1575
2209
  "src/pages/ArqelIndexPage.tsx"() {
2210
+ init_ActionMenu();
1576
2211
  init_ResourceIndex();
1577
2212
  }
1578
2213
  });