@oondemand/oon-core-front 0.3.12 → 0.3.14

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/index.d.ts CHANGED
@@ -246,6 +246,7 @@ interface OonColumnDef {
246
246
  permissions?: string[];
247
247
  capabilities?: string[];
248
248
  }
249
+ type OonFormFieldWidget = "default" | "checkbox";
249
250
  interface OonFormFieldDef {
250
251
  field: string;
251
252
  label?: string;
@@ -255,6 +256,19 @@ interface OonFormFieldDef {
255
256
  /** model relacionada quando kind === "ref". */
256
257
  ref?: string;
257
258
  renderer?: string;
259
+ /** Controle visual pontual sem substituir a página/formulário inteiro. */
260
+ widget?: OonFormFieldWidget;
261
+ /** Valor persistido quando um checkbox está marcado. Default: true. */
262
+ checkedValue?: string | number | boolean;
263
+ /** Valor persistido quando um checkbox está desmarcado. Default: false. */
264
+ uncheckedValue?: string | number | boolean;
265
+ /** Agrupa campos relacionados em um bloco visual comum. */
266
+ group?: string;
267
+ /** Rótulo exibido uma vez acima do grupo. Default: group. */
268
+ groupLabel?: string;
269
+ groupDescription?: string;
270
+ /** Exibe controles do mesmo grupo na mesma linha quando houver espaço. */
271
+ inline?: boolean;
258
272
  permissions?: string[];
259
273
  capabilities?: string[];
260
274
  }
@@ -412,7 +426,7 @@ interface CollectionViewDef {
412
426
  mode?: CollectionMode;
413
427
  /** modo "full": colunas explícitas. */
414
428
  columns?: OonColumnDef[];
415
- /** modo "full": campos de formulário explícitos. */
429
+ /** Campos explícitos ou overrides pontuais sobre a metadata do model. */
416
430
  form?: OonFormFieldDef[];
417
431
  importExport?: boolean;
418
432
  renderer?: string;
@@ -583,6 +597,8 @@ interface CentralUiManifest {
583
597
  path?: string;
584
598
  label?: string;
585
599
  section?: string;
600
+ /** Overrides pontuais de campos; os demais continuam vindo da metadata. */
601
+ form?: OonFormFieldDef[];
586
602
  list?: OonCollectionListConfig;
587
603
  relations?: Record<string, OonRelationDef>;
588
604
  detailModal?: OonDetailModalConfig;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { StrictMode } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
4
 
5
5
  // src/app/OonApp.tsx
6
- import { useMemo as useMemo6 } from "react";
6
+ import { useMemo as useMemo7 } from "react";
7
7
  import { RouterProvider } from "react-router-dom";
8
8
 
9
9
  // src/api/createRestClient.ts
@@ -953,7 +953,7 @@ function DataGrid({
953
953
  }
954
954
 
955
955
  // src/components/internal/DynamicForm.tsx
956
- import { useEffect as useEffect4, useState as useState5 } from "react";
956
+ import { useEffect as useEffect4, useMemo as useMemo4, useState as useState5 } from "react";
957
957
  import { Box as Box4, Button as Button4, Flex as Flex4, Heading as Heading2, Input as Input3, NativeSelect as NativeSelect2, SimpleGrid, Stack as Stack2, Text as Text5, Textarea } from "@chakra-ui/react";
958
958
 
959
959
  // src/components/internal/ReferenceSelect.tsx
@@ -1443,10 +1443,25 @@ var controlProps = (invalid) => ({
1443
1443
  boxShadow: invalid ? "0 0 0 3px rgba(220,38,38,.08)" : void 0,
1444
1444
  fontSize: "11px"
1445
1445
  });
1446
+ function buildItems(fields) {
1447
+ const items = [];
1448
+ const renderedGroups = /* @__PURE__ */ new Set();
1449
+ fields.forEach((field) => {
1450
+ if (!field.group) {
1451
+ items.push({ type: "field", field });
1452
+ return;
1453
+ }
1454
+ if (renderedGroups.has(field.group)) return;
1455
+ renderedGroups.add(field.group);
1456
+ items.push({ type: "group", id: field.group, fields: fields.filter((candidate) => candidate.group === field.group) });
1457
+ });
1458
+ return items;
1459
+ }
1446
1460
  function DynamicForm({ title, eyebrow = "Cadastro", fields, initialValues, submitting, error, onSubmit, onCancel, onFieldChange }) {
1447
1461
  const [values, setValues] = useState5(() => Object.fromEntries(fields.map((field) => [field.field, toValue(initialValues?.[field.field], field.kind)])));
1448
1462
  const [errors, setErrors] = useState5({});
1449
1463
  const [dismissed, setDismissed] = useState5(false);
1464
+ const items = useMemo4(() => buildItems(fields), [fields]);
1450
1465
  useEffect4(() => {
1451
1466
  const listener = (event) => {
1452
1467
  if (event.key !== "Escape" || event.defaultPrevented || submitting) return;
@@ -1496,6 +1511,83 @@ function DynamicForm({ title, eyebrow = "Cadastro", fields, initialValues, submi
1496
1511
  setErrors({});
1497
1512
  onSubmit(payload);
1498
1513
  };
1514
+ const renderField = (field, grouped = false) => {
1515
+ const label = fieldLabel({ name: field.field, label: field.label });
1516
+ const message = errors[field.field];
1517
+ const id = idFor(field.field);
1518
+ const errorId = message ? errorIdFor(field.field) : void 0;
1519
+ const multiline = isMultiline(field);
1520
+ const span = field.span === 2 || multiline;
1521
+ const wrap = (control, showLabel = true) => /* @__PURE__ */ jsxs5(Box4, { minW: grouped ? "150px" : void 0, flex: grouped ? "0 1 auto" : void 0, gridColumn: !grouped && span ? "1 / -1" : void 0, children: [
1522
+ showLabel ? /* @__PURE__ */ jsx9(Label, { id, label, required: field.required }) : null,
1523
+ control,
1524
+ /* @__PURE__ */ jsx9(ErrorText, { id: errorId, text: message })
1525
+ ] }, field.field);
1526
+ if (field.widget === "checkbox") {
1527
+ const checkedValue = String(field.checkedValue ?? true);
1528
+ const uncheckedValue = String(field.uncheckedValue ?? false);
1529
+ const checked = (values[field.field] ?? uncheckedValue) === checkedValue;
1530
+ return wrap(
1531
+ /* @__PURE__ */ jsxs5(
1532
+ Box4,
1533
+ {
1534
+ as: "label",
1535
+ display: "flex",
1536
+ minH: "37px",
1537
+ px: "11px",
1538
+ alignItems: "center",
1539
+ gap: "8px",
1540
+ color: "#344054",
1541
+ bg: checked ? "#F0F7FF" : "white",
1542
+ borderWidth: "1px",
1543
+ borderColor: message ? "#DC2626" : checked ? "#8FC9E7" : "#D5DDE3",
1544
+ borderRadius: "7px",
1545
+ cursor: "pointer",
1546
+ transition: "background .15s ease, border-color .15s ease",
1547
+ _hover: { borderColor: "#8FC9E7", bg: "#F7FBFE" },
1548
+ children: [
1549
+ /* @__PURE__ */ jsx9(
1550
+ "input",
1551
+ {
1552
+ id,
1553
+ type: "checkbox",
1554
+ checked,
1555
+ "aria-invalid": !!message,
1556
+ "aria-describedby": errorId,
1557
+ style: { width: 16, height: 16, accentColor: "#0474AF", cursor: "pointer" },
1558
+ onChange: (event) => change(field.field, event.currentTarget.checked ? checkedValue : uncheckedValue)
1559
+ }
1560
+ ),
1561
+ /* @__PURE__ */ jsxs5(Text5, { as: "span", fontSize: "11px", fontWeight: "600", children: [
1562
+ label,
1563
+ field.required ? /* @__PURE__ */ jsx9(Text5, { as: "span", color: "#DC2626", children: " *" }) : null
1564
+ ] })
1565
+ ]
1566
+ }
1567
+ ),
1568
+ false
1569
+ );
1570
+ }
1571
+ if (field.kind === "ref" && field.ref) return wrap(/* @__PURE__ */ jsx9(ReferenceSelect, { field: field.field, inputId: id, errorId, label, refModel: field.ref, value: values[field.field] ?? "", initialValue: initialValues?.[field.field], required: field.required, invalid: !!message, error: message, onChange: (value) => change(field.field, value) }));
1572
+ if (field.kind === "enum" && field.options?.length) return wrap(/* @__PURE__ */ jsxs5(NativeSelect2.Root, { size: "sm", children: [
1573
+ /* @__PURE__ */ jsxs5(NativeSelect2.Field, { id, "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), onChange: (event) => change(field.field, event.currentTarget.value), children: [
1574
+ /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014 Selecione \u2014" }),
1575
+ field.options.map((option) => /* @__PURE__ */ jsx9("option", { value: option, children: option }, option))
1576
+ ] }),
1577
+ /* @__PURE__ */ jsx9(NativeSelect2.Indicator, {})
1578
+ ] }));
1579
+ if (field.kind === "boolean") return wrap(/* @__PURE__ */ jsxs5(NativeSelect2.Root, { size: "sm", children: [
1580
+ /* @__PURE__ */ jsxs5(NativeSelect2.Field, { id, "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), onChange: (event) => change(field.field, event.currentTarget.value), children: [
1581
+ /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014 Selecione \u2014" }),
1582
+ /* @__PURE__ */ jsx9("option", { value: "true", children: "Sim" }),
1583
+ /* @__PURE__ */ jsx9("option", { value: "false", children: "N\xE3o" })
1584
+ ] }),
1585
+ /* @__PURE__ */ jsx9(NativeSelect2.Indicator, {})
1586
+ ] }));
1587
+ if (multiline) return wrap(/* @__PURE__ */ jsx9(Textarea, { id, rows: field.rows ?? 4, resize: "vertical", "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), lineHeight: "1.5", onChange: (event) => change(field.field, event.currentTarget.value) }));
1588
+ const type = field.kind === "date" ? "date" : ["number", "currency", "currencyConverted"].includes(field.kind ?? "") ? "number" : "text";
1589
+ return wrap(/* @__PURE__ */ jsx9(Input3, { id, type, step: type === "number" ? "any" : void 0, autoComplete: "off", "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), onChange: (event) => change(field.field, event.currentTarget.value) }));
1590
+ };
1499
1591
  const invalid = fields.filter((field) => errors[field.field]);
1500
1592
  const generic = dismissed ? null : messageOf(error);
1501
1593
  return /* @__PURE__ */ jsx9(Flex4, { position: "fixed", inset: 0, zIndex: 1e3, align: "center", justify: "center", p: { base: 2, md: 6 }, bg: "rgba(15,23,42,.48)", backdropFilter: "blur(2px)", onMouseDown: (event) => {
@@ -1525,38 +1617,15 @@ function DynamicForm({ title, eyebrow = "Cadastro", fields, initialValues, submi
1525
1617
  /* @__PURE__ */ jsx9(AlertIcon, {}),
1526
1618
  generic
1527
1619
  ] }) : null,
1528
- /* @__PURE__ */ jsx9(SimpleGrid, { columns: { base: 1, lg: 2 }, gapX: "16px", gapY: "15px", children: fields.map((baseField) => {
1529
- const field = baseField;
1530
- const label = fieldLabel({ name: field.field, label: field.label });
1531
- const message = errors[field.field];
1532
- const id = idFor(field.field);
1533
- const errorId = message ? errorIdFor(field.field) : void 0;
1534
- const multiline = isMultiline(field);
1535
- const span = field.span === 2 || multiline;
1536
- const wrap = (control) => /* @__PURE__ */ jsxs5(Box4, { gridColumn: span ? "1 / -1" : void 0, children: [
1537
- /* @__PURE__ */ jsx9(Label, { id, label, required: field.required }),
1538
- control,
1539
- /* @__PURE__ */ jsx9(ErrorText, { id: errorId, text: message })
1540
- ] }, field.field);
1541
- if (field.kind === "ref" && field.ref) return wrap(/* @__PURE__ */ jsx9(ReferenceSelect, { field: field.field, inputId: id, errorId, label, refModel: field.ref, value: values[field.field] ?? "", initialValue: initialValues?.[field.field], required: field.required, invalid: !!message, error: message, onChange: (value) => change(field.field, value) }));
1542
- if (field.kind === "enum" && field.options?.length) return wrap(/* @__PURE__ */ jsxs5(NativeSelect2.Root, { size: "sm", children: [
1543
- /* @__PURE__ */ jsxs5(NativeSelect2.Field, { id, "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), onChange: (event) => change(field.field, event.currentTarget.value), children: [
1544
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014 Selecione \u2014" }),
1545
- field.options.map((option) => /* @__PURE__ */ jsx9("option", { value: option, children: option }, option))
1546
- ] }),
1547
- /* @__PURE__ */ jsx9(NativeSelect2.Indicator, {})
1548
- ] }));
1549
- if (field.kind === "boolean") return wrap(/* @__PURE__ */ jsxs5(NativeSelect2.Root, { size: "sm", children: [
1550
- /* @__PURE__ */ jsxs5(NativeSelect2.Field, { id, "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), onChange: (event) => change(field.field, event.currentTarget.value), children: [
1551
- /* @__PURE__ */ jsx9("option", { value: "", children: "\u2014 Selecione \u2014" }),
1552
- /* @__PURE__ */ jsx9("option", { value: "true", children: "Sim" }),
1553
- /* @__PURE__ */ jsx9("option", { value: "false", children: "N\xE3o" })
1554
- ] }),
1555
- /* @__PURE__ */ jsx9(NativeSelect2.Indicator, {})
1556
- ] }));
1557
- if (multiline) return wrap(/* @__PURE__ */ jsx9(Textarea, { id, rows: field.rows ?? 4, resize: "vertical", "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), lineHeight: "1.5", onChange: (event) => change(field.field, event.currentTarget.value) }));
1558
- const type = field.kind === "date" ? "date" : ["number", "currency", "currencyConverted"].includes(field.kind ?? "") ? "number" : "text";
1559
- return wrap(/* @__PURE__ */ jsx9(Input3, { id, type, step: type === "number" ? "any" : void 0, autoComplete: "off", "aria-invalid": !!message, "aria-describedby": errorId, value: values[field.field] ?? "", ...controlProps(!!message), onChange: (event) => change(field.field, event.currentTarget.value) }));
1620
+ /* @__PURE__ */ jsx9(SimpleGrid, { columns: { base: 1, lg: 2 }, gapX: "16px", gapY: "15px", children: items.map((item) => {
1621
+ if (item.type === "field") return renderField(item.field);
1622
+ const first = item.fields[0];
1623
+ const inline = item.fields.some((field) => field.inline !== false);
1624
+ return /* @__PURE__ */ jsxs5(Box4, { gridColumn: "1 / -1", p: "12px", bg: "#FAFCFD", borderWidth: "1px", borderColor: "#E5E9ED", borderRadius: "9px", children: [
1625
+ /* @__PURE__ */ jsx9(Text5, { color: "#344054", fontSize: "11px", fontWeight: "700", children: first.groupLabel ?? item.id }),
1626
+ first.groupDescription ? /* @__PURE__ */ jsx9(Text5, { mt: "2px", mb: "10px", color: "#667085", fontSize: "10px", children: first.groupDescription }) : /* @__PURE__ */ jsx9(Box4, { h: "8px" }),
1627
+ /* @__PURE__ */ jsx9(Flex4, { direction: inline ? "row" : "column", wrap: "wrap", gap: "9px", children: item.fields.map((field) => renderField(field, true)) })
1628
+ ] }, item.id);
1560
1629
  }) })
1561
1630
  ] }) }),
1562
1631
  /* @__PURE__ */ jsxs5(Flex4, { minH: "68px", px: "20px", py: "14px", justify: "flex-end", gap: "9px", borderTopWidth: "1px", borderColor: "#E5E9ED", children: [
@@ -1571,7 +1640,7 @@ function DynamicForm({ title, eyebrow = "Cadastro", fields, initialValues, submi
1571
1640
  }
1572
1641
 
1573
1642
  // src/components/internal/CoreDetailModal.tsx
1574
- import { useEffect as useEffect6, useMemo as useMemo5, useState as useState9 } from "react";
1643
+ import { useEffect as useEffect6, useMemo as useMemo6, useState as useState9 } from "react";
1575
1644
  import { Box as Box8, Button as Button8, Flex as Flex7, Heading as Heading3, Text as Text10 } from "@chakra-ui/react";
1576
1645
  import { useQuery as useQuery6, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
1577
1646
 
@@ -1632,7 +1701,7 @@ function CoreSummaryCards({ cards = [], parent, relations }) {
1632
1701
  }
1633
1702
 
1634
1703
  // src/components/internal/CoreRelatedGrid.tsx
1635
- import { useMemo as useMemo4, useState as useState7 } from "react";
1704
+ import { useMemo as useMemo5, useState as useState7 } from "react";
1636
1705
  import { Badge as Badge3, Box as Box6, Button as Button6, Flex as Flex5, Table as Table2, Text as Text8 } from "@chakra-ui/react";
1637
1706
  import { useMutation, useQuery as useQuery5, useQueryClient } from "@tanstack/react-query";
1638
1707
 
@@ -1748,7 +1817,7 @@ function CoreRelatedGrid({ tab, parent, relations, refreshToken, onRefresh }) {
1748
1817
  const parentValue = relation ? parent[relation.parentKey ?? "_id"] : void 0;
1749
1818
  const key = ["oon", "related", relation?.model, relation?.foreignKey, parentValue, refreshToken];
1750
1819
  const query = useQuery5({ queryKey: key, enabled: !!relation && parentValue != null, queryFn: () => resource.list({ [relation.foreignKey]: parentValue, pageSize: 1e3 }) });
1751
- const columns = useMemo4(() => (tab.columns?.length ? tab.columns : schema?.fields.slice(0, 8).map((f) => f.name) ?? []).map((column) => normalizeColumn(column, schema)).filter((column) => !column.hidden), [tab.columns, schema]);
1820
+ const columns = useMemo5(() => (tab.columns?.length ? tab.columns : schema?.fields.slice(0, 8).map((f) => f.name) ?? []).map((column) => normalizeColumn(column, schema)).filter((column) => !column.hidden), [tab.columns, schema]);
1752
1821
  const [drafts, setDrafts] = useState7({});
1753
1822
  const [errors, setErrors] = useState7({});
1754
1823
  const editable = tab.type === "relatedGrid" && tab.editable && tab.editMode === "inline";
@@ -1876,7 +1945,7 @@ function CoreDetailModal({ model, title, config, recordId, initialRecord, initia
1876
1945
  useEffect6(() => {
1877
1946
  if (parentQuery.data) setParent(parentQuery.data);
1878
1947
  }, [parentQuery.data]);
1879
- const formFields = useMemo5(() => schema ? formFieldsFromMeta(schema.fields) : [], [schema]);
1948
+ const formFields = useMemo6(() => schema ? formFieldsFromMeta(schema.fields) : [], [schema]);
1880
1949
  const refresh = (targets) => {
1881
1950
  const list = targets?.length ? targets : ["self"];
1882
1951
  if (list.includes("parent") || list.includes("all")) {
@@ -1912,13 +1981,24 @@ function CoreDetailModal({ model, title, config, recordId, initialRecord, initia
1912
1981
 
1913
1982
  // src/components/CoreCollection.tsx
1914
1983
  import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1984
+ function mergeFormFields(metadataFields, overrides) {
1985
+ if (!overrides?.length) return metadataFields;
1986
+ const byField = new Map(overrides.map((field) => [field.field, field]));
1987
+ const merged = metadataFields.map((field) => ({ ...field, ...byField.get(field.field) }));
1988
+ const known = new Set(metadataFields.map((field) => field.field));
1989
+ overrides.forEach((field) => {
1990
+ if (!known.has(field.field)) merged.push(field);
1991
+ });
1992
+ return merged;
1993
+ }
1915
1994
  function CoreCollection({ model, label, section = "Cole\xE7\xE3o", mode = "dynamic", endpoint, columns, form, importExport, list, relations, detailModal }) {
1916
1995
  const needsSchema = mode !== "full" || !columns || !form || !endpoint;
1917
1996
  const schemaQuery = useModelSchema(needsSchema ? model : "");
1918
1997
  const schema = schemaQuery.data;
1919
1998
  const basePath = endpoint ?? schema?.basePath ?? `/${model.toLowerCase()}s`;
1920
1999
  const resolvedColumns = (list?.columns?.length ? list.columns.map((column) => normalizeColumn(column, schema)) : columns) ?? (schema ? columnsFromMeta(schema.fields) : []);
1921
- const resolvedForm = form ?? (schema ? formFieldsFromMeta(schema.fields) : []);
2000
+ const metadataForm = schema ? formFieldsFromMeta(schema.fields) : [];
2001
+ const resolvedForm = mode === "full" && form?.length ? form : mergeFormFields(metadataForm, form);
1922
2002
  const resource = useOonResource(basePath);
1923
2003
  const queryClient = useQueryClient3();
1924
2004
  const [formState, setFormState] = useState10({ open: false });
@@ -3438,7 +3518,7 @@ function localLoginUrl() {
3438
3518
  return `/login?redirect=${encodeURIComponent(current)}`;
3439
3519
  }
3440
3520
  function OonApp({ config }) {
3441
- const { http, storage, router } = useMemo6(() => {
3521
+ const { http, storage, router } = useMemo7(() => {
3442
3522
  const storage2 = createTokenStorage(config.app.id);
3443
3523
  const externalLoginUrl = config.auth?.loginUrl ?? config.api.meusAppsUrl;
3444
3524
  const http2 = createRestClient({
@@ -3533,6 +3613,7 @@ function manifestToConfig(manifest, runtime) {
3533
3613
  path: c.path,
3534
3614
  label: c.label,
3535
3615
  section: c.section,
3616
+ form: c.form,
3536
3617
  list: c.list,
3537
3618
  relations: c.relations,
3538
3619
  detailModal: c.detailModal