@burdenoff/microfe-crewfoundry 2026.924.1 → 2026.1013.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/crewfoundry/components/forms/FormModal.d.ts +25 -1
- package/dist/crewfoundry/components/forms/FormModal.d.ts.map +1 -1
- package/dist/crewfoundry/components/forms/FormModal.js +151 -136
- package/dist/crewfoundry/components/forms/FormModal.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js +49 -49
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +260 -115
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +199 -166
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +118 -104
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.d.ts +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
- package/dist/crewfoundry/utils/dateUtils.d.ts +47 -0
- package/dist/crewfoundry/utils/dateUtils.d.ts.map +1 -0
- package/dist/crewfoundry/utils/dateUtils.js +22 -0
- package/dist/crewfoundry/utils/dateUtils.js.map +1 -0
- package/package.json +1 -1
|
@@ -10,22 +10,46 @@ export interface FieldConfig {
|
|
|
10
10
|
label: string;
|
|
11
11
|
type: FormFieldType;
|
|
12
12
|
required?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Optional dynamic-required predicate. When provided, the field's `required`
|
|
15
|
+
* state at runtime is `(required || requiredWhen(values))`. Both the `*`
|
|
16
|
+
* marker AND the built-in required-field check (when the form is
|
|
17
|
+
* submitted) honor this rule. Existing forms that don't set it see no
|
|
18
|
+
* change — purely opt-in.
|
|
19
|
+
*/
|
|
20
|
+
requiredWhen?: (values: FormValues) => boolean;
|
|
13
21
|
placeholder?: string;
|
|
14
22
|
options?: FieldOption[];
|
|
15
23
|
/** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */
|
|
16
24
|
colSpan?: 1 | 2;
|
|
25
|
+
/** Lower bound for `date` / `number` inputs — the browser will not let the
|
|
26
|
+
* user pick a value below this. Pass a YYYY-MM-DD string for date fields.
|
|
27
|
+
* Ignored for other field types. */
|
|
28
|
+
min?: string | number;
|
|
29
|
+
/** Upper bound for `date` / `number` inputs — symmetric to `min`. */
|
|
30
|
+
max?: string | number;
|
|
17
31
|
}
|
|
18
32
|
export interface FormModalProps {
|
|
19
33
|
open: boolean;
|
|
20
34
|
onOpenChange: (open: boolean) => void;
|
|
21
35
|
title: string;
|
|
22
36
|
description?: string;
|
|
37
|
+
/** Optional inline error banner shown above the form fields. Pages should
|
|
38
|
+
* pass the first validation message so the user sees feedback inside the
|
|
39
|
+
* modal — useful while `useGlobalUi().showToast` has no rendered viewport
|
|
40
|
+
* (shared provider bug — see BOFF-7588). */
|
|
41
|
+
errorMessage?: string;
|
|
42
|
+
/** Fired when this FormModal's own required-field check rejects a submit
|
|
43
|
+
* (the consumer can mirror it into `errorMessage` or its own state).
|
|
44
|
+
* The FormModal still toasts (and silently, while the provider bug stands)
|
|
45
|
+
* and early-returns so onSubmit is not invoked — opt-in duplication. */
|
|
46
|
+
onValidationError?: (message: string) => void;
|
|
23
47
|
fields: FieldConfig[];
|
|
24
48
|
initialValues?: FormValues;
|
|
25
49
|
submitLabel?: string;
|
|
26
50
|
onSubmit: (values: FormValues) => Promise<void>;
|
|
27
51
|
}
|
|
28
|
-
export declare function FormModal({ open, onOpenChange, title, description, fields, initialValues, submitLabel, onSubmit, }: FormModalProps): import("react").JSX.Element;
|
|
52
|
+
export declare function FormModal({ open, onOpenChange, title, description, errorMessage, onValidationError, fields, initialValues, submitLabel, onSubmit, }: FormModalProps): import("react").JSX.Element;
|
|
29
53
|
export interface UseFormModalResult<TContext> {
|
|
30
54
|
open: boolean;
|
|
31
55
|
/** The row/context passed to openModal (e.g. the entity being edited). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormModal.d.ts","sourceRoot":"","sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"names":[],"mappings":"AAmBA,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,MAAM,GACN,QAAQ,CAAC;AAEb,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC;AAC7D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,wFAAwF;IACxF,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"FormModal.d.ts","sourceRoot":"","sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"names":[],"mappings":"AAmBA,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,MAAM,GACN,QAAQ,CAAC;AAEb,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC;AAC7D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,wFAAwF;IACxF,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAChB;;yCAEqC;IACrC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;iDAG6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;6EAGyE;IACzE,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AA4ED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,aAAa,EACb,WAAW,EACX,QAAQ,GACT,EAAE,cAAc,+BAgThB;AAGD,MAAM,WAAW,kBAAkB,CAAC,QAAQ;IAC1C,IAAI,EAAE,OAAO,CAAC;IACd,0EAA0E;IAC1E,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC;IACxC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,GAAG,IAAI,KAAK,kBAAkB,CAAC,QAAQ,CAAC,CAmB5E"}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { useTr as e } from "../../../shared/hooks/useTr.js";
|
|
2
2
|
import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import { Fragment as
|
|
5
|
-
import { Button as
|
|
6
|
-
import { useGlobalUi as
|
|
3
|
+
import { AlertCircle as a, X as o } from "lucide-react";
|
|
4
|
+
import { Fragment as s, jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
5
|
+
import { Button as u, Input as d, Label as f, ResponsiveDialog as p, ResponsiveDialogContent as m, ResponsiveDialogDescription as h, ResponsiveDialogFooter as g, ResponsiveDialogHeader as _, ResponsiveDialogTitle as v, Switch as y, Textarea as b } from "@burdenoff/fe-libs/ui";
|
|
6
|
+
import { useGlobalUi as x } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
7
7
|
//#region src/crewfoundry/components/forms/FormModal.tsx
|
|
8
|
-
var
|
|
8
|
+
var S = (e, t) => {
|
|
9
9
|
let n = {};
|
|
10
10
|
for (let r of e) {
|
|
11
11
|
let e = t?.[r.name];
|
|
12
12
|
r.type === "switch" ? n[r.name] = !!e : r.type === "tags" ? Array.isArray(e) ? n[r.name] = e.map((e) => String(e)) : typeof e == "string" && e.length > 0 ? n[r.name] = e.split(",").map((e) => e.trim()).filter(Boolean) : n[r.name] = [] : n[r.name] = e == null ? "" : String(e);
|
|
13
13
|
}
|
|
14
14
|
return n;
|
|
15
|
-
},
|
|
15
|
+
}, C = (e, t) => e.type === "switch" ? !1 : e.type === "tags" ? !Array.isArray(t) || t.length === 0 : typeof t != "string" || t.trim() === "", w = (e, t) => !!(e.required || e.requiredWhen?.(t)), T = (e, t) => {
|
|
16
16
|
let n = {};
|
|
17
17
|
for (let r of e) {
|
|
18
18
|
let e = t[r.name];
|
|
@@ -24,23 +24,23 @@ var x = (e, t) => {
|
|
|
24
24
|
} else n[r.name] = typeof e == "string" ? e : "";
|
|
25
25
|
}
|
|
26
26
|
return n;
|
|
27
|
-
},
|
|
28
|
-
function
|
|
29
|
-
let { showToast:
|
|
27
|
+
}, E = "flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
|
|
28
|
+
function D({ open: D, onOpenChange: O, title: k, description: A, errorMessage: j, onValidationError: M, fields: N, initialValues: P, submitLabel: F, onSubmit: I }) {
|
|
29
|
+
let { showToast: L } = x(), R = e(), z = F ?? R("formModal.submitLabel", "Save"), [B, V] = i(() => S(N, P)), [H, U] = i({}), [W, G] = i(!1), K = r(N), q = r(P), J = r(!1);
|
|
30
30
|
n(() => {
|
|
31
|
-
|
|
32
|
-
}, [
|
|
33
|
-
|
|
34
|
-
}, [
|
|
35
|
-
let
|
|
36
|
-
|
|
31
|
+
K.current = N, q.current = P;
|
|
32
|
+
}, [N, P]), n(() => {
|
|
33
|
+
D && !J.current && (V(S(K.current, q.current)), U({}), G(!1)), J.current = D;
|
|
34
|
+
}, [D]);
|
|
35
|
+
let Y = t((e, t) => {
|
|
36
|
+
V((n) => ({
|
|
37
37
|
...n,
|
|
38
38
|
[e]: t
|
|
39
39
|
}));
|
|
40
|
-
}, []),
|
|
41
|
-
|
|
40
|
+
}, []), X = t((e) => {
|
|
41
|
+
U((t) => {
|
|
42
42
|
let n = (t[e] ?? "").trim();
|
|
43
|
-
return n &&
|
|
43
|
+
return n && V((t) => {
|
|
44
44
|
let r = Array.isArray(t[e]) ? t[e] : [];
|
|
45
45
|
return r.includes(n) ? t : {
|
|
46
46
|
...t,
|
|
@@ -51,158 +51,173 @@ function T({ open: T, onOpenChange: E, title: D, description: O, fields: k, init
|
|
|
51
51
|
[e]: ""
|
|
52
52
|
};
|
|
53
53
|
});
|
|
54
|
-
}, []),
|
|
55
|
-
|
|
54
|
+
}, []), Z = t((e, t) => {
|
|
55
|
+
V((n) => {
|
|
56
56
|
let r = Array.isArray(n[e]) ? n[e] : [];
|
|
57
57
|
return {
|
|
58
58
|
...n,
|
|
59
59
|
[e]: r.filter((e) => e !== t)
|
|
60
60
|
};
|
|
61
61
|
});
|
|
62
|
-
}, []),
|
|
63
|
-
if (e.preventDefault(),
|
|
64
|
-
let t = { ...
|
|
65
|
-
for (let e of
|
|
66
|
-
let n = (
|
|
62
|
+
}, []), Q = t(async (e) => {
|
|
63
|
+
if (e.preventDefault(), W) return;
|
|
64
|
+
let t = { ...B };
|
|
65
|
+
for (let e of N) if (e.type === "tags") {
|
|
66
|
+
let n = (H[e.name] ?? "").trim();
|
|
67
67
|
if (n) {
|
|
68
68
|
let r = Array.isArray(t[e.name]) ? t[e.name] : [];
|
|
69
69
|
r.includes(n) || (t[e.name] = [...r, n]);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
let n =
|
|
72
|
+
let n = N.find((e) => w(e, t) && C(e, t[e.name]));
|
|
73
73
|
if (n) {
|
|
74
|
-
|
|
74
|
+
let e = R("formModal.fieldRequired", "{{label}} is required", { label: n.label });
|
|
75
|
+
L(e, "warning"), M?.(e);
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
G(!0);
|
|
78
79
|
try {
|
|
79
|
-
await
|
|
80
|
+
await I(T(N, t)), O(!1);
|
|
80
81
|
} catch (e) {
|
|
81
|
-
|
|
82
|
+
L(e instanceof Error ? e.message : R("formModal.submitError", "Something went wrong"), "error");
|
|
82
83
|
} finally {
|
|
83
|
-
|
|
84
|
+
G(!1);
|
|
84
85
|
}
|
|
85
86
|
}, [
|
|
86
|
-
k,
|
|
87
|
-
E,
|
|
88
|
-
M,
|
|
89
87
|
N,
|
|
90
|
-
|
|
88
|
+
O,
|
|
89
|
+
I,
|
|
90
|
+
L,
|
|
91
|
+
W,
|
|
92
|
+
H,
|
|
91
93
|
R,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
B,
|
|
95
|
+
M
|
|
94
96
|
]);
|
|
95
|
-
return /* @__PURE__ */
|
|
96
|
-
open:
|
|
97
|
-
onOpenChange:
|
|
98
|
-
children: /* @__PURE__ */
|
|
97
|
+
return /* @__PURE__ */ c(p, {
|
|
98
|
+
open: D,
|
|
99
|
+
onOpenChange: O,
|
|
100
|
+
children: /* @__PURE__ */ l(m, {
|
|
99
101
|
className: "max-h-[85vh] overflow-y-auto sm:max-w-[560px]",
|
|
100
|
-
children: [/* @__PURE__ */
|
|
101
|
-
onSubmit:
|
|
102
|
+
children: [/* @__PURE__ */ l(_, { children: [/* @__PURE__ */ c(v, { children: k }), A ? /* @__PURE__ */ c(h, { children: A }) : null] }), /* @__PURE__ */ l("form", {
|
|
103
|
+
onSubmit: Q,
|
|
102
104
|
className: "space-y-4",
|
|
103
|
-
children: [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
className:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
children: [
|
|
106
|
+
j ? /* @__PURE__ */ l("div", {
|
|
107
|
+
role: "alert",
|
|
108
|
+
className: "flex items-start gap-2 rounded-input border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text",
|
|
109
|
+
children: [/* @__PURE__ */ c(a, {
|
|
110
|
+
className: "mt-0.5 h-4 w-4 shrink-0",
|
|
111
|
+
"aria-hidden": !0
|
|
112
|
+
}), /* @__PURE__ */ c("span", { children: j })]
|
|
113
|
+
}) : null,
|
|
114
|
+
/* @__PURE__ */ c("div", {
|
|
115
|
+
className: "grid grid-cols-1 gap-4 md:grid-cols-2",
|
|
116
|
+
children: N.map((e) => {
|
|
117
|
+
let t = `formmodal-${e.name}`, n = e.colSpan === 2 || e.colSpan === void 0 && (e.type === "textarea" || e.type === "tags"), r = B[e.name];
|
|
118
|
+
return /* @__PURE__ */ c("div", {
|
|
119
|
+
className: `flex flex-col gap-1.5 ${n ? "md:col-span-2" : ""}`,
|
|
120
|
+
children: e.type === "switch" ? /* @__PURE__ */ l("div", {
|
|
121
|
+
className: "flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2",
|
|
122
|
+
children: [/* @__PURE__ */ l(f, {
|
|
123
|
+
htmlFor: t,
|
|
124
|
+
children: [e.label, w(e, B) ? /* @__PURE__ */ c("span", {
|
|
125
|
+
className: "text-status-error-fg",
|
|
126
|
+
children: " *"
|
|
127
|
+
}) : null]
|
|
128
|
+
}), /* @__PURE__ */ c(y, {
|
|
129
|
+
id: t,
|
|
130
|
+
checked: !!r,
|
|
131
|
+
onCheckedChange: (t) => Y(e.name, t),
|
|
132
|
+
disabled: W
|
|
133
|
+
})]
|
|
134
|
+
}) : /* @__PURE__ */ l(s, { children: [/* @__PURE__ */ l(f, {
|
|
112
135
|
htmlFor: t,
|
|
113
|
-
children: [e.label, e
|
|
136
|
+
children: [e.label, w(e, B) ? /* @__PURE__ */ c("span", {
|
|
114
137
|
className: "text-status-error-fg",
|
|
115
138
|
children: " *"
|
|
116
139
|
}) : null]
|
|
117
|
-
}), /* @__PURE__ */
|
|
140
|
+
}), e.type === "textarea" ? /* @__PURE__ */ c(b, {
|
|
141
|
+
id: t,
|
|
142
|
+
value: typeof r == "string" ? r : "",
|
|
143
|
+
placeholder: e.placeholder,
|
|
144
|
+
disabled: W,
|
|
145
|
+
onChange: (t) => Y(e.name, t.target.value)
|
|
146
|
+
}) : e.type === "select" ? /* @__PURE__ */ l("select", {
|
|
118
147
|
id: t,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
disabled:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
className: E,
|
|
149
|
+
value: typeof r == "string" ? r : "",
|
|
150
|
+
disabled: W,
|
|
151
|
+
onChange: (t) => Y(e.name, t.target.value),
|
|
152
|
+
children: [/* @__PURE__ */ c("option", {
|
|
153
|
+
value: "",
|
|
154
|
+
disabled: !0,
|
|
155
|
+
children: e.placeholder ?? R("formModal.selectPlaceholder", "Select…")
|
|
156
|
+
}), (e.options ?? []).map((e) => /* @__PURE__ */ c("option", {
|
|
157
|
+
value: e.value,
|
|
158
|
+
children: e.label
|
|
159
|
+
}, e.value))]
|
|
160
|
+
}) : e.type === "tags" ? /* @__PURE__ */ l("div", {
|
|
161
|
+
className: "flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5",
|
|
162
|
+
children: [(Array.isArray(r) ? r : []).map((t) => /* @__PURE__ */ l("span", {
|
|
163
|
+
className: "inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary",
|
|
164
|
+
children: [t, /* @__PURE__ */ c("button", {
|
|
165
|
+
type: "button",
|
|
166
|
+
"aria-label": R("formModal.removeTag", "Remove {{tag}}", { tag: t }),
|
|
167
|
+
className: "text-text-secondary hover:text-text-primary",
|
|
168
|
+
onClick: () => Z(e.name, t),
|
|
169
|
+
disabled: W,
|
|
170
|
+
children: /* @__PURE__ */ c(o, { className: "h-3 w-3" })
|
|
171
|
+
})]
|
|
172
|
+
}, t)), /* @__PURE__ */ c("input", {
|
|
173
|
+
id: t,
|
|
174
|
+
className: "min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder",
|
|
175
|
+
value: H[e.name] ?? "",
|
|
176
|
+
placeholder: e.placeholder ?? R("formModal.tagsPlaceholder", "Add and press Enter"),
|
|
177
|
+
disabled: W,
|
|
178
|
+
onChange: (t) => U((n) => ({
|
|
179
|
+
...n,
|
|
180
|
+
[e.name]: t.target.value
|
|
181
|
+
})),
|
|
182
|
+
onKeyDown: (t) => {
|
|
183
|
+
if (t.key === "Enter" || t.key === ",") t.preventDefault(), X(e.name);
|
|
184
|
+
else if (t.key === "Backspace" && (H[e.name] ?? "") === "") {
|
|
185
|
+
let t = Array.isArray(r) ? r : [];
|
|
186
|
+
t.length > 0 && Z(e.name, t[t.length - 1]);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
onBlur: () => X(e.name)
|
|
160
190
|
})]
|
|
161
|
-
}
|
|
191
|
+
}) : /* @__PURE__ */ c(d, {
|
|
162
192
|
id: t,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
onChange: (t) => G(e.name, t.target.value)
|
|
187
|
-
})] })
|
|
188
|
-
}, e.name);
|
|
189
|
-
})
|
|
190
|
-
}), /* @__PURE__ */ c(h, { children: [/* @__PURE__ */ s(l, {
|
|
191
|
-
type: "button",
|
|
192
|
-
variant: "outline",
|
|
193
|
-
onClick: () => E(!1),
|
|
194
|
-
disabled: B,
|
|
195
|
-
children: P("formModal.cancel", "Cancel")
|
|
196
|
-
}), /* @__PURE__ */ s(l, {
|
|
197
|
-
type: "submit",
|
|
198
|
-
disabled: B,
|
|
199
|
-
children: B ? P("formModal.saving", "Saving…") : F
|
|
200
|
-
})] })]
|
|
193
|
+
type: e.type === "number" ? "number" : e.type === "date" ? "date" : e.type === "email" ? "email" : "text",
|
|
194
|
+
min: e.min,
|
|
195
|
+
max: e.max,
|
|
196
|
+
value: typeof r == "string" ? r : "",
|
|
197
|
+
placeholder: e.placeholder,
|
|
198
|
+
disabled: W,
|
|
199
|
+
onChange: (t) => Y(e.name, t.target.value)
|
|
200
|
+
})] })
|
|
201
|
+
}, e.name);
|
|
202
|
+
})
|
|
203
|
+
}),
|
|
204
|
+
/* @__PURE__ */ l(g, { children: [/* @__PURE__ */ c(u, {
|
|
205
|
+
type: "button",
|
|
206
|
+
variant: "outline",
|
|
207
|
+
onClick: () => O(!1),
|
|
208
|
+
disabled: W,
|
|
209
|
+
children: R("formModal.cancel", "Cancel")
|
|
210
|
+
}), /* @__PURE__ */ c(u, {
|
|
211
|
+
type: "submit",
|
|
212
|
+
disabled: W,
|
|
213
|
+
children: W ? R("formModal.saving", "Saving…") : z
|
|
214
|
+
})] })
|
|
215
|
+
]
|
|
201
216
|
})]
|
|
202
217
|
})
|
|
203
218
|
});
|
|
204
219
|
}
|
|
205
|
-
function
|
|
220
|
+
function O() {
|
|
206
221
|
let [e, n] = i(!1), [r, a] = i(null);
|
|
207
222
|
return {
|
|
208
223
|
open: e,
|
|
@@ -217,6 +232,6 @@ function E() {
|
|
|
217
232
|
};
|
|
218
233
|
}
|
|
219
234
|
//#endregion
|
|
220
|
-
export {
|
|
235
|
+
export { D as FormModal, O as useFormModal };
|
|
221
236
|
|
|
222
237
|
//# sourceMappingURL=FormModal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { X } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n fields,\n initialValues,\n submitLabel,\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const tr = useTr();\n const resolvedSubmitLabel = submitLabel ?? tr('formModal.submitLabel', 'Save');\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback((name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n }, []);\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find(\n (field) => field.required && isEmptyValue(field, staged[field.name])\n );\n if (missing) {\n showToast(\n tr('formModal.fieldRequired', '{{label}} is required', { label: missing.label }),\n 'warning'\n );\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(\n error instanceof Error\n ? error.message\n : tr('formModal.submitError', 'Something went wrong'),\n 'error'\n );\n } finally {\n setSubmitting(false);\n }\n },\n [fields, onOpenChange, onSubmit, showToast, submitting, tagDrafts, tr, values]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined &&\n (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? tr('formModal.selectPlaceholder', 'Select…')}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={tr('formModal.removeTag', 'Remove {{tag}}', { tag })}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={\n field.placeholder ??\n tr('formModal.tagsPlaceholder', 'Add and press Enter')\n }\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({\n ...prev,\n [field.name]: event.target.value,\n }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n {tr('formModal.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? tr('formModal.saving', 'Saving…') : resolvedSubmitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { AlertCircle, X } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n /**\n * Optional dynamic-required predicate. When provided, the field's `required`\n * state at runtime is `(required || requiredWhen(values))`. Both the `*`\n * marker AND the built-in required-field check (when the form is\n * submitted) honor this rule. Existing forms that don't set it see no\n * change — purely opt-in.\n */\n requiredWhen?: (values: FormValues) => boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n /** Lower bound for `date` / `number` inputs — the browser will not let the\n * user pick a value below this. Pass a YYYY-MM-DD string for date fields.\n * Ignored for other field types. */\n min?: string | number;\n /** Upper bound for `date` / `number` inputs — symmetric to `min`. */\n max?: string | number;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n /** Optional inline error banner shown above the form fields. Pages should\n * pass the first validation message so the user sees feedback inside the\n * modal — useful while `useGlobalUi().showToast` has no rendered viewport\n * (shared provider bug — see BOFF-7588). */\n errorMessage?: string;\n /** Fired when this FormModal's own required-field check rejects a submit\n * (the consumer can mirror it into `errorMessage` or its own state).\n * The FormModal still toasts (and silently, while the provider bug stands)\n * and early-returns so onSubmit is not invoked — opt-in duplication. */\n onValidationError?: (message: string) => void;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\n/** Whether this field is required at this moment, given the current values.\n * Combines the static `required` flag with the optional `requiredWhen`\n * predicate so the `*` marker and the blocking submit check stay in sync.\n */\nconst isRequiredAt = (field: FieldConfig, values: FormValues): boolean => {\n if (field.required) return true;\n if (field.requiredWhen?.(values)) return true;\n return false;\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n errorMessage,\n onValidationError,\n fields,\n initialValues,\n submitLabel,\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const tr = useTr();\n const resolvedSubmitLabel = submitLabel ?? tr('formModal.submitLabel', 'Save');\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback((name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n }, []);\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find(\n (field) => isRequiredAt(field, staged) && isEmptyValue(field, staged[field.name])\n );\n if (missing) {\n const message = tr('formModal.fieldRequired', '{{label}} is required', {\n label: missing.label,\n });\n showToast(message, 'warning');\n onValidationError?.(message);\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(\n error instanceof Error\n ? error.message\n : tr('formModal.submitError', 'Something went wrong'),\n 'error'\n );\n } finally {\n setSubmitting(false);\n }\n },\n [\n fields,\n onOpenChange,\n onSubmit,\n showToast,\n submitting,\n tagDrafts,\n tr,\n values,\n onValidationError,\n ]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-input border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\"\n >\n <AlertCircle className=\"mt-0.5 h-4 w-4 shrink-0\" aria-hidden />\n <span>{errorMessage}</span>\n </div>\n ) : null}\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined &&\n (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {isRequiredAt(field, values) ? (\n <span className=\"text-status-error-fg\"> *</span>\n ) : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? tr('formModal.selectPlaceholder', 'Select…')}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={tr('formModal.removeTag', 'Remove {{tag}}', { tag })}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={\n field.placeholder ??\n tr('formModal.tagsPlaceholder', 'Add and press Enter')\n }\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({\n ...prev,\n [field.name]: event.target.value,\n }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n min={field.min}\n max={field.max}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n {tr('formModal.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? tr('formModal.saving', 'Saving…') : resolvedSubmitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsHiringAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AnalyticsHiringAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.tsx"],"names":[],"mappings":"AAuDA,wBAAgB,gCAAgC,gCAkY/C"}
|