@open-mercato/ui 0.6.4-develop.4382.1.6b4f656b77 → 0.6.4

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.
Files changed (133) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/AGENTS.md +2 -1
  3. package/dist/backend/AppShell.js +6 -0
  4. package/dist/backend/AppShell.js.map +2 -2
  5. package/dist/backend/CrudForm.js +212 -85
  6. package/dist/backend/CrudForm.js.map +3 -3
  7. package/dist/backend/DataTable.js +77 -26
  8. package/dist/backend/DataTable.js.map +3 -3
  9. package/dist/backend/FlashMessages.js +21 -2
  10. package/dist/backend/FlashMessages.js.map +2 -2
  11. package/dist/backend/JsonBuilder.js +64 -41
  12. package/dist/backend/JsonBuilder.js.map +2 -2
  13. package/dist/backend/PerspectiveSidebar.js +8 -1
  14. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  15. package/dist/backend/RowActions.js +1 -1
  16. package/dist/backend/RowActions.js.map +2 -2
  17. package/dist/backend/conflicts/RecordConflictBanner.js +73 -0
  18. package/dist/backend/conflicts/RecordConflictBanner.js.map +7 -0
  19. package/dist/backend/conflicts/index.js +32 -0
  20. package/dist/backend/conflicts/index.js.map +7 -0
  21. package/dist/backend/conflicts/store.js +50 -0
  22. package/dist/backend/conflicts/store.js.map +7 -0
  23. package/dist/backend/detail/ActivitiesSection.js +1 -0
  24. package/dist/backend/detail/ActivitiesSection.js.map +2 -2
  25. package/dist/backend/detail/InlineEditors.js +2 -51
  26. package/dist/backend/detail/InlineEditors.js.map +3 -3
  27. package/dist/backend/detail/RecordNotFoundState.js +10 -5
  28. package/dist/backend/detail/RecordNotFoundState.js.map +2 -2
  29. package/dist/backend/filters/AdvancedFilterPanel.js +14 -0
  30. package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
  31. package/dist/backend/filters/FilteredEmptyResults.js +12 -4
  32. package/dist/backend/filters/FilteredEmptyResults.js.map +2 -2
  33. package/dist/backend/filters/ListEmptyState.js +44 -0
  34. package/dist/backend/filters/ListEmptyState.js.map +7 -0
  35. package/dist/backend/filters/SearchEmptyResults.js +28 -0
  36. package/dist/backend/filters/SearchEmptyResults.js.map +7 -0
  37. package/dist/backend/icons/lucideRegistry.generated.js +8 -0
  38. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  39. package/dist/backend/injection/useGuardedMutation.js +8 -1
  40. package/dist/backend/injection/useGuardedMutation.js.map +2 -2
  41. package/dist/backend/inputs/MarkdownField.js +20 -0
  42. package/dist/backend/inputs/MarkdownField.js.map +7 -0
  43. package/dist/backend/inputs/MdxEditorImpl.js +98 -0
  44. package/dist/backend/inputs/MdxEditorImpl.js.map +7 -0
  45. package/dist/backend/inputs/SwitchableMarkdownInput.js +6 -38
  46. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
  47. package/dist/backend/markdown/markdownToPlainText.js +29 -0
  48. package/dist/backend/markdown/markdownToPlainText.js.map +2 -2
  49. package/dist/backend/messages/EmailThreadsPanel.js +212 -0
  50. package/dist/backend/messages/EmailThreadsPanel.js.map +7 -0
  51. package/dist/backend/messages/index.js +4 -0
  52. package/dist/backend/messages/index.js.map +2 -2
  53. package/dist/backend/messages/mergeOptimisticEmailThreads.js +55 -0
  54. package/dist/backend/messages/mergeOptimisticEmailThreads.js.map +7 -0
  55. package/dist/backend/notifications/NotificationDispatcher.js +1 -0
  56. package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
  57. package/dist/backend/notifications/useNotificationsPoll.js +7 -0
  58. package/dist/backend/notifications/useNotificationsPoll.js.map +2 -2
  59. package/dist/backend/notifications/useNotificationsSse.js +8 -0
  60. package/dist/backend/notifications/useNotificationsSse.js.map +2 -2
  61. package/dist/backend/sidebar/SidebarCustomizationEditor.js +23 -7
  62. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  63. package/dist/backend/utils/optimisticLock.js +34 -0
  64. package/dist/backend/utils/optimisticLock.js.map +7 -0
  65. package/dist/portal/hooks/useTenantContext.js +1 -1
  66. package/dist/portal/hooks/useTenantContext.js.map +2 -2
  67. package/dist/primitives/alert.js.map +1 -1
  68. package/dist/primitives/checkbox.js +2 -1
  69. package/dist/primitives/checkbox.js.map +2 -2
  70. package/dist/types/css.d.js +1 -0
  71. package/dist/types/css.d.js.map +7 -0
  72. package/package.json +17 -13
  73. package/src/backend/AppShell.tsx +10 -0
  74. package/src/backend/CrudForm.tsx +320 -97
  75. package/src/backend/DataTable.tsx +113 -34
  76. package/src/backend/FlashMessages.tsx +31 -2
  77. package/src/backend/JsonBuilder.tsx +79 -26
  78. package/src/backend/PerspectiveSidebar.tsx +8 -1
  79. package/src/backend/RowActions.tsx +1 -1
  80. package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +0 -1
  81. package/src/backend/__tests__/CrudForm.datetime.test.tsx +0 -1
  82. package/src/backend/__tests__/CrudForm.defaults.test.tsx +0 -1
  83. package/src/backend/__tests__/CrudForm.dotPath.test.tsx +311 -0
  84. package/src/backend/__tests__/CrudForm.doubleSubmit.test.tsx +0 -1
  85. package/src/backend/__tests__/CrudForm.navigation.test.tsx +120 -1
  86. package/src/backend/__tests__/CrudForm.optimisticLock.test.tsx +287 -0
  87. package/src/backend/__tests__/CrudForm.render.test.tsx +74 -2
  88. package/src/backend/__tests__/CrudForm.sortable.test.tsx +0 -1
  89. package/src/backend/__tests__/CrudForm.transformData.test.tsx +0 -1
  90. package/src/backend/__tests__/CrudForm.unsavedNavigation.test.tsx +51 -3
  91. package/src/backend/__tests__/CrudForm.validation.test.tsx +0 -1
  92. package/src/backend/__tests__/CustomDataSection.test.tsx +0 -1
  93. package/src/backend/__tests__/FlashMessages.test.tsx +47 -0
  94. package/src/backend/__tests__/JsonBuilder.test.tsx +135 -0
  95. package/src/backend/__tests__/lazy-heavy-libraries.test.ts +16 -3
  96. package/src/backend/conflicts/RecordConflictBanner.tsx +77 -0
  97. package/src/backend/conflicts/__tests__/RecordConflictBanner.test.tsx +61 -0
  98. package/src/backend/conflicts/__tests__/store.test.ts +78 -0
  99. package/src/backend/conflicts/index.ts +49 -0
  100. package/src/backend/conflicts/store.ts +94 -0
  101. package/src/backend/detail/ActivitiesSection.tsx +1 -0
  102. package/src/backend/detail/InlineEditors.tsx +14 -76
  103. package/src/backend/detail/RecordNotFoundState.tsx +13 -6
  104. package/src/backend/detail/__tests__/RecordNotFoundState.test.tsx +80 -0
  105. package/src/backend/filters/AdvancedFilterPanel.tsx +27 -0
  106. package/src/backend/filters/FilteredEmptyResults.tsx +23 -10
  107. package/src/backend/filters/ListEmptyState.tsx +69 -0
  108. package/src/backend/filters/SearchEmptyResults.tsx +42 -0
  109. package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +98 -0
  110. package/src/backend/filters/__tests__/ListEmptyState.test.tsx +53 -0
  111. package/src/backend/icons/lucideRegistry.generated.tsx +8 -0
  112. package/src/backend/injection/__tests__/useGuardedMutation.optimisticLock.test.tsx +118 -0
  113. package/src/backend/injection/useGuardedMutation.ts +16 -1
  114. package/src/backend/inputs/MarkdownField.tsx +34 -0
  115. package/src/backend/inputs/MdxEditorImpl.tsx +113 -0
  116. package/src/backend/inputs/SwitchableMarkdownInput.tsx +5 -54
  117. package/src/backend/markdown/__tests__/markdownToPlainText.test.ts +13 -0
  118. package/src/backend/markdown/markdownToPlainText.ts +41 -0
  119. package/src/backend/messages/EmailThreadsPanel.tsx +351 -0
  120. package/src/backend/messages/__tests__/mergeOptimisticEmailThreads.test.ts +126 -0
  121. package/src/backend/messages/index.ts +12 -0
  122. package/src/backend/messages/mergeOptimisticEmailThreads.ts +84 -0
  123. package/src/backend/notifications/NotificationDispatcher.ts +2 -0
  124. package/src/backend/notifications/useNotificationsPoll.ts +5 -0
  125. package/src/backend/notifications/useNotificationsSse.ts +6 -0
  126. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +35 -11
  127. package/src/backend/utils/__tests__/optimisticLock.test.ts +126 -0
  128. package/src/backend/utils/optimisticLock.ts +70 -0
  129. package/src/portal/hooks/useTenantContext.ts +7 -2
  130. package/src/primitives/__tests__/checkbox.test.tsx +28 -0
  131. package/src/primitives/alert.tsx +1 -1
  132. package/src/primitives/checkbox.tsx +2 -1
  133. package/src/types/css.d.ts +5 -0
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender } from "@tanstack/react-table";
6
6
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
7
- import { RefreshCw, Loader2, SlidersHorizontal, MoreHorizontal, Circle, Filter, ChevronUp, ChevronDown, ChevronsUpDown, Check } from "lucide-react";
7
+ import { RefreshCw, Loader2, SlidersHorizontal, MoreHorizontal, Circle, Filter, ChevronUp, ChevronDown, ChevronsUpDown, Check, Inbox } from "lucide-react";
8
8
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../primitives/table.js";
9
9
  import { Button } from "../primitives/button.js";
10
10
  import { Checkbox } from "../primitives/checkbox.js";
@@ -22,6 +22,7 @@ import { TooltipProvider } from "../primitives/tooltip.js";
22
22
  import { TruncatedCell } from "./TruncatedCell.js";
23
23
  import { FilterBar } from "./FilterBar.js";
24
24
  import { FilteredEmptyResults } from "./filters/FilteredEmptyResults.js";
25
+ import { SearchEmptyResults } from "./filters/SearchEmptyResults.js";
25
26
  import { useCustomFieldFilterDefs } from "./utils/customFieldFilters.js";
26
27
  import { fetchCustomFieldDefinitionsPayload } from "./utils/customFieldDefs.js";
27
28
  import { RowActions } from "./RowActions.js";
@@ -31,7 +32,9 @@ import { useAppEvent } from "./injection/useAppEvent.js";
31
32
  import { useInjectionDataWidgets } from "./injection/useInjectionDataWidgets.js";
32
33
  import { resolveInjectedIcon } from "./injection/resolveInjectedIcon.js";
33
34
  import { serializeExport, defaultExportFilename } from "@open-mercato/shared/lib/crud/exporters";
34
- import { apiCall } from "./utils/apiCall.js";
35
+ import { apiCall, withScopedApiRequestHeaders } from "./utils/apiCall.js";
36
+ import { buildOptimisticLockHeader } from "./utils/optimisticLock.js";
37
+ import { surfaceRecordConflict } from "./conflicts/index.js";
35
38
  import { raiseCrudError } from "./utils/serverErrors.js";
36
39
  import { PerspectiveSidebar } from "./PerspectiveSidebar.js";
37
40
  import { Popover, PopoverTrigger, PopoverContent } from "../primitives/popover.js";
@@ -643,6 +646,7 @@ function DataTable({
643
646
  injectionSpotId,
644
647
  injectionContext,
645
648
  replacementHandle,
649
+ extensionTableId: extensionTableIdProp,
646
650
  stickyFirstColumn = false,
647
651
  stickyActionsColumn = false,
648
652
  virtualized = false,
@@ -803,14 +807,20 @@ function DataTable({
803
807
  const initialPerspectiveAppliedRef = React.useRef(Boolean(mergedInitialSettings));
804
808
  const extensionTableId = React.useMemo(() => {
805
809
  if (perspective?.tableId) return perspective.tableId;
810
+ if (extensionTableIdProp) return extensionTableIdProp;
806
811
  if (injectionSpotId?.startsWith("data-table:")) return injectionSpotId.slice("data-table:".length);
807
812
  return null;
808
- }, [injectionSpotId, perspective?.tableId]);
809
- const resolvedInjectionSpotId = injectionSpotId ?? (perspective?.tableId ? `data-table:${perspective.tableId}` : null);
813
+ }, [injectionSpotId, perspective?.tableId, extensionTableIdProp]);
814
+ const resolvedInjectionSpotId = injectionSpotId ?? (perspective?.tableId ? `data-table:${perspective.tableId}` : null) ?? (extensionTableIdProp ? `data-table:${extensionTableIdProp}` : null);
810
815
  const resolvedReplacementHandle = replacementHandle ?? ComponentReplacementHandles.dataTable(extensionTableId ?? "unknown");
811
816
  const baseInjectionContext = React.useMemo(
812
- () => injectionContext ?? { tableId: perspective?.tableId ?? null, title: typeof title === "string" ? title : void 0 },
813
- [injectionContext, perspective?.tableId, title]
817
+ () => {
818
+ if (injectionContext) return injectionContext;
819
+ const resolvedTableId = extensionTableId ?? perspective?.tableId ?? null;
820
+ const baseTitle = typeof title === "string" ? title : void 0;
821
+ return { tableId: resolvedTableId, title: baseTitle };
822
+ },
823
+ [injectionContext, perspective?.tableId, extensionTableId, title]
814
824
  );
815
825
  const headerInjectionSpotId = React.useMemo(
816
826
  () => resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:header` : null,
@@ -1217,13 +1227,17 @@ function DataTable({
1217
1227
  if (process.env.NODE_ENV !== "production") {
1218
1228
  console.debug("[DataTable] perspective payload", payload);
1219
1229
  }
1220
- const call = await apiCall(
1221
- `/api/perspectives/${encodeURIComponent(perspectiveTableId)}`,
1222
- {
1223
- method: "POST",
1224
- headers: { "Content-Type": "application/json" },
1225
- body: JSON.stringify(payload)
1226
- }
1230
+ const existing = input.perspectiveId ? perspectiveData?.perspectives.find((p) => p.id === input.perspectiveId) ?? null : null;
1231
+ const call = await withScopedApiRequestHeaders(
1232
+ buildOptimisticLockHeader(existing?.updatedAt ?? null),
1233
+ () => apiCall(
1234
+ `/api/perspectives/${encodeURIComponent(perspectiveTableId)}`,
1235
+ {
1236
+ method: "POST",
1237
+ headers: { "Content-Type": "application/json" },
1238
+ body: JSON.stringify(payload)
1239
+ }
1240
+ )
1227
1241
  );
1228
1242
  if (call.status === 404) {
1229
1243
  throw new Error(t("ui.dataTable.perspectives.error.apiUnavailable", "Perspectives API is not available. Run `yarn generate` to regenerate module routes and restart the dev server."));
@@ -1242,6 +1256,12 @@ function DataTable({
1242
1256
  if (data2.perspective) {
1243
1257
  applyPerspectiveSettings(data2.perspective.settings, data2.perspective.id);
1244
1258
  }
1259
+ },
1260
+ onError: (error2) => {
1261
+ if (perspectiveTableId) {
1262
+ void queryClient.invalidateQueries({ queryKey: perspectiveQueryKey });
1263
+ }
1264
+ surfaceRecordConflict(error2, t);
1245
1265
  }
1246
1266
  });
1247
1267
  const resolveColumnLabel = React.useCallback((column) => {
@@ -1940,6 +1960,20 @@ function DataTable({
1940
1960
  const toolbarWrapperClassName = embedded ? "mt-2" : "mt-3 pt-3 border-t";
1941
1961
  const tableScrollWrapperClassName = embedded ? "" : "overflow-auto";
1942
1962
  const virtualScrollRef = React.useRef(null);
1963
+ const [tableScrollEl, setTableScrollEl] = React.useState(null);
1964
+ const [emptyStateViewportWidth, setEmptyStateViewportWidth] = React.useState(null);
1965
+ const setTableScrollWrapperRef = React.useCallback((node) => {
1966
+ setTableScrollEl(node);
1967
+ virtualScrollRef.current = node;
1968
+ }, []);
1969
+ React.useEffect(() => {
1970
+ if (!tableScrollEl || typeof ResizeObserver === "undefined") return;
1971
+ const update = () => setEmptyStateViewportWidth(tableScrollEl.clientWidth);
1972
+ update();
1973
+ const observer = new ResizeObserver(update);
1974
+ observer.observe(tableScrollEl);
1975
+ return () => observer.disconnect();
1976
+ }, [tableScrollEl]);
1943
1977
  const allRows = table.getRowModel().rows;
1944
1978
  const rowVirtualizer = virtualized ? useVirtualizer({
1945
1979
  count: allRows.length,
@@ -2026,7 +2060,7 @@ function DataTable({
2026
2060
  sensors: dndSensors,
2027
2061
  columnIds: headerColumnIds,
2028
2062
  onDragEnd: handleHeaderDragEnd,
2029
- children: /* @__PURE__ */ jsx("div", { ref: virtualized ? virtualScrollRef : void 0, className: tableScrollWrapperClassName, style: virtualMaxHeightStyle, children: /* @__PURE__ */ jsxs(Table, { className: "min-w-[640px] md:min-w-0", children: [
2063
+ children: /* @__PURE__ */ jsx("div", { ref: setTableScrollWrapperRef, className: tableScrollWrapperClassName, style: virtualMaxHeightStyle, children: /* @__PURE__ */ jsxs(Table, { className: "min-w-[640px] md:min-w-0", children: [
2030
2064
  /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => /* @__PURE__ */ jsxs(TableRow, { children: [
2031
2065
  hasInjectedBulkActions ? /* @__PURE__ */ jsx(TableHead, { className: "w-8", children: /* @__PURE__ */ jsx(
2032
2066
  Checkbox,
@@ -2168,19 +2202,36 @@ function DataTable({
2168
2202
  const bottomPadding = lastItem ? rowVirtualizer.getTotalSize() - lastItem.end : 0;
2169
2203
  return bottomPadding > 0 ? /* @__PURE__ */ jsx("tr", { style: { height: `${bottomPadding}px` } }) : null;
2170
2204
  })() : null
2171
- ] }) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: mergedColumns.length + (rowActions || injectedRowActions.length > 0 ? 1 : 0) + (hasInjectedBulkActions ? 1 : 0), className: "py-6", children: filterAwareEmptyState?.active ? /* @__PURE__ */ jsx(
2172
- FilteredEmptyResults,
2205
+ ] }) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: mergedColumns.length + (rowActions || injectedRowActions.length > 0 ? 1 : 0) + (hasInjectedBulkActions ? 1 : 0), className: "p-0", children: /* @__PURE__ */ jsx(
2206
+ "div",
2173
2207
  {
2174
- entityNamePlural: filterAwareEmptyState.entityNamePlural,
2175
- canRemoveLast: filterAwareEmptyState.canRemoveLast,
2176
- onClearAll: filterAwareEmptyState.onClearAll,
2177
- onRemoveLast: filterAwareEmptyState.onRemoveLast
2178
- }
2179
- ) : emptyState && typeof emptyState !== "string" ? emptyState : /* @__PURE__ */ jsx(
2180
- EmptyState,
2181
- {
2182
- size: "sm",
2183
- title: typeof emptyState === "string" ? emptyState : t("ui.dataTable.emptyState.default", "No results.")
2208
+ className: cn("sticky left-0 flex justify-center py-6", emptyStateViewportWidth ? "" : "w-fit"),
2209
+ style: emptyStateViewportWidth ? { width: emptyStateViewportWidth } : void 0,
2210
+ children: filterAwareEmptyState?.active ? /* @__PURE__ */ jsx(
2211
+ FilteredEmptyResults,
2212
+ {
2213
+ entityNamePlural: filterAwareEmptyState.entityNamePlural,
2214
+ canRemoveLast: filterAwareEmptyState.canRemoveLast,
2215
+ onClearAll: filterAwareEmptyState.onClearAll,
2216
+ onRemoveLast: filterAwareEmptyState.onRemoveLast,
2217
+ onClearSearch: searchValue && searchValue.trim().length > 0 && onSearchChange ? () => onSearchChange("") : void 0
2218
+ }
2219
+ ) : searchValue && searchValue.trim().length > 0 && onSearchChange ? /* @__PURE__ */ jsx(
2220
+ SearchEmptyResults,
2221
+ {
2222
+ query: searchValue.trim(),
2223
+ entityNamePlural: filterAwareEmptyState?.entityNamePlural,
2224
+ onClearSearch: () => onSearchChange("")
2225
+ }
2226
+ ) : emptyState && typeof emptyState !== "string" ? emptyState : /* @__PURE__ */ jsx(
2227
+ EmptyState,
2228
+ {
2229
+ variant: "subtle",
2230
+ icon: /* @__PURE__ */ jsx(Inbox, { className: "size-6", "aria-hidden": true }),
2231
+ title: typeof emptyState === "string" ? emptyState : t("ui.dataTable.emptyState.default", "No results."),
2232
+ description: typeof emptyState === "string" ? void 0 : t("ui.dataTable.emptyState.defaultDescription", "Items will appear here once added.")
2233
+ }
2234
+ )
2184
2235
  }
2185
2236
  ) }) }) })
2186
2237
  ] }) })