@dyrected/admin 2.5.65 → 2.6.1

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.mjs CHANGED
@@ -5,7 +5,7 @@ import { HashRouter, Link, MemoryRouter, Route, Routes, useLocation, useNavigate
5
5
  import { QueryClient, QueryClientProvider, keepPreviousData, useInfiniteQuery, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
6
6
  import { DyrectedError, PREVIEW_TOKEN_PARAM, createClient } from "@dyrected/sdk";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
- import { AlertCircle, AlertTriangle, AlignCenter, AlignLeft, AlignRight, Archive, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpRight, Bold, Braces, Calendar, Check, CheckCircle, CheckCircle2, ChevronDown, ChevronLeft, ChevronLeftIcon, ChevronRight, ChevronRightIcon, ChevronUp, ChevronsUpDown, Circle, Clock, Clock3, Code, Copy, Database, Download, ExternalLink, Eye, EyeOff, FileDown, FileIcon, FileText, FileUp, Filter, Globe, GripVertical, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, History, Home, Image as Image$1, Info, Italic, KeyRound, Layers, LayoutDashboard, LayoutGrid, Library, Link as Link$1, List, ListOrdered, Loader2, Lock, LogOut, Mail, Menu, Monitor, Moon, MoreHorizontal, MousePointer2, PanelLeftClose, PanelLeftOpen, Pencil, Play, Plus, Quote, RotateCcw, Save, Scissors, Search, Settings, Settings2, Share2, Shield, Smartphone, Sparkles, Star, Strikethrough, Sun, Table, Trash2, Underline, Undo2, Upload, UploadCloud, Users, Video, Volume2, Workflow, X, XCircle, icons } from "lucide-react";
8
+ import { AlertCircle, AlertTriangle, AlignCenter, AlignLeft, AlignRight, Archive, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpRight, Bold, Braces, Calendar, Check, CheckCircle, CheckCircle2, ChevronDown, ChevronLeft, ChevronLeftIcon, ChevronRight, ChevronRightIcon, ChevronUp, ChevronsUpDown, Circle, Clock, Clock3, Code, Copy, Database, Download, ExternalLink, Eye, EyeOff, FileDown, FileIcon, FileText, FileUp, Filter, Globe, GripVertical, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, History, Home, Image as Image$1, Info, Italic, KeyRound, Layers, LayoutDashboard, LayoutGrid, Library, Link as Link$1, List, ListOrdered, Loader2, Lock, LogOut, Mail, Menu, MessageSquarePlus, Monitor, Moon, MoreHorizontal, MousePointer2, PanelLeftClose, PanelLeftOpen, Pencil, Play, Plus, Quote, RotateCcw, Save, Scissors, Search, Settings, Settings2, Share2, Shield, Smartphone, Sparkles, Star, Strikethrough, Sun, Table, Trash2, Underline, Undo2, Upload, UploadCloud, Users, Video, Volume2, Workflow, X, XCircle, icons } from "lucide-react";
9
9
  import { clsx } from "clsx";
10
10
  import { extendTailwindMerge } from "tailwind-merge";
11
11
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
@@ -15,7 +15,7 @@ import * as SheetPrimitive from "@radix-ui/react-dialog";
15
15
  import { Toaster, toast } from "sonner";
16
16
  import * as PopoverPrimitive from "@radix-ui/react-popover";
17
17
  import * as SelectPrimitive from "@radix-ui/react-select";
18
- import { flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
18
+ import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
19
19
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
20
20
  import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
21
21
  import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
@@ -812,10 +812,65 @@ function SidebarInner({ schemas, isLoading, location, logout, isEmbedded, collap
812
812
  const collections = (schemas?.collections)?.filter((c) => !c?.admin?.hidden && !c?.slug.startsWith("platform_")) ?? [];
813
813
  const globals = (schemas?.globals)?.filter((g) => !g?.admin?.hidden && !g?.slug.startsWith("platform_")) ?? [];
814
814
  const uploadCollections = collections.filter((c) => c.upload);
815
+ const standardCollections = collections.filter((c) => !c.upload && !c.auth);
816
+ const authCollections = collections.filter((c) => !c.upload && c.auth);
815
817
  const groupLabel = (text) => !collapsed ? /* @__PURE__ */ jsx("p", {
816
818
  className: "dy-px-3 dy-mb-1.5 dy-text-[10px] dy-font-semibold dy-uppercase dy-tracking-widest dy-text-muted-foreground/50",
817
819
  children: text
818
820
  }) : /* @__PURE__ */ jsx("div", { className: "dy-my-2 dy-mx-3 dy-h-px dy-bg-border" });
821
+ const renderCollectionItem = (col) => {
822
+ const isReadOnly = col.access?.read && !col.access?.create && !col.access?.update && !col.access?.delete;
823
+ const navLabel = /* @__PURE__ */ jsxs("div", {
824
+ className: "dy-flex dy-items-center dy-gap-1.5 dy-min-w-0",
825
+ children: [/* @__PURE__ */ jsx("span", {
826
+ className: "dy-truncate",
827
+ children: col.labels?.plural ?? col.label ?? col.slug
828
+ }), !collapsed && /* @__PURE__ */ jsxs("div", {
829
+ className: "dy-flex dy-gap-1 dy-shrink-0",
830
+ children: [
831
+ col.auth && /* @__PURE__ */ jsx(Shield, { className: "dy-h-4 dy-w-4 dy-text-primary/70" }),
832
+ col.shared && /* @__PURE__ */ jsx(Share2, { className: "dy-h-4 dy-w-4 dy-text-purple-500/70" }),
833
+ isReadOnly && /* @__PURE__ */ jsx(Lock, { className: "dy-h-4 dy-w-4 dy-text-muted-foreground/40" })
834
+ ]
835
+ })]
836
+ });
837
+ return /* @__PURE__ */ jsx(NavItem, {
838
+ to: `/collections/${col.slug}`,
839
+ icon: resolveAdminIcon(col.admin?.icon, col.auth ? Users : Database),
840
+ label: navLabel,
841
+ active: location.pathname.startsWith(`/collections/${col.slug}`),
842
+ collapsed,
843
+ onClick: onNavigate
844
+ }, col.slug);
845
+ };
846
+ const renderCollectionSection = (sectionCollections, ungroupedLabel) => {
847
+ if (sectionCollections.length === 0) return null;
848
+ const groups = /* @__PURE__ */ new Map();
849
+ const ungrouped = [];
850
+ sectionCollections.forEach((col) => {
851
+ const groupName = col.admin?.group;
852
+ if (groupName) {
853
+ if (!groups.has(groupName)) groups.set(groupName, []);
854
+ groups.get(groupName).push(col);
855
+ return;
856
+ }
857
+ ungrouped.push(col);
858
+ });
859
+ return /* @__PURE__ */ jsxs("div", {
860
+ className: "dy-space-y-1",
861
+ children: [Array.from(groups.entries()).map(([groupName, cols]) => /* @__PURE__ */ jsx(NavGroup, {
862
+ label: groupName,
863
+ collapsed,
864
+ defaultExpanded: true,
865
+ children: cols.map((col) => renderCollectionItem(col))
866
+ }, groupName)), ungrouped.length > 0 && /* @__PURE__ */ jsx(NavGroup, {
867
+ label: ungroupedLabel,
868
+ collapsed,
869
+ defaultExpanded: true,
870
+ children: ungrouped.map((col) => renderCollectionItem(col))
871
+ })]
872
+ });
873
+ };
819
874
  const branding = schemas?.admin?.branding;
820
875
  const meta = schemas?.admin?.meta;
821
876
  return /* @__PURE__ */ jsxs("div", {
@@ -870,65 +925,17 @@ function SidebarInner({ schemas, isLoading, location, logout, isEmbedded, collap
870
925
  collapsed,
871
926
  onClick: onNavigate
872
927
  }, col.slug))] }),
873
- (isLoading || collections.filter((c) => !c.upload).length > 0) && /* @__PURE__ */ jsx("div", { children: isLoading ? /* @__PURE__ */ jsx("div", {
928
+ (isLoading || standardCollections.length > 0 || authCollections.length > 0) && /* @__PURE__ */ jsx("div", { children: isLoading ? /* @__PURE__ */ jsx("div", {
874
929
  className: "dy-space-y-1 dy-px-1",
875
930
  children: [
876
931
  1,
877
932
  2,
878
933
  3
879
934
  ].map((i) => /* @__PURE__ */ jsx("div", { className: cn("dy-h-8 dy-rounded-md dy-bg-muted/60 dy-animate-pulse", collapsed ? "dy-mx-1" : "dy-mx-2") }, i))
880
- }) : (() => {
881
- const nonUpload = collections.filter((col) => !col.upload);
882
- const groups = /* @__PURE__ */ new Map();
883
- const ungrouped = [];
884
- nonUpload.forEach((col) => {
885
- let g = col.admin?.group;
886
- if (!g && col.auth) g = "System";
887
- if (g) {
888
- if (!groups.has(g)) groups.set(g, []);
889
- groups.get(g).push(col);
890
- } else ungrouped.push(col);
891
- });
892
- const renderCollectionItem = (col) => {
893
- const isReadOnly = col.access?.read && !col.access?.create && !col.access?.update && !col.access?.delete;
894
- const navLabel = /* @__PURE__ */ jsxs("div", {
895
- className: "dy-flex dy-items-center dy-gap-1.5 dy-min-w-0",
896
- children: [/* @__PURE__ */ jsx("span", {
897
- className: "dy-truncate",
898
- children: col.labels?.plural ?? col.label ?? col.slug
899
- }), !collapsed && /* @__PURE__ */ jsxs("div", {
900
- className: "dy-flex dy-gap-1 dy-shrink-0",
901
- children: [
902
- col.auth && /* @__PURE__ */ jsx(Shield, { className: "dy-h-4 dy-w-4 dy-text-primary/70" }),
903
- col.shared && /* @__PURE__ */ jsx(Share2, { className: "dy-h-4 dy-w-4 dy-text-purple-500/70" }),
904
- isReadOnly && /* @__PURE__ */ jsx(Lock, { className: "dy-h-4 dy-w-4 dy-text-muted-foreground/40" })
905
- ]
906
- })]
907
- });
908
- return /* @__PURE__ */ jsx(NavItem, {
909
- to: `/collections/${col.slug}`,
910
- icon: resolveAdminIcon(col.admin?.icon, col.auth ? Users : Database),
911
- label: navLabel,
912
- active: location.pathname.startsWith(`/collections/${col.slug}`),
913
- collapsed,
914
- onClick: onNavigate
915
- }, col.slug);
916
- };
917
- return /* @__PURE__ */ jsxs("div", {
918
- className: "dy-space-y-1",
919
- children: [Array.from(groups.entries()).map(([groupName, cols]) => /* @__PURE__ */ jsx(NavGroup, {
920
- label: groupName,
921
- collapsed,
922
- defaultExpanded: true,
923
- children: cols.map((col) => renderCollectionItem(col))
924
- }, groupName)), ungrouped.length > 0 && /* @__PURE__ */ jsx(NavGroup, {
925
- label: "Collections",
926
- collapsed,
927
- defaultExpanded: true,
928
- children: ungrouped.map((col) => renderCollectionItem(col))
929
- })]
930
- });
931
- })() }),
935
+ }) : /* @__PURE__ */ jsxs("div", {
936
+ className: "dy-space-y-1",
937
+ children: [renderCollectionSection(standardCollections, "Collections"), renderCollectionSection(authCollections, "Auth")]
938
+ }) }),
932
939
  globals.length > 0 && /* @__PURE__ */ jsxs("div", { children: [groupLabel("Configuration"), /* @__PURE__ */ jsx("div", {
933
940
  className: "dy-space-y-0.5",
934
941
  children: globals.map((glob) => /* @__PURE__ */ jsx(NavItem, {
@@ -1038,7 +1045,7 @@ function isNewerVersion(latest, current) {
1038
1045
  return false;
1039
1046
  }
1040
1047
  function useUpdateCheck() {
1041
- const currentVersion = "2.5.65";
1048
+ const currentVersion = "2.6.0";
1042
1049
  const [updateInfo, setUpdateInfo] = useState(() => {
1043
1050
  if (typeof window === "undefined") return null;
1044
1051
  const cacheKey = "dyrected_latest_release";
@@ -1264,7 +1271,7 @@ function getStatusLabel(doc) {
1264
1271
  }
1265
1272
  function Dashboard() {
1266
1273
  const { client, components, user } = useDyrected();
1267
- const currentVersion = "2.5.65";
1274
+ const currentVersion = "2.6.0";
1268
1275
  const [latestVersion, setLatestVersion] = useState(() => {
1269
1276
  if (typeof window === "undefined") return null;
1270
1277
  return localStorage.getItem("dyrected_latest_release");
@@ -2238,9 +2245,8 @@ function usePreferences(key, defaultValue) {
2238
2245
  //#endregion
2239
2246
  //#region src/components/ui/data-table.tsx
2240
2247
  var EMPTY_VISIBILITY = {};
2241
- function DataTable({ columns, data, searchKey, rowSelection: externalRowSelection, onRowSelectionChange, bulkActions, toolbarActions, persistenceKey, initialColumnVisibility, hideViewButton = false }) {
2248
+ function DataTable({ columns, data, searchPlaceholder, searchValue, onSearchChange, rowSelection: externalRowSelection, onRowSelectionChange, bulkActions, toolbarActions, persistenceKey, initialColumnVisibility, hideViewButton = false }) {
2242
2249
  const [sorting, setSorting] = React$1.useState([]);
2243
- const [columnFilters, setColumnFilters] = React$1.useState([]);
2244
2250
  const [columnVisibility, setColumnVisibility] = usePreferences(persistenceKey ? `visibility_${persistenceKey}` : "temp_visibility", initialColumnVisibility ?? EMPTY_VISIBILITY);
2245
2251
  const [internalRowSelection, setInternalRowSelection] = React$1.useState({});
2246
2252
  const rowSelection = externalRowSelection || internalRowSelection;
@@ -2249,15 +2255,12 @@ function DataTable({ columns, data, searchKey, rowSelection: externalRowSelectio
2249
2255
  data,
2250
2256
  columns,
2251
2257
  onSortingChange: setSorting,
2252
- onColumnFiltersChange: setColumnFilters,
2253
2258
  getCoreRowModel: getCoreRowModel(),
2254
2259
  getSortedRowModel: getSortedRowModel(),
2255
- getFilteredRowModel: getFilteredRowModel(),
2256
2260
  onColumnVisibilityChange: setColumnVisibility,
2257
2261
  onRowSelectionChange: setRowSelection,
2258
2262
  state: {
2259
2263
  sorting,
2260
- columnFilters,
2261
2264
  columnVisibility,
2262
2265
  rowSelection
2263
2266
  }
@@ -2289,15 +2292,12 @@ function DataTable({ columns, data, searchKey, rowSelection: externalRowSelectio
2289
2292
  })
2290
2293
  })] })]
2291
2294
  }),
2292
- searchKey && (() => {
2293
- const col = table.getColumn(searchKey);
2294
- return /* @__PURE__ */ jsx(Input, {
2295
- placeholder: `Search by ${(col && typeof col.columnDef.header === "string" ? col.columnDef.header : searchKey).toLowerCase()}...`,
2296
- value: table.getColumn(searchKey)?.getFilterValue() ?? "",
2297
- onChange: (event) => table.getColumn(searchKey)?.setFilterValue(event.target.value),
2298
- className: "dy-order-2 dy-h-9 dy-w-full sm:dy-order-1 sm:dy-max-w-sm"
2299
- });
2300
- })(),
2295
+ onSearchChange ? /* @__PURE__ */ jsx(Input, {
2296
+ placeholder: searchPlaceholder || "Search...",
2297
+ value: searchValue ?? "",
2298
+ onChange: (event) => onSearchChange(event.target.value),
2299
+ className: "dy-order-2 dy-h-9 dy-w-full sm:dy-order-1 sm:dy-max-w-sm"
2300
+ }) : null,
2301
2301
  bulkActions && table.getFilteredSelectedRowModel().rows.length > 0 && /* @__PURE__ */ jsx("div", {
2302
2302
  className: "dy-order-3 dy-flex dy-w-full dy-items-center dy-gap-2 dy-animate-in dy-slide-in-from-left-2 sm:dy-order-2 sm:dy-w-auto",
2303
2303
  children: bulkActions(table.getFilteredSelectedRowModel().rows.map((r) => r.original.id))
@@ -3058,6 +3058,135 @@ function CsvImporter({ slug, schema, onClose }) {
3058
3058
  });
3059
3059
  }
3060
3060
  //#endregion
3061
+ //#region src/lib/document-title.ts
3062
+ var FALLBACK_FIELD_NAMES = [
3063
+ "title",
3064
+ "name",
3065
+ "label",
3066
+ "heading",
3067
+ "email",
3068
+ "subject"
3069
+ ];
3070
+ var MAX_TITLE_DEPTH = 8;
3071
+ function findField(collection, fieldName) {
3072
+ if (!collection?.fields || !fieldName) return void 0;
3073
+ return collection.fields.find((field) => field.name === fieldName);
3074
+ }
3075
+ function findNestedField(fields, fieldName) {
3076
+ if (!fields || !fieldName) return void 0;
3077
+ return fields.find((field) => field.name === fieldName);
3078
+ }
3079
+ function resolveCollectionTitleFieldName(collection) {
3080
+ const configured = findField(collection, collection?.admin?.useAsTitle);
3081
+ if (configured?.name) return configured.name;
3082
+ for (const fieldName of FALLBACK_FIELD_NAMES) {
3083
+ const field = findField(collection, fieldName);
3084
+ if (field?.name) return field.name;
3085
+ }
3086
+ return collection?.fields?.find((field) => !!field.name && field.type !== "join" && field.type !== "row")?.name;
3087
+ }
3088
+ function getObjectFallbackTitle(value) {
3089
+ for (const key of FALLBACK_FIELD_NAMES) {
3090
+ const candidate = value[key];
3091
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
3092
+ }
3093
+ const idValue = value.id;
3094
+ if (typeof idValue === "string" && idValue.trim()) return idValue;
3095
+ if (typeof idValue === "number") return String(idValue);
3096
+ return null;
3097
+ }
3098
+ function resolveStructuredTitleFieldName(field) {
3099
+ if (!field?.fields?.length) return void 0;
3100
+ const configured = findNestedField(field.fields, field.admin?.useAsTitle);
3101
+ if (configured?.name) return configured.name;
3102
+ for (const fieldName of FALLBACK_FIELD_NAMES) {
3103
+ const candidate = findNestedField(field.fields, fieldName);
3104
+ if (candidate?.name) return candidate.name;
3105
+ }
3106
+ return field.fields.find((childField) => !!childField.name && childField.type !== "join" && childField.type !== "row")?.name;
3107
+ }
3108
+ function summarizeTitles(values, limit = 3) {
3109
+ const unique = values.filter(Boolean);
3110
+ if (unique.length === 0) return null;
3111
+ if (unique.length <= limit) return unique.join(", ");
3112
+ return `${unique.slice(0, limit).join(", ")} +${unique.length - limit} more`;
3113
+ }
3114
+ function resolveRelationshipValueTitle(value, field, collections, depth) {
3115
+ if (!field || field.type !== "relationship" || depth >= MAX_TITLE_DEPTH) return null;
3116
+ const relatedCollection = collections?.find((collection) => collection.slug === field.relationTo || collection.slug === field.collection);
3117
+ const relatedTitleFieldName = resolveCollectionTitleFieldName(relatedCollection);
3118
+ const relatedTitleField = findField(relatedCollection, relatedTitleFieldName);
3119
+ const resolveOne = (item) => {
3120
+ if (item == null) return null;
3121
+ if (typeof item === "string" || typeof item === "number") return String(item);
3122
+ if (typeof item !== "object" || Array.isArray(item)) return null;
3123
+ const record = item;
3124
+ if (relatedTitleFieldName && relatedTitleField) {
3125
+ const relatedValue = record[relatedTitleFieldName];
3126
+ const resolved = resolveValueTitleInternal(relatedValue, relatedTitleField, collections, depth + 1);
3127
+ if (resolved) return resolved;
3128
+ }
3129
+ return getObjectFallbackTitle(record);
3130
+ };
3131
+ if (Array.isArray(value)) return summarizeTitles(value.map((item) => resolveOne(item)).filter((item) => !!item));
3132
+ return resolveOne(value);
3133
+ }
3134
+ function resolveStructuredObjectTitle(value, field, collections, depth) {
3135
+ if (!field?.fields?.length || depth >= MAX_TITLE_DEPTH) return getObjectFallbackTitle(value);
3136
+ const titleFieldName = resolveStructuredTitleFieldName(field);
3137
+ const titleField = findNestedField(field.fields, titleFieldName);
3138
+ if (titleFieldName && titleField) {
3139
+ const resolved = resolveValueTitleInternal(value[titleFieldName], titleField, collections, depth + 1);
3140
+ if (resolved) return resolved;
3141
+ }
3142
+ return getObjectFallbackTitle(value);
3143
+ }
3144
+ function resolveStructuredArrayTitle(value, field, collections, depth) {
3145
+ if (depth >= MAX_TITLE_DEPTH) return null;
3146
+ return summarizeTitles(value.map((item) => {
3147
+ if (item == null) return null;
3148
+ if (typeof item !== "object" || Array.isArray(item)) return resolveValueTitleInternal(item, void 0, collections, depth + 1);
3149
+ return resolveStructuredObjectTitle(item, field, collections, depth + 1);
3150
+ }).filter((item) => !!item));
3151
+ }
3152
+ function resolveValueTitle(value, field, collections) {
3153
+ return resolveValueTitleInternal(value, field, collections, 0);
3154
+ }
3155
+ function resolveValueTitleInternal(value, field, collections, depth) {
3156
+ if (value == null) return null;
3157
+ if (field?.type === "relationship") {
3158
+ const relationTitle = resolveRelationshipValueTitle(value, field, collections, depth);
3159
+ if (relationTitle) return relationTitle;
3160
+ }
3161
+ if (field?.type === "array" && Array.isArray(value)) {
3162
+ const arrayTitle = resolveStructuredArrayTitle(value, field, collections, depth);
3163
+ if (arrayTitle) return arrayTitle;
3164
+ }
3165
+ if (field?.type === "object" && typeof value === "object" && !Array.isArray(value)) {
3166
+ const objectTitle = resolveStructuredObjectTitle(value, field, collections, depth);
3167
+ if (objectTitle) return objectTitle;
3168
+ }
3169
+ if (Array.isArray(value)) return summarizeTitles(value.map((item) => resolveValueTitleInternal(item, void 0, collections, depth + 1)).filter((item) => !!item));
3170
+ if (typeof value === "string") return value;
3171
+ if (typeof value === "number") return String(value);
3172
+ if (typeof value === "boolean") return value ? "true" : "false";
3173
+ if (typeof value === "object") return getObjectFallbackTitle(value);
3174
+ return null;
3175
+ }
3176
+ function resolveDocumentTitle(args) {
3177
+ const { entry, collection, collections } = args;
3178
+ if (!entry) return "";
3179
+ const titleFieldName = resolveCollectionTitleFieldName(collection);
3180
+ const titleField = findField(collection, titleFieldName);
3181
+ if (titleFieldName && titleField) {
3182
+ const title = resolveValueTitle(entry[titleFieldName], titleField, collections);
3183
+ if (title) return title;
3184
+ }
3185
+ const fallback = getObjectFallbackTitle(entry);
3186
+ if (fallback) return fallback;
3187
+ return String(entry.id ?? "");
3188
+ }
3189
+ //#endregion
3061
3190
  //#region src/lib/format.ts
3062
3191
  /** Normalize the shorthand string form into the object form. */
3063
3192
  function resolveNumberFormat(format) {
@@ -3409,6 +3538,23 @@ function RenderCell({ value, field, client, schemas }) {
3409
3538
  children: formatDate(value, field.admin?.format, field.type)
3410
3539
  })]
3411
3540
  });
3541
+ if (field.type === "icon") {
3542
+ if (!isAdminIconName(value)) return /* @__PURE__ */ jsx("span", {
3543
+ className: "dy-text-muted-foreground",
3544
+ children: "-"
3545
+ });
3546
+ return /* @__PURE__ */ jsxs("div", {
3547
+ className: "dy-inline-flex dy-items-center dy-gap-2 dy-text-foreground",
3548
+ children: [/* @__PURE__ */ jsx(resolveAdminIcon(value, Calendar), {
3549
+ className: "dy-h-4 dy-w-4 dy-shrink-0",
3550
+ "aria-hidden": "true"
3551
+ }), /* @__PURE__ */ jsx("span", {
3552
+ className: "dy-text-sm dy-font-medium",
3553
+ title: String(value),
3554
+ children: String(value)
3555
+ })]
3556
+ });
3557
+ }
3412
3558
  const relationTo = field.relationTo || field.collection;
3413
3559
  if (field.type === "image" || field.type === "relationship" && isUploadCollection(relationTo, schemas)) {
3414
3560
  if (!value) return /* @__PURE__ */ jsx("span", {
@@ -3430,8 +3576,7 @@ function RenderCell({ value, field, client, schemas }) {
3430
3576
  });
3431
3577
  }
3432
3578
  if (field.type === "relationship" && typeof value === "object") {
3433
- const relTo = field.relationTo || field.collection;
3434
- const displayValue = value[(schemas?.collections?.find((c) => c?.slug === relTo))?.admin?.useAsTitle || "title"] || value.name || value.id || "Unknown";
3579
+ const displayValue = resolveValueTitle(value, field, schemas?.collections) || "Unknown";
3435
3580
  return /* @__PURE__ */ jsx("div", {
3436
3581
  className: "dy-flex dy-items-center dy-gap-2",
3437
3582
  children: /* @__PURE__ */ jsx(Badge, {
@@ -3445,22 +3590,34 @@ function RenderCell({ value, field, client, schemas }) {
3445
3590
  className: "dy-text-[11px] dy-text-muted-foreground dy-font-mono dy-bg-muted/30 dy-px-1.5 dy-py-0.5 dy-rounded",
3446
3591
  children: formatJson(value, field.admin?.format)
3447
3592
  });
3448
- if (Array.isArray(value)) return /* @__PURE__ */ jsxs("div", {
3449
- className: "dy-flex dy-flex-wrap dy-gap-1",
3450
- children: [value.slice(0, 2).map((item, i) => /* @__PURE__ */ jsx(Badge, {
3451
- variant: "outline",
3452
- className: "dy-text-[10px] dy-px-1.5 dy-h-5",
3453
- children: typeof item === "object" ? item.title || item.name || item.id : String(item)
3454
- }, i)), value.length > 2 && /* @__PURE__ */ jsxs("span", {
3455
- className: "dy-text-[10px] dy-text-muted-foreground",
3456
- children: [
3457
- "+",
3458
- value.length - 2,
3459
- " more"
3460
- ]
3461
- })]
3462
- });
3593
+ if (Array.isArray(value)) {
3594
+ const structuredTitle = resolveValueTitle(value, field, schemas?.collections);
3595
+ if (structuredTitle) return /* @__PURE__ */ jsx("span", {
3596
+ className: "dy-text-[11px] dy-text-muted-foreground dy-font-medium dy-leading-tight",
3597
+ children: structuredTitle
3598
+ });
3599
+ return /* @__PURE__ */ jsxs("div", {
3600
+ className: "dy-flex dy-flex-wrap dy-gap-1",
3601
+ children: [value.slice(0, 2).map((item, i) => /* @__PURE__ */ jsx(Badge, {
3602
+ variant: "outline",
3603
+ className: "dy-text-[10px] dy-px-1.5 dy-h-5",
3604
+ children: typeof item === "object" ? item.title || item.name || item.id : String(item)
3605
+ }, i)), value.length > 2 && /* @__PURE__ */ jsxs("span", {
3606
+ className: "dy-text-[10px] dy-text-muted-foreground",
3607
+ children: [
3608
+ "+",
3609
+ value.length - 2,
3610
+ " more"
3611
+ ]
3612
+ })]
3613
+ });
3614
+ }
3463
3615
  if (typeof value === "object" && !Array.isArray(value)) {
3616
+ const structuredTitle = resolveValueTitle(value, field, schemas?.collections);
3617
+ if (structuredTitle) return /* @__PURE__ */ jsx("span", {
3618
+ className: "dy-text-[11px] dy-text-muted-foreground dy-font-medium dy-leading-tight",
3619
+ children: structuredTitle
3620
+ });
3464
3621
  const entries = Object.entries(value).filter(([, v]) => typeof v !== "object" && v !== null && v !== void 0).slice(0, 3);
3465
3622
  if (entries.length > 0) return /* @__PURE__ */ jsxs("span", {
3466
3623
  className: "dy-text-[11px] dy-text-muted-foreground dy-font-medium dy-leading-tight",
@@ -6389,6 +6546,16 @@ function JoinField({ schema, control }) {
6389
6546
  name: schema.name
6390
6547
  })?.docs;
6391
6548
  const getLabel = (item) => item[displayField] || item.name || item.slug || item.id;
6549
+ const showCreateButton = schema.admin?.showCreateButton !== false;
6550
+ const showViewButton = schema.admin?.showViewButton !== false;
6551
+ const handleCreateNew = () => {
6552
+ const params = new URLSearchParams({ [onField]: docId });
6553
+ navigate(`/collections/${targetCollection}/new?${params.toString()}`);
6554
+ };
6555
+ const handleViewAll = () => {
6556
+ const params = new URLSearchParams({ where: JSON.stringify({ [onField]: { equals: docId } }) });
6557
+ navigate(`/collections/${targetCollection}?${params.toString()}`);
6558
+ };
6392
6559
  if (!docId) return /* @__PURE__ */ jsxs("p", {
6393
6560
  className: "dy-text-xs dy-text-muted-foreground/60 dy-italic dy-py-2",
6394
6561
  children: [
@@ -6423,17 +6590,27 @@ function JoinField({ schema, control }) {
6423
6590
  " found."
6424
6591
  ]
6425
6592
  })
6426
- }), /* @__PURE__ */ jsxs(Button, {
6427
- type: "button",
6428
- variant: "outline",
6429
- size: "sm",
6430
- className: "dy-h-8 dy-text-[11px] dy-font-bold dy-rounded-lg dy-border-primary/20 hover:dy-bg-primary/5 hover:dy-text-primary dy-transition-all dy-shadow-sm",
6431
- onClick: () => navigate(`/collections/${targetCollection}/new?${onField}=${docId}`),
6432
- children: [
6433
- /* @__PURE__ */ jsx(Plus, { className: "dy-w-3 dy-h-3 dy-mr-1.5" }),
6434
- "Create new ",
6435
- targetSchema?.labels?.singular || targetCollection
6436
- ]
6593
+ }), (showCreateButton || showViewButton) && /* @__PURE__ */ jsxs("div", {
6594
+ className: cn("dy-flex dy-items-center dy-gap-3", showCreateButton && showViewButton ? "dy-justify-between" : showCreateButton ? "dy-justify-end" : "dy-justify-start"),
6595
+ children: [showViewButton && /* @__PURE__ */ jsxs(Button, {
6596
+ type: "button",
6597
+ variant: "ghost",
6598
+ size: "sm",
6599
+ className: "dy-h-8 dy-text-[11px] dy-font-semibold dy-rounded-lg dy-text-muted-foreground hover:dy-text-foreground hover:dy-bg-muted/60 dy-transition-all",
6600
+ onClick: handleViewAll,
6601
+ children: [/* @__PURE__ */ jsx(ExternalLink, { className: "dy-w-3 dy-h-3 dy-mr-1.5" }), "View all"]
6602
+ }), showCreateButton && /* @__PURE__ */ jsxs(Button, {
6603
+ type: "button",
6604
+ variant: "outline",
6605
+ size: "sm",
6606
+ className: "dy-h-8 dy-text-[11px] dy-font-bold dy-rounded-lg dy-border-primary/20 hover:dy-bg-primary/5 hover:dy-text-primary dy-transition-all dy-shadow-sm",
6607
+ onClick: handleCreateNew,
6608
+ children: [
6609
+ /* @__PURE__ */ jsx(Plus, { className: "dy-w-3 dy-h-3 dy-mr-1.5" }),
6610
+ "Create new ",
6611
+ targetSchema?.labels?.singular || targetCollection
6612
+ ]
6613
+ })]
6437
6614
  })]
6438
6615
  });
6439
6616
  }
@@ -8970,7 +9147,7 @@ function ArrayFieldRenderer({ schema, basePath, control, renderField }) {
8970
9147
  const { schemas } = useDyrected();
8971
9148
  const { drillInEnabled, drillInto, reconcileAfterMutation, activePath, registerFieldArray, unregisterFieldArray } = useNestedEditor();
8972
9149
  const [isBulkOpen, setIsBulkOpen] = React$1.useState(false);
8973
- const [expandedIds, setExpandedIds] = React$1.useState({});
9150
+ const [expandedIds, setExpandedIds] = React$1.useState(() => Object.fromEntries(fields.map((field) => [field.id, true])));
8974
9151
  const watchedItems = useWatch({
8975
9152
  control,
8976
9153
  name: basePath
@@ -9057,7 +9234,7 @@ function ArrayFieldRenderer({ schema, basePath, control, renderField }) {
9057
9234
  });
9058
9235
  nextIds.forEach((id) => {
9059
9236
  if (next[id] === void 0) {
9060
- next[id] = false;
9237
+ next[id] = true;
9061
9238
  changed = true;
9062
9239
  }
9063
9240
  });
@@ -9552,10 +9729,29 @@ function FormFieldRendererInner({ schema, basePath, control, collection, canUpda
9552
9729
  });
9553
9730
  }
9554
9731
  //#endregion
9732
+ //#region src/lib/workflow-autosave.ts
9733
+ var DEFAULT_WORKFLOW_AUTOSAVE_DELAY_MS = 1500;
9734
+ function isWorkflowEnabledCollection(collection) {
9735
+ return Boolean(collection?.workflow || collection?.drafts);
9736
+ }
9737
+ function resolveWorkflowAutosaveSettings(collection) {
9738
+ if (!isWorkflowEnabledCollection(collection)) return {
9739
+ enabled: false,
9740
+ delayMs: DEFAULT_WORKFLOW_AUTOSAVE_DELAY_MS
9741
+ };
9742
+ return {
9743
+ enabled: collection?.admin?.autosave !== false,
9744
+ delayMs: collection?.admin?.autosaveDelayMs ?? 1500
9745
+ };
9746
+ }
9747
+ function classifyWorkflowAutosaveError(error) {
9748
+ return (typeof error === "object" && error !== null && "statusCode" in error ? error.statusCode : void 0) === 409 ? "conflict" : "error";
9749
+ }
9750
+ //#endregion
9555
9751
  //#region src/components/forms/form-engine.tsx
9556
9752
  /** Query-string key holding the active form tab (replace-navigated). */
9557
9753
  var TAB_PARAM = "tab";
9558
- function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onChange, isLoading, submitLabel = "Save", hideSubmit = false, readOnly, onDataChange, passwordChangeMode = null, documentId, defaultTabLabel }) {
9754
+ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onChange, isLoading, submitLabel = "Save", hideSubmit = false, readOnly, onDataChange, passwordChangeMode = null, documentId, defaultTabLabel, autosave }, ref) {
9559
9755
  const { activePath, navigateToPath, getStableId } = useNestedEditor();
9560
9756
  const [searchParams, setSearchParams] = useSearchParams();
9561
9757
  const isDrilledIn = activePath.length > 0;
@@ -9603,6 +9799,12 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
9603
9799
  });
9604
9800
  const { isDirty } = form.formState;
9605
9801
  const flatErrors = getFlatErrors(form.formState.errors);
9802
+ const autosaveEnabled = Boolean(autosave?.enabled && autosave.onSave && !readOnly);
9803
+ const autosaveStateRef = React.useRef("idle");
9804
+ const emitAutosaveState = useCallback((state) => {
9805
+ autosaveStateRef.current = state;
9806
+ autosave?.onStatusChange?.(state);
9807
+ }, [autosave]);
9606
9808
  useEffect(() => {
9607
9809
  if (flatErrors.length > 0 && form.formState.submitCount > 0) console.warn("[Validation] Submission failed with errors:", form.formState.errors);
9608
9810
  }, [
@@ -9613,16 +9815,71 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
9613
9815
  useEffect(() => {
9614
9816
  onChange?.(isDirty);
9615
9817
  }, [isDirty, onChange]);
9818
+ useEffect(() => {
9819
+ if (!autosaveEnabled) {
9820
+ emitAutosaveState("idle");
9821
+ return;
9822
+ }
9823
+ if (!isDirty) {
9824
+ if (autosaveStateRef.current !== "saving") emitAutosaveState("saved");
9825
+ return;
9826
+ }
9827
+ if (autosaveStateRef.current !== "saving") emitAutosaveState("dirty");
9828
+ }, [
9829
+ autosaveEnabled,
9830
+ emitAutosaveState,
9831
+ isDirty
9832
+ ]);
9616
9833
  const watchedValues = useWatch({ control: form.control });
9617
- const handleFormSubmit = useCallback(async (data) => {
9834
+ const persistFormData = useCallback(async (data, persist, options) => {
9618
9835
  const draftKey = `dyrected_draft:${collection}:${defaultValues?.id || "global"}`;
9619
- localStorage.removeItem(draftKey);
9620
- await onSubmit(data);
9836
+ if (options?.reportAutosaveStatus) emitAutosaveState("saving");
9837
+ try {
9838
+ localStorage.removeItem(draftKey);
9839
+ const result = await persist(data);
9840
+ form.reset(data);
9841
+ if (options?.reportAutosaveStatus) emitAutosaveState("saved");
9842
+ return result;
9843
+ } catch (error) {
9844
+ if (options?.reportAutosaveStatus) emitAutosaveState(classifyWorkflowAutosaveError(error));
9845
+ throw error;
9846
+ }
9621
9847
  }, [
9622
9848
  collection,
9623
9849
  defaultValues,
9624
- onSubmit
9850
+ emitAutosaveState,
9851
+ form
9852
+ ]);
9853
+ const handleFormSubmit = useCallback(async (data) => {
9854
+ return persistFormData(data, onSubmit, { reportAutosaveStatus: autosaveEnabled });
9855
+ }, [
9856
+ autosaveEnabled,
9857
+ onSubmit,
9858
+ persistFormData
9859
+ ]);
9860
+ const submitCurrentDraft = useCallback(() => {
9861
+ return new Promise((resolve, reject) => {
9862
+ form.handleSubmit(async (data) => {
9863
+ try {
9864
+ resolve(await handleFormSubmit(data));
9865
+ } catch (error) {
9866
+ reject(error);
9867
+ }
9868
+ }, () => {
9869
+ if (autosaveEnabled) emitAutosaveState("dirty");
9870
+ reject(/* @__PURE__ */ new Error("Please resolve validation errors before continuing."));
9871
+ })();
9872
+ });
9873
+ }, [
9874
+ autosaveEnabled,
9875
+ emitAutosaveState,
9876
+ form,
9877
+ handleFormSubmit
9625
9878
  ]);
9879
+ React.useImperativeHandle(ref, () => ({
9880
+ submitCurrentDraft,
9881
+ isDirty: () => form.formState.isDirty
9882
+ }), [form.formState.isDirty, submitCurrentDraft]);
9626
9883
  useEffect(() => {
9627
9884
  if (readOnly || isLoading) return;
9628
9885
  const handleKeyDown = (e) => {
@@ -9691,6 +9948,24 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
9691
9948
  collection,
9692
9949
  defaultValues
9693
9950
  ]);
9951
+ useEffect(() => {
9952
+ if (!autosaveEnabled || !isDirty || isLoading) return;
9953
+ const handler = setTimeout(() => {
9954
+ form.handleSubmit(async (data) => {
9955
+ await persistFormData(data, autosave.onSave, { reportAutosaveStatus: true }).catch(() => {});
9956
+ }, () => emitAutosaveState("dirty"))();
9957
+ }, autosave?.delayMs ?? 1500);
9958
+ return () => clearTimeout(handler);
9959
+ }, [
9960
+ autosave,
9961
+ autosaveEnabled,
9962
+ emitAutosaveState,
9963
+ form,
9964
+ isDirty,
9965
+ isLoading,
9966
+ persistFormData,
9967
+ watchedValues
9968
+ ]);
9694
9969
  useEffect(() => {
9695
9970
  if (onDataChange) {
9696
9971
  const handler = setTimeout(() => {
@@ -10036,10 +10311,17 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
10036
10311
  })
10037
10312
  })] });
10038
10313
  }
10039
- function FormEngine(props) {
10040
- if (React.useContext(NestedEditorContext)) return /* @__PURE__ */ jsx(FormEngineInner, { ...props });
10041
- return /* @__PURE__ */ jsx(NestedEditorProvider, { children: /* @__PURE__ */ jsx(FormEngineInner, { ...props }) });
10042
- }
10314
+ var ForwardedFormEngineInner = React.forwardRef(FormEngineInner);
10315
+ var FormEngine = React.forwardRef(function FormEngine(props, ref) {
10316
+ if (React.useContext(NestedEditorContext)) return /* @__PURE__ */ jsx(ForwardedFormEngineInner, {
10317
+ ...props,
10318
+ ref
10319
+ });
10320
+ return /* @__PURE__ */ jsx(NestedEditorProvider, { children: /* @__PURE__ */ jsx(ForwardedFormEngineInner, {
10321
+ ...props,
10322
+ ref
10323
+ }) });
10324
+ });
10043
10325
  //#endregion
10044
10326
  //#region src/components/forms/fields/relationship-picker.tsx
10045
10327
  var PAGE_SIZE = 50;
@@ -10913,69 +11195,564 @@ function SpreadsheetEditor({ slug, schema, data, onSave, isSaving }) {
10913
11195
  });
10914
11196
  }
10915
11197
  //#endregion
10916
- //#region src/pages/collections/list-page.tsx
10917
- function SortableColumnItem({ id, label, visible, onToggleVisible }) {
10918
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
10919
- return /* @__PURE__ */ jsxs("div", {
10920
- ref: setNodeRef,
10921
- style: {
10922
- transform: CSS.Transform.toString(transform),
10923
- transition,
10924
- zIndex: isDragging ? 10 : 1
10925
- },
10926
- className: cn("dy-flex dy-items-center dy-gap-2.5 dy-p-2 dy-bg-background dy-border dy-border-border/60 dy-rounded-lg dy-shadow-sm dy-transition-all", isDragging && "dy-opacity-50 dy-border-primary"),
10927
- children: [
10928
- /* @__PURE__ */ jsx("div", {
10929
- ...attributes,
10930
- ...listeners,
10931
- className: "dy-cursor-grab active:dy-cursor-grabbing dy-text-muted-foreground/60 hover:dy-text-foreground dy-p-0.5",
10932
- children: /* @__PURE__ */ jsx(GripVertical, { className: "dy-h-3.5 dy-w-3.5" })
10933
- }),
10934
- /* @__PURE__ */ jsx(Checkbox, {
10935
- checked: visible,
10936
- onCheckedChange: (val) => onToggleVisible(!!val)
10937
- }),
10938
- /* @__PURE__ */ jsx("div", {
10939
- className: "dy-flex-1 dy-text-xs dy-font-medium dy-text-foreground",
10940
- children: label
10941
- })
10942
- ]
10943
- });
11198
+ //#region src/lib/workflow-ui.ts
11199
+ function resolveWorkflowState(workflowConfig, workflowMeta) {
11200
+ if (!workflowConfig || !workflowMeta?.state) return null;
11201
+ return workflowConfig.states.find((state) => state.name === workflowMeta.state) ?? null;
10944
11202
  }
10945
- function resolvePublishedStateName(schema, item) {
11203
+ function resolveWorkflowStateFromDocument(workflowConfig, item) {
11204
+ return resolveWorkflowState(workflowConfig, item._workflow ?? null);
11205
+ }
11206
+ function resolvePublishingStatus(schema, item) {
10946
11207
  const hasPublishingState = !!(schema?.workflow || schema?.drafts);
10947
- const workflowConfig = schema?.workflow;
10948
- const workflowMeta = item?._workflow;
10949
- const workflowState = workflowConfig && workflowMeta ? workflowConfig.states?.find((state) => state.name === workflowMeta.state) : null;
10950
- if (workflowState) return workflowState.published ? "Published" : "Draft";
11208
+ const workflowState = resolveWorkflowStateFromDocument(schema?.workflow ?? null, item);
11209
+ const workflowMeta = item._workflow;
11210
+ if (workflowState) {
11211
+ const rawWorkflowLabel = (workflowState.label || workflowState.name || "").trim();
11212
+ const workflowStateLabel = rawWorkflowLabel && !["published", "draft"].includes(rawWorkflowLabel.toLowerCase()) ? rawWorkflowLabel : null;
11213
+ if (workflowState.published) return {
11214
+ label: "Published",
11215
+ color: "success",
11216
+ workflowStateLabel
11217
+ };
11218
+ if (workflowMeta?.publishedRevision) return {
11219
+ label: "Changed",
11220
+ color: "info",
11221
+ workflowStateLabel
11222
+ };
11223
+ return {
11224
+ label: "Draft",
11225
+ color: "warning",
11226
+ workflowStateLabel
11227
+ };
11228
+ }
10951
11229
  if (!hasPublishingState) return null;
10952
11230
  const plainStatus = item.status;
10953
- if (plainStatus === "published") return "Published";
10954
- if (plainStatus === "draft") return "Draft";
11231
+ if (plainStatus === "published") return {
11232
+ label: "Published",
11233
+ color: "success",
11234
+ workflowStateLabel: null
11235
+ };
11236
+ if (plainStatus === "draft") return {
11237
+ label: "Draft",
11238
+ color: "warning",
11239
+ workflowStateLabel: null
11240
+ };
10955
11241
  return null;
10956
11242
  }
10957
- function resolveWorkflowState(schema, item) {
10958
- const workflowConfig = schema?.workflow;
10959
- const workflowMeta = item?._workflow;
10960
- return workflowConfig && workflowMeta ? workflowConfig.states?.find((state) => state.name === workflowMeta.state) ?? null : null;
11243
+ function getAvailableWorkflowTransitions(workflowConfig, workflowMeta) {
11244
+ if (!workflowConfig || !workflowMeta) return [];
11245
+ const available = new Set(workflowMeta.availableTransitions ?? []);
11246
+ return workflowConfig.transitions.filter((transition) => available.has(transition.name));
10961
11247
  }
10962
- function CollectionListPage({ slug }) {
10963
- const { client, components, user } = useDyrected();
11248
+ function getPrimaryWorkflowTransition(transitions) {
11249
+ if (transitions.length === 0) return null;
11250
+ if (transitions.length === 1) return transitions[0] ?? null;
11251
+ return transitions.find((transition) => !transition.unpublish) ?? transitions[0] ?? null;
11252
+ }
11253
+ function groupWorkflowTransitions(transitions) {
11254
+ return transitions.reduce((groups, transition) => {
11255
+ if (transition.unpublish) groups.unpublish.push(transition);
11256
+ else groups.normal.push(transition);
11257
+ return groups;
11258
+ }, {
11259
+ normal: [],
11260
+ unpublish: []
11261
+ });
11262
+ }
11263
+ function getCommonWorkflowTransitions(workflowConfig, docs) {
11264
+ if (!workflowConfig || docs.length === 0) return [];
11265
+ const availableSets = docs.map((doc) => doc._workflow?.availableTransitions).filter((value) => Array.isArray(value) && value.length > 0).map((value) => new Set(value));
11266
+ if (availableSets.length !== docs.length) return [];
11267
+ return workflowConfig.transitions.filter((transition) => availableSets.every((set) => set.has(transition.name)));
11268
+ }
11269
+ //#endregion
11270
+ //#region src/components/workflow/workflow-transition-controls.tsx
11271
+ function formatSingleTransitionSuccessMessage(transitionLabel, documentLabel) {
11272
+ return documentLabel?.trim() ? `${documentLabel}: ${transitionLabel}` : `${transitionLabel} completed`;
11273
+ }
11274
+ function StateIcon$1({ color }) {
11275
+ if (color === "success") return /* @__PURE__ */ jsx(CheckCircle, { className: "dy-h-3.5 dy-w-3.5" });
11276
+ if (color === "warning") return /* @__PURE__ */ jsx(Clock, { className: "dy-h-3.5 dy-w-3.5" });
11277
+ if (color === "danger") return /* @__PURE__ */ jsx(XCircle, { className: "dy-h-3.5 dy-w-3.5" });
11278
+ if (color === "info") return /* @__PURE__ */ jsx(AlertCircle, { className: "dy-h-3.5 dy-w-3.5" });
11279
+ return /* @__PURE__ */ jsx(Clock, { className: "dy-h-3.5 dy-w-3.5" });
11280
+ }
11281
+ function WorkflowCommentDialog({ transition, pending, onOpenChange, onConfirm }) {
11282
+ const [comment, setComment] = React$1.useState("");
11283
+ React$1.useEffect(() => {
11284
+ if (!transition) setComment("");
11285
+ }, [transition]);
11286
+ return /* @__PURE__ */ jsx(Dialog, {
11287
+ open: !!transition,
11288
+ onOpenChange,
11289
+ children: /* @__PURE__ */ jsxs(DialogContent, {
11290
+ className: "sm:dy-max-w-md",
11291
+ children: [
11292
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: transition?.label }), /* @__PURE__ */ jsx(DialogDescription, { children: "Add the required comment before applying this workflow transition." })] }),
11293
+ /* @__PURE__ */ jsxs("div", {
11294
+ className: "dy-space-y-2",
11295
+ children: [/* @__PURE__ */ jsx("label", {
11296
+ className: "dy-text-xs dy-font-semibold dy-text-muted-foreground",
11297
+ children: "Comment"
11298
+ }), /* @__PURE__ */ jsx("textarea", {
11299
+ className: "dy-min-h-28 dy-w-full dy-rounded-lg dy-border dy-border-border/60 dy-bg-background dy-px-3 dy-py-2 dy-text-sm dy-text-foreground placeholder:dy-text-muted-foreground focus:dy-outline-none focus:dy-ring-2 focus:dy-ring-primary/30",
11300
+ value: comment,
11301
+ onChange: (event) => setComment(event.target.value),
11302
+ placeholder: `Required for "${transition?.label ?? "this transition"}"`,
11303
+ autoFocus: true
11304
+ })]
11305
+ }),
11306
+ /* @__PURE__ */ jsxs(DialogFooter, {
11307
+ className: "dy-flex dy-justify-end dy-gap-2",
11308
+ children: [/* @__PURE__ */ jsx(Button, {
11309
+ variant: "outline",
11310
+ onClick: () => onOpenChange(false),
11311
+ disabled: pending,
11312
+ children: "Cancel"
11313
+ }), /* @__PURE__ */ jsx(Button, {
11314
+ onClick: () => onConfirm(comment.trim()),
11315
+ disabled: pending || !comment.trim(),
11316
+ children: pending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-4 dy-w-4 dy-animate-spin" }) : transition?.label
11317
+ })]
11318
+ })
11319
+ ]
11320
+ })
11321
+ });
11322
+ }
11323
+ function WorkflowTransitionActionItems({ transitions, workflowConfig, onSelect, disabled }) {
11324
+ return transitions.map((transition) => {
11325
+ return /* @__PURE__ */ jsxs(DropdownMenuItem, {
11326
+ onClick: () => onSelect(transition),
11327
+ disabled,
11328
+ className: "dy-flex dy-items-center dy-gap-2",
11329
+ children: [
11330
+ /* @__PURE__ */ jsx(StateIcon$1, { color: workflowConfig.states.find((state) => state.name === transition.to)?.color }),
11331
+ /* @__PURE__ */ jsx("span", {
11332
+ className: "dy-flex-1",
11333
+ children: transition.label
11334
+ }),
11335
+ transition.requireComment && /* @__PURE__ */ jsx(MessageSquarePlus, { className: "dy-h-3.5 dy-w-3.5 dy-text-muted-foreground" })
11336
+ ]
11337
+ }, transition.name);
11338
+ });
11339
+ }
11340
+ function useWorkflowTransitionExecutor({ collection, documentIds, documentLabels, expectedRevisions, invalidateQueryKeys, onComplete, prepareTransition }) {
11341
+ const { client } = useDyrected();
10964
11342
  const queryClient = useQueryClient();
10965
- const [page, setPage] = React$1.useState(1);
10966
- const [rowSelection, setRowSelection] = React$1.useState({});
10967
- const [isImportOpen, setIsImportOpen] = React$1.useState(false);
10968
- const [searchParams, setSearchParams] = useSearchParams();
10969
- const whereParam = searchParams.get("where");
10970
- const rules = React$1.useMemo(() => {
10971
- if (!whereParam) return [];
10972
- try {
10973
- return whereToRules(JSON.parse(whereParam));
10974
- } catch {
10975
- return [];
11343
+ const wfClient = client;
11344
+ const [commentTransition, setCommentTransition] = React$1.useState(null);
11345
+ const [isPreparing, setIsPreparing] = React$1.useState(false);
11346
+ const getDefaultContext = React$1.useCallback(() => ({
11347
+ documentIds,
11348
+ documentLabels,
11349
+ expectedRevisions,
11350
+ invalidateQueryKeys
11351
+ }), [
11352
+ documentIds,
11353
+ documentLabels,
11354
+ expectedRevisions,
11355
+ invalidateQueryKeys
11356
+ ]);
11357
+ const mutation = useMutation({
11358
+ mutationFn: async ({ transition, comment, context }) => {
11359
+ let successCount = 0;
11360
+ let failureCount = 0;
11361
+ let firstError;
11362
+ for (const id of context.documentIds) try {
11363
+ await wfClient.transition(collection, id, transition.name, {
11364
+ expectedRevision: context.expectedRevisions?.[id],
11365
+ comment
11366
+ });
11367
+ successCount += 1;
11368
+ } catch (error) {
11369
+ failureCount += 1;
11370
+ if (!firstError) firstError = error instanceof Error ? error : new Error(String(error));
11371
+ }
11372
+ return {
11373
+ transition,
11374
+ successCount,
11375
+ failureCount,
11376
+ firstError,
11377
+ context
11378
+ };
11379
+ },
11380
+ onSuccess: async (result) => {
11381
+ setCommentTransition(null);
11382
+ const queryKeys = [...invalidateQueryKeys ?? [], ...result.context.invalidateQueryKeys ?? []];
11383
+ if (queryKeys.length > 0) await Promise.all(queryKeys.map((queryKey) => queryClient.invalidateQueries({ queryKey: [...queryKey] })));
11384
+ if (result.failureCount === 0) if (result.context.documentIds.length === 1) {
11385
+ const singleDocumentId = result.context.documentIds[0] ?? "";
11386
+ toast.success(formatSingleTransitionSuccessMessage(result.transition.label, result.context.documentLabels?.[singleDocumentId]));
11387
+ } else toast.success(`Applied "${result.transition.label}" to ${result.successCount} ${result.successCount === 1 ? "entry" : "entries"}`);
11388
+ else if (result.successCount === 0) toast.error("Transition failed", { description: result.firstError?.message ?? "The workflow transition could not be completed." });
11389
+ else toast(`Applied "${result.transition.label}" to ${result.successCount} entries`, { description: `${result.failureCount} failed. ${result.firstError?.message ?? ""}`.trim() });
11390
+ onComplete?.(result);
11391
+ },
11392
+ onError: (error) => {
11393
+ setCommentTransition(null);
11394
+ toast.error("Transition failed", { description: error.message });
10976
11395
  }
10977
- }, [whereParam]);
10978
- const handleRulesChange = React$1.useCallback((newRules) => {
11396
+ });
11397
+ return {
11398
+ requestTransition: React$1.useCallback(async (transition) => {
11399
+ try {
11400
+ setIsPreparing(true);
11401
+ const context = await (prepareTransition?.(transition) ?? Promise.resolve(getDefaultContext()));
11402
+ if (!context || context.documentIds.length === 0) return;
11403
+ if (transition.requireComment) {
11404
+ setCommentTransition({
11405
+ transition,
11406
+ context
11407
+ });
11408
+ return;
11409
+ }
11410
+ mutation.mutate({
11411
+ transition,
11412
+ context
11413
+ });
11414
+ } catch (error) {
11415
+ const message = error instanceof Error ? error.message : "The workflow action could not be prepared.";
11416
+ toast.error("Could not continue", { description: message });
11417
+ } finally {
11418
+ setIsPreparing(false);
11419
+ }
11420
+ }, [
11421
+ getDefaultContext,
11422
+ mutation,
11423
+ prepareTransition
11424
+ ]),
11425
+ commentTransition,
11426
+ setCommentTransition,
11427
+ confirmComment: React$1.useCallback((comment) => {
11428
+ if (!commentTransition) return;
11429
+ mutation.mutate({
11430
+ transition: commentTransition.transition,
11431
+ comment,
11432
+ context: commentTransition.context
11433
+ });
11434
+ }, [commentTransition, mutation]),
11435
+ mutation,
11436
+ isPreparing
11437
+ };
11438
+ }
11439
+ function WorkflowTransitionSplitButton({ collection, documentId, workflowConfig, workflowMeta, invalidateQueryKeys, onComplete, onPendingChange, onSaveDraft, saveDraftPending, documentLabel, className, prepareTransition }) {
11440
+ const transitions = React$1.useMemo(() => getAvailableWorkflowTransitions(workflowConfig, workflowMeta), [workflowConfig, workflowMeta]);
11441
+ const groupedTransitions = React$1.useMemo(() => groupWorkflowTransitions(transitions), [transitions]);
11442
+ const primaryTransition = React$1.useMemo(() => getPrimaryWorkflowTransition(transitions), [transitions]);
11443
+ const { requestTransition, commentTransition, setCommentTransition, confirmComment, mutation, isPreparing } = useWorkflowTransitionExecutor({
11444
+ collection,
11445
+ documentIds: [documentId],
11446
+ documentLabels: { [documentId]: documentLabel },
11447
+ expectedRevisions: { [documentId]: workflowMeta.revision },
11448
+ invalidateQueryKeys,
11449
+ onComplete,
11450
+ prepareTransition
11451
+ });
11452
+ const activeTransitionName = mutation.variables?.transition?.name;
11453
+ const isPrimaryLoading = mutation.isPending && activeTransitionName === primaryTransition?.name;
11454
+ const hasTransitions = transitions.length > 0;
11455
+ const hasMenuItems = hasTransitions || !!onSaveDraft;
11456
+ const isBusy = mutation.isPending || isPreparing || !!saveDraftPending;
11457
+ React$1.useEffect(() => {
11458
+ onPendingChange?.(mutation.isPending || isPreparing);
11459
+ }, [
11460
+ isPreparing,
11461
+ mutation.isPending,
11462
+ onPendingChange
11463
+ ]);
11464
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
11465
+ className: cn("dy-inline-flex dy-items-center", className),
11466
+ children: [/* @__PURE__ */ jsx("div", { className: "dy-mx-1 dy-h-6 dy-w-px dy-bg-border/60" }), /* @__PURE__ */ jsxs("div", {
11467
+ className: "dy-inline-flex dy-overflow-hidden dy-rounded-lg dy-border dy-border-primary/20 dy-shadow-sm",
11468
+ children: [/* @__PURE__ */ jsx(Button, {
11469
+ size: "sm",
11470
+ className: "dy-h-9 dy-rounded-none dy-border-0 dy-px-4 dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90",
11471
+ disabled: !primaryTransition || isBusy,
11472
+ onClick: () => {
11473
+ if (primaryTransition) requestTransition(primaryTransition);
11474
+ },
11475
+ title: primaryTransition?.label ?? "No workflow actions available",
11476
+ children: isPrimaryLoading || isPreparing ? /* @__PURE__ */ jsxs("span", {
11477
+ className: "dy-flex dy-items-center dy-gap-2",
11478
+ children: [/* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }), isPreparing ? "Saving…" : "Applying…"]
11479
+ }) : primaryTransition?.label ?? "No actions"
11480
+ }), /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
11481
+ asChild: true,
11482
+ children: /* @__PURE__ */ jsx(Button, {
11483
+ size: "sm",
11484
+ variant: "ghost",
11485
+ className: "dy-h-9 dy-rounded-none dy-border-0 dy-px-2 dy-text-primary-foreground hover:dy-bg-primary/90 hover:dy-text-primary-foreground",
11486
+ disabled: !hasMenuItems || isBusy,
11487
+ "aria-label": "More workflow actions",
11488
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "dy-h-4 dy-w-4" })
11489
+ })
11490
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
11491
+ align: "end",
11492
+ className: "dy-min-w-56",
11493
+ children: [onSaveDraft && /* @__PURE__ */ jsxs(DropdownMenuItem, {
11494
+ onClick: () => void onSaveDraft(),
11495
+ disabled: isBusy,
11496
+ className: "dy-flex dy-items-center dy-gap-2",
11497
+ children: [saveDraftPending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "dy-h-3.5 dy-w-3.5" }), /* @__PURE__ */ jsx("span", {
11498
+ className: "dy-flex-1",
11499
+ children: saveDraftPending ? "Saving draft..." : "Save draft"
11500
+ })]
11501
+ }), hasTransitions && /* @__PURE__ */ jsxs(Fragment, { children: [
11502
+ onSaveDraft && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
11503
+ /* @__PURE__ */ jsx(WorkflowTransitionActionItems, {
11504
+ transitions: groupedTransitions.normal,
11505
+ workflowConfig,
11506
+ onSelect: (transition) => {
11507
+ requestTransition(transition);
11508
+ },
11509
+ disabled: isBusy
11510
+ }),
11511
+ groupedTransitions.unpublish.length > 0 && groupedTransitions.normal.length > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
11512
+ /* @__PURE__ */ jsx(WorkflowTransitionActionItems, {
11513
+ transitions: groupedTransitions.unpublish,
11514
+ workflowConfig,
11515
+ onSelect: (transition) => {
11516
+ requestTransition(transition);
11517
+ },
11518
+ disabled: isBusy
11519
+ })
11520
+ ] })]
11521
+ })] })]
11522
+ })]
11523
+ }), /* @__PURE__ */ jsx(WorkflowCommentDialog, {
11524
+ transition: commentTransition?.transition ?? null,
11525
+ pending: mutation.isPending,
11526
+ onOpenChange: (open) => !open && setCommentTransition(null),
11527
+ onConfirm: confirmComment
11528
+ })] });
11529
+ }
11530
+ function WorkflowTransitionMenu({ collection, documentIds, workflowConfig, transitions, expectedRevisions, invalidateQueryKeys, onComplete, trigger, documentLabels, align = "end", sideOffset = 4, disabled }) {
11531
+ const { requestTransition, commentTransition, setCommentTransition, confirmComment, mutation, isPreparing } = useWorkflowTransitionExecutor({
11532
+ collection,
11533
+ documentIds,
11534
+ documentLabels,
11535
+ expectedRevisions,
11536
+ invalidateQueryKeys,
11537
+ onComplete
11538
+ });
11539
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
11540
+ asChild: true,
11541
+ disabled: disabled || transitions.length === 0,
11542
+ children: trigger
11543
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
11544
+ align,
11545
+ sideOffset,
11546
+ className: "dy-min-w-56",
11547
+ children: /* @__PURE__ */ jsx(WorkflowTransitionActionItems, {
11548
+ transitions,
11549
+ workflowConfig,
11550
+ onSelect: (transition) => {
11551
+ requestTransition(transition);
11552
+ },
11553
+ disabled: mutation.isPending || isPreparing
11554
+ })
11555
+ })] }), /* @__PURE__ */ jsx(WorkflowCommentDialog, {
11556
+ transition: commentTransition?.transition ?? null,
11557
+ pending: mutation.isPending,
11558
+ onOpenChange: (open) => !open && setCommentTransition(null),
11559
+ onConfirm: confirmComment
11560
+ })] });
11561
+ }
11562
+ function WorkflowTransitionPanelActions({ collection, documentId, workflowConfig, workflowMeta, onSaveDraft, saveDraftPending, prepareTransition }) {
11563
+ const transitions = React$1.useMemo(() => getAvailableWorkflowTransitions(workflowConfig, workflowMeta), [workflowConfig, workflowMeta]);
11564
+ const groupedTransitions = React$1.useMemo(() => groupWorkflowTransitions(transitions), [transitions]);
11565
+ const { requestTransition, commentTransition, setCommentTransition, confirmComment, mutation, isPreparing } = useWorkflowTransitionExecutor({
11566
+ collection,
11567
+ documentIds: [documentId],
11568
+ expectedRevisions: { [documentId]: workflowMeta.revision },
11569
+ invalidateQueryKeys: [
11570
+ [
11571
+ "entry",
11572
+ collection,
11573
+ documentId
11574
+ ],
11575
+ ["collection", collection],
11576
+ [
11577
+ "workflow-history",
11578
+ collection,
11579
+ documentId
11580
+ ]
11581
+ ],
11582
+ prepareTransition
11583
+ });
11584
+ const activeTransitionName = mutation.variables?.transition?.name;
11585
+ if (transitions.length === 0 && !onSaveDraft) return /* @__PURE__ */ jsx("p", {
11586
+ className: "dy-text-xs dy-text-muted-foreground/50 dy-italic",
11587
+ children: "No transitions available from this state."
11588
+ });
11589
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
11590
+ className: "dy-space-y-2",
11591
+ children: [
11592
+ onSaveDraft && /* @__PURE__ */ jsxs(Button, {
11593
+ size: "sm",
11594
+ variant: "secondary",
11595
+ className: "dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2",
11596
+ disabled: mutation.isPending || isPreparing || saveDraftPending,
11597
+ onClick: () => void onSaveDraft(),
11598
+ children: [saveDraftPending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "dy-h-3.5 dy-w-3.5" }), saveDraftPending ? "Saving draft..." : "Save draft"]
11599
+ }),
11600
+ groupedTransitions.normal.map((transition) => {
11601
+ const isLoading = mutation.isPending && activeTransitionName === transition.name;
11602
+ const targetState = workflowConfig.states.find((state) => state.name === transition.to);
11603
+ return /* @__PURE__ */ jsxs(Button, {
11604
+ size: "sm",
11605
+ variant: "default",
11606
+ className: "dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2",
11607
+ disabled: mutation.isPending || isPreparing,
11608
+ onClick: () => {
11609
+ requestTransition(transition);
11610
+ },
11611
+ children: [isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(StateIcon$1, { color: targetState?.color }), transition.label]
11612
+ }, transition.name);
11613
+ }),
11614
+ groupedTransitions.unpublish.length > 0 && groupedTransitions.normal.length > 0 && /* @__PURE__ */ jsx("div", {
11615
+ className: "dy-px-1 dy-pt-2",
11616
+ children: /* @__PURE__ */ jsx("div", { className: "dy-h-px dy-bg-border/60" })
11617
+ }),
11618
+ groupedTransitions.unpublish.map((transition) => {
11619
+ const isLoading = mutation.isPending && activeTransitionName === transition.name;
11620
+ const targetState = workflowConfig.states.find((state) => state.name === transition.to);
11621
+ return /* @__PURE__ */ jsxs(Button, {
11622
+ size: "sm",
11623
+ variant: "outline",
11624
+ className: "dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2",
11625
+ disabled: mutation.isPending || isPreparing,
11626
+ onClick: () => {
11627
+ requestTransition(transition);
11628
+ },
11629
+ children: [isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(StateIcon$1, { color: targetState?.color }), transition.label]
11630
+ }, transition.name);
11631
+ })
11632
+ ]
11633
+ }), /* @__PURE__ */ jsx(WorkflowCommentDialog, {
11634
+ transition: commentTransition?.transition ?? null,
11635
+ pending: mutation.isPending,
11636
+ onOpenChange: (open) => !open && setCommentTransition(null),
11637
+ onConfirm: confirmComment
11638
+ })] });
11639
+ }
11640
+ //#endregion
11641
+ //#region src/pages/collections/list-page.tsx
11642
+ function SortableColumnItem({ id, label, visible, onToggleVisible }) {
11643
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
11644
+ return /* @__PURE__ */ jsxs("div", {
11645
+ ref: setNodeRef,
11646
+ style: {
11647
+ transform: CSS.Transform.toString(transform),
11648
+ transition,
11649
+ zIndex: isDragging ? 10 : 1
11650
+ },
11651
+ className: cn("dy-flex dy-items-center dy-gap-2.5 dy-p-2 dy-bg-background dy-border dy-border-border/60 dy-rounded-lg dy-shadow-sm dy-transition-all", isDragging && "dy-opacity-50 dy-border-primary"),
11652
+ children: [
11653
+ /* @__PURE__ */ jsx("div", {
11654
+ ...attributes,
11655
+ ...listeners,
11656
+ className: "dy-cursor-grab active:dy-cursor-grabbing dy-text-muted-foreground/60 hover:dy-text-foreground dy-p-0.5",
11657
+ children: /* @__PURE__ */ jsx(GripVertical, { className: "dy-h-3.5 dy-w-3.5" })
11658
+ }),
11659
+ /* @__PURE__ */ jsx(Checkbox, {
11660
+ checked: visible,
11661
+ onCheckedChange: (val) => onToggleVisible(!!val)
11662
+ }),
11663
+ /* @__PURE__ */ jsx("div", {
11664
+ className: "dy-flex-1 dy-text-xs dy-font-medium dy-text-foreground",
11665
+ children: label
11666
+ })
11667
+ ]
11668
+ });
11669
+ }
11670
+ function WorkflowStatusCell({ schema, slug, item }) {
11671
+ const workflowConfig = schema.workflow ?? null;
11672
+ const workflowMeta = item._workflow ?? null;
11673
+ const publishingStatus = resolvePublishingStatus(schema, item);
11674
+ const documentLabel = resolveDocumentTitle({
11675
+ entry: item,
11676
+ collection: schema,
11677
+ collections: [schema]
11678
+ });
11679
+ if (!publishingStatus) return /* @__PURE__ */ jsx("span", {
11680
+ className: "dy-text-xs dy-text-muted-foreground",
11681
+ children: "-"
11682
+ });
11683
+ const workflowState = resolveWorkflowStateFromDocument(workflowConfig, item);
11684
+ const badgePresentation = {
11685
+ className: WORKFLOW_BADGE_COLORS[publishingStatus.color],
11686
+ style: void 0
11687
+ };
11688
+ const workflowStagePresentation = workflowState && publishingStatus.workflowStateLabel ? getWorkflowBadgePresentation(workflowState.color) : null;
11689
+ const transitions = getAvailableWorkflowTransitions(workflowConfig, workflowMeta);
11690
+ const badge = /* @__PURE__ */ jsxs("div", {
11691
+ className: "dy-inline-flex dy-items-center dy-gap-1.5",
11692
+ children: [/* @__PURE__ */ jsx(Badge, {
11693
+ variant: "outline",
11694
+ className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-shrink-0", transitions.length > 0 && "hover:dy-opacity-85", badgePresentation.className),
11695
+ style: badgePresentation.style,
11696
+ children: publishingStatus.label
11697
+ }), publishingStatus.workflowStateLabel && workflowStagePresentation && /* @__PURE__ */ jsx(Badge, {
11698
+ variant: "outline",
11699
+ className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-semibold dy-tracking-wide dy-shrink-0", transitions.length > 0 && "hover:dy-opacity-85", workflowStagePresentation.className),
11700
+ style: workflowStagePresentation.style,
11701
+ children: publishingStatus.workflowStateLabel
11702
+ })]
11703
+ });
11704
+ if (!workflowConfig || !workflowMeta || transitions.length === 0 || typeof item.id !== "string") return badge;
11705
+ return /* @__PURE__ */ jsx(WorkflowTransitionMenu, {
11706
+ collection: slug,
11707
+ documentIds: [item.id],
11708
+ documentLabels: { [item.id]: documentLabel },
11709
+ workflowConfig,
11710
+ transitions,
11711
+ expectedRevisions: { [item.id]: workflowMeta.revision },
11712
+ invalidateQueryKeys: [
11713
+ ["collection", slug],
11714
+ [
11715
+ "entry",
11716
+ slug,
11717
+ item.id
11718
+ ],
11719
+ [
11720
+ "workflow-history",
11721
+ slug,
11722
+ item.id
11723
+ ]
11724
+ ],
11725
+ trigger: /* @__PURE__ */ jsx("button", {
11726
+ type: "button",
11727
+ className: "dy-inline-flex",
11728
+ title: "Change workflow state",
11729
+ "aria-label": "Change workflow state",
11730
+ children: badge
11731
+ })
11732
+ });
11733
+ }
11734
+ function CollectionListPage({ slug }) {
11735
+ return /* @__PURE__ */ jsx(CollectionListPageContent, { slug }, slug);
11736
+ }
11737
+ function CollectionListPageContent({ slug }) {
11738
+ const { client, components, user } = useDyrected();
11739
+ const queryClient = useQueryClient();
11740
+ const [page, setPage] = React$1.useState(1);
11741
+ const [rowSelection, setRowSelection] = React$1.useState({});
11742
+ const [isImportOpen, setIsImportOpen] = React$1.useState(false);
11743
+ const [searchParams, setSearchParams] = useSearchParams();
11744
+ const whereParam = searchParams.get("where");
11745
+ const searchParam = searchParams.get("search") || "";
11746
+ const debouncedSearch = useDebouncedValue(searchParam.trim(), 250);
11747
+ const rules = React$1.useMemo(() => {
11748
+ if (!whereParam) return [];
11749
+ try {
11750
+ return whereToRules(JSON.parse(whereParam));
11751
+ } catch {
11752
+ return [];
11753
+ }
11754
+ }, [whereParam]);
11755
+ const handleRulesChange = React$1.useCallback((newRules) => {
10979
11756
  setSearchParams((prev) => {
10980
11757
  if (newRules.length === 0) prev.delete("where");
10981
11758
  else prev.set("where", JSON.stringify(rulesToWhere(newRules)));
@@ -10983,12 +11760,6 @@ function CollectionListPage({ slug }) {
10983
11760
  }, { replace: true });
10984
11761
  setPage(1);
10985
11762
  }, [setSearchParams]);
10986
- const [prevSlug, setPrevSlug] = React$1.useState(slug);
10987
- if (prevSlug !== slug) {
10988
- setPrevSlug(slug);
10989
- setPage(1);
10990
- setRowSelection({});
10991
- }
10992
11763
  const { data: schemas } = useQuery({
10993
11764
  queryKey: ["schemas"],
10994
11765
  queryFn: () => client.getSchemas(),
@@ -11139,7 +11910,8 @@ function CollectionListPage({ slug }) {
11139
11910
  "collection",
11140
11911
  slug,
11141
11912
  page,
11142
- whereParam
11913
+ whereParam,
11914
+ debouncedSearch
11143
11915
  ],
11144
11916
  queryFn: () => {
11145
11917
  const queryParams = {
@@ -11150,6 +11922,7 @@ function CollectionListPage({ slug }) {
11150
11922
  if (whereParam) try {
11151
11923
  queryParams.where = JSON.parse(whereParam);
11152
11924
  } catch {}
11925
+ if (debouncedSearch) queryParams.search = debouncedSearch;
11153
11926
  return client.collection(slug).find(queryParams).exec();
11154
11927
  },
11155
11928
  enabled: !!client
@@ -11294,6 +12067,10 @@ function CollectionListPage({ slug }) {
11294
12067
  }
11295
12068
  if (window.confirm(`Delete ${cleanIds.length} entries? This cannot be undone.`)) bulkDeleteMutation.mutate(cleanIds);
11296
12069
  }
12070
+ const workflowConfig = schema?.workflow ?? null;
12071
+ const selectedWorkflowDocs = React$1.useMemo(() => Object.keys(rowSelection).filter((id) => rowSelection[id]).map((id) => response?.docs?.find((doc) => String(doc.id) === id)).filter((doc) => !!doc && typeof doc.id === "string" && !!doc._workflow), [rowSelection, response]);
12072
+ const sharedWorkflowTransitions = React$1.useMemo(() => getCommonWorkflowTransitions(workflowConfig, selectedWorkflowDocs), [workflowConfig, selectedWorkflowDocs]);
12073
+ const selectedWorkflowRevisions = React$1.useMemo(() => Object.fromEntries(selectedWorkflowDocs.map((doc) => [doc.id, doc._workflow?.revision])), [selectedWorkflowDocs]);
11297
12074
  const columns = React$1.useMemo(() => {
11298
12075
  if (!schema) return [];
11299
12076
  const allDisplayFields = schema.fields.filter((f) => f.name && f.name !== "password" && !f.admin?.hidden && f.type !== "row" && f.type !== "join");
@@ -11451,21 +12228,10 @@ function CollectionListPage({ slug }) {
11451
12228
  header: "Status",
11452
12229
  enableHiding: false,
11453
12230
  cell: ({ row }) => {
11454
- const status = resolvePublishedStateName(schema, row.original);
11455
- if (!status) return /* @__PURE__ */ jsx("span", {
11456
- className: "dy-text-xs dy-text-muted-foreground",
11457
- children: "-"
11458
- });
11459
- const workflowState = resolveWorkflowState(schema, row.original);
11460
- const badgePresentation = workflowState ? getWorkflowBadgePresentation(workflowState.color) : {
11461
- className: status === "Published" ? WORKFLOW_BADGE_COLORS.success : WORKFLOW_BADGE_COLORS.warning,
11462
- style: void 0
11463
- };
11464
- return /* @__PURE__ */ jsx(Badge, {
11465
- variant: "outline",
11466
- className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-shrink-0", badgePresentation.className),
11467
- style: badgePresentation.style,
11468
- children: status
12231
+ return /* @__PURE__ */ jsx(WorkflowStatusCell, {
12232
+ schema,
12233
+ slug,
12234
+ item: row.original
11469
12235
  });
11470
12236
  }
11471
12237
  });
@@ -11644,7 +12410,7 @@ function CollectionListPage({ slug }) {
11644
12410
  }),
11645
12411
  /* @__PURE__ */ jsx(PageHeader, {
11646
12412
  title: schema.labels?.plural || schema.slug,
11647
- description: `Manage your ${schema.labels?.plural || schema.slug} entries and update content.`,
12413
+ description: schema.admin?.description || `Manage your ${schema.labels?.plural || schema.slug} entries and update content.`,
11648
12414
  icon: resolveAdminIcon(schema.admin?.icon, schema.auth ? Users : Database),
11649
12415
  children: /* @__PURE__ */ jsxs("div", {
11650
12416
  className: "dy-flex dy-items-center dy-gap-2 dy-w-full sm:dy-w-auto",
@@ -11826,7 +12592,16 @@ function CollectionListPage({ slug }) {
11826
12592
  }) : /* @__PURE__ */ jsx(DataTable, {
11827
12593
  columns,
11828
12594
  data: response?.docs || [],
11829
- searchKey: schema.admin?.useAsTitle || schema.fields.find((f) => !f.admin?.hidden)?.name || "id",
12595
+ searchPlaceholder: `Search ${schema.labels?.plural || schema.slug}...`,
12596
+ searchValue: searchParam,
12597
+ onSearchChange: (value) => {
12598
+ setSearchParams((prev) => {
12599
+ if (value.trim()) prev.set("search", value);
12600
+ else prev.delete("search");
12601
+ return prev;
12602
+ }, { replace: true });
12603
+ setPage(1);
12604
+ },
11830
12605
  onRowSelectionChange: setRowSelection,
11831
12606
  rowSelection,
11832
12607
  hideViewButton: true,
@@ -11870,31 +12645,63 @@ function CollectionListPage({ slug }) {
11870
12645
  }
11871
12646
  return canDelete;
11872
12647
  });
11873
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
11874
- variant: "outline",
11875
- size: "sm",
11876
- className: "dy-h-8",
11877
- onClick: () => handleExportSelected(selectedIds),
11878
- disabled: selectedIds.length === 0,
11879
- children: [
11880
- /* @__PURE__ */ jsx(FileDown, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
11881
- "Export Selected (",
11882
- selectedIds.length,
11883
- ")"
11884
- ]
11885
- }), /* @__PURE__ */ jsxs(Button, {
11886
- variant: "destructive",
11887
- size: "sm",
11888
- className: "dy-h-8",
11889
- onClick: () => handleBulkDelete(deletableIds),
11890
- disabled: bulkDeleteMutation.isPending || deletableIds.length === 0,
11891
- children: [
11892
- /* @__PURE__ */ jsx(Trash2, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
11893
- "Delete Selected (",
11894
- deletableIds.length,
11895
- ")"
11896
- ]
11897
- })] });
12648
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
12649
+ workflowConfig && selectedWorkflowDocs.length > 0 && sharedWorkflowTransitions.length > 0 && /* @__PURE__ */ jsx(WorkflowTransitionMenu, {
12650
+ collection: slug,
12651
+ documentIds: selectedWorkflowDocs.map((doc) => doc.id),
12652
+ documentLabels: Object.fromEntries(selectedWorkflowDocs.map((doc) => [doc.id, resolveDocumentTitle({
12653
+ entry: doc,
12654
+ collection: schema,
12655
+ collections: [schema]
12656
+ })])),
12657
+ workflowConfig,
12658
+ transitions: sharedWorkflowTransitions,
12659
+ expectedRevisions: selectedWorkflowRevisions,
12660
+ invalidateQueryKeys: [["collection", slug]],
12661
+ onComplete: () => setRowSelection({}),
12662
+ trigger: /* @__PURE__ */ jsxs(Button, {
12663
+ variant: "outline",
12664
+ size: "sm",
12665
+ className: "dy-h-8",
12666
+ disabled: selectedWorkflowDocs.length === 0,
12667
+ children: [
12668
+ "Change state (",
12669
+ selectedWorkflowDocs.length,
12670
+ ")"
12671
+ ]
12672
+ })
12673
+ }),
12674
+ workflowConfig && selectedWorkflowDocs.length > 0 && sharedWorkflowTransitions.length === 0 && /* @__PURE__ */ jsx("span", {
12675
+ className: "dy-text-xs dy-text-muted-foreground",
12676
+ children: "No shared workflow action for this selection."
12677
+ }),
12678
+ /* @__PURE__ */ jsxs(Button, {
12679
+ variant: "outline",
12680
+ size: "sm",
12681
+ className: "dy-h-8",
12682
+ onClick: () => handleExportSelected(selectedIds),
12683
+ disabled: selectedIds.length === 0,
12684
+ children: [
12685
+ /* @__PURE__ */ jsx(FileDown, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
12686
+ "Export Selected (",
12687
+ selectedIds.length,
12688
+ ")"
12689
+ ]
12690
+ }),
12691
+ /* @__PURE__ */ jsxs(Button, {
12692
+ variant: "destructive",
12693
+ size: "sm",
12694
+ className: "dy-h-8",
12695
+ onClick: () => handleBulkDelete(deletableIds),
12696
+ disabled: bulkDeleteMutation.isPending || deletableIds.length === 0,
12697
+ children: [
12698
+ /* @__PURE__ */ jsx(Trash2, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
12699
+ "Delete Selected (",
12700
+ deletableIds.length,
12701
+ ")"
12702
+ ]
12703
+ })
12704
+ ] });
11898
12705
  }
11899
12706
  }, slug),
11900
12707
  /* @__PURE__ */ jsx(AdminComponentSlot, {
@@ -12168,55 +12975,11 @@ function StateIcon({ color }) {
12168
12975
  if (color === "info") return /* @__PURE__ */ jsx(AlertCircle, { className: "dy-h-3.5 dy-w-3.5" });
12169
12976
  return /* @__PURE__ */ jsx(Clock, { className: "dy-h-3.5 dy-w-3.5" });
12170
12977
  }
12171
- function CommentDialog({ transitionLabel, onConfirm, onCancel, isPending }) {
12172
- const [comment, setComment] = useState("");
12173
- return /* @__PURE__ */ jsxs("div", {
12174
- className: "dy-mt-3 dy-space-y-2",
12175
- children: [
12176
- /* @__PURE__ */ jsxs("label", {
12177
- className: "dy-text-xs dy-font-medium dy-text-muted-foreground",
12178
- children: ["Comment ", /* @__PURE__ */ jsx("span", {
12179
- className: "dy-text-destructive",
12180
- children: "*"
12181
- })]
12182
- }),
12183
- /* @__PURE__ */ jsx("textarea", {
12184
- className: "dy-w-full dy-rounded-lg dy-border dy-border-border/60 dy-bg-background dy-px-3 dy-py-2 dy-text-sm dy-text-foreground placeholder:dy-text-muted-foreground focus:dy-outline-none focus:dy-ring-2 focus:dy-ring-primary/30 dy-resize-none",
12185
- rows: 3,
12186
- placeholder: `Required for "${transitionLabel}"…`,
12187
- value: comment,
12188
- onChange: (e) => setComment(e.target.value),
12189
- autoFocus: true
12190
- }),
12191
- /* @__PURE__ */ jsxs("div", {
12192
- className: "dy-flex dy-gap-2",
12193
- children: [/* @__PURE__ */ jsx(Button, {
12194
- size: "sm",
12195
- className: "dy-h-8 dy-px-3 dy-rounded-lg dy-text-xs dy-font-semibold",
12196
- disabled: !comment.trim() || isPending,
12197
- onClick: () => onConfirm(comment.trim()),
12198
- children: isPending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3 dy-w-3 dy-animate-spin" }) : transitionLabel
12199
- }), /* @__PURE__ */ jsx(Button, {
12200
- size: "sm",
12201
- variant: "ghost",
12202
- className: "dy-h-8 dy-px-3 dy-rounded-lg dy-text-xs",
12203
- onClick: onCancel,
12204
- disabled: isPending,
12205
- children: "Cancel"
12206
- })]
12207
- })
12208
- ]
12209
- });
12210
- }
12211
- function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig }) {
12978
+ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig, onCompareToLive, compareToLiveEnabled = false, compareToLiveReason = null, onSaveDraft, saveDraftPending, prepareTransition }) {
12212
12979
  const { client } = useDyrected();
12213
- const queryClient = useQueryClient();
12214
12980
  const [showHistory, setShowHistory] = useState(false);
12215
- const [pendingComment, setPendingComment] = useState(null);
12216
12981
  const currentState = workflowConfig.states.find((s) => s.name === workflowMeta.state);
12217
12982
  const colors = stateColors(currentState?.color);
12218
- const available = workflowMeta.availableTransitions ?? [];
12219
- const availableTransitions = workflowConfig.transitions.filter((t) => available.includes(t.name));
12220
12983
  const wfClient = client;
12221
12984
  const { data: historyResult, isLoading: historyLoading } = useQuery({
12222
12985
  queryKey: [
@@ -12228,34 +12991,6 @@ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig })
12228
12991
  enabled: showHistory && !!client
12229
12992
  });
12230
12993
  const history = historyResult?.docs ?? [];
12231
- const transitionMutation = useMutation({
12232
- mutationFn: ({ name, comment }) => wfClient.transition(collection, documentId, name, {
12233
- expectedRevision: workflowMeta.revision,
12234
- comment
12235
- }),
12236
- onSuccess: () => {
12237
- setPendingComment(null);
12238
- queryClient.invalidateQueries({ queryKey: [
12239
- "entry",
12240
- collection,
12241
- documentId
12242
- ] });
12243
- queryClient.invalidateQueries({ queryKey: [
12244
- "workflow-history",
12245
- collection,
12246
- documentId
12247
- ] });
12248
- toast.success("Transition applied");
12249
- },
12250
- onError: (err) => {
12251
- setPendingComment(null);
12252
- toast.error("Transition failed", { description: err.message });
12253
- }
12254
- });
12255
- function handleTransitionClick(t) {
12256
- if (t.requireComment) setPendingComment(t.name);
12257
- else transitionMutation.mutate({ name: t.name });
12258
- }
12259
12994
  return /* @__PURE__ */ jsxs("div", {
12260
12995
  className: "dy-rounded-2xl dy-border dy-border-border/50 dy-bg-muted/10 dy-p-4 dy-space-y-4",
12261
12996
  children: [
@@ -12273,32 +13008,32 @@ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig })
12273
13008
  className: "dy-flex dy-gap-4 dy-text-[11px] dy-text-muted-foreground/60",
12274
13009
  children: [/* @__PURE__ */ jsxs("span", { children: ["Revision ", workflowMeta.revision] }), workflowMeta.publishedAt && /* @__PURE__ */ jsxs("span", { children: ["Published ", new Date(workflowMeta.publishedAt).toLocaleDateString()] })]
12275
13010
  }),
12276
- availableTransitions.length > 0 && /* @__PURE__ */ jsx("div", {
12277
- className: "dy-space-y-2",
12278
- children: availableTransitions.map((t) => {
12279
- const isWaiting = pendingComment === t.name;
12280
- const isLoading = transitionMutation.isPending && transitionMutation.variables?.name === t.name;
12281
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs(Button, {
12282
- size: "sm",
12283
- variant: t.unpublish || t.name === "reject" ? "outline" : "default",
12284
- className: cn("dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2", isWaiting && "dy-ring-2 dy-ring-primary/30"),
12285
- disabled: transitionMutation.isPending,
12286
- onClick: () => handleTransitionClick(t),
12287
- children: [isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(StateIcon, { color: workflowConfig.states.find((s) => s.name === t.to)?.color }), t.label]
12288
- }), isWaiting && /* @__PURE__ */ jsx(CommentDialog, {
12289
- transitionLabel: t.label,
12290
- isPending: transitionMutation.isPending,
12291
- onConfirm: (comment) => transitionMutation.mutate({
12292
- name: t.name,
12293
- comment
12294
- }),
12295
- onCancel: () => setPendingComment(null)
12296
- })] }, t.name);
12297
- })
13011
+ /* @__PURE__ */ jsx(WorkflowTransitionPanelActions, {
13012
+ collection,
13013
+ documentId,
13014
+ workflowConfig,
13015
+ workflowMeta,
13016
+ onSaveDraft,
13017
+ saveDraftPending,
13018
+ prepareTransition
12298
13019
  }),
12299
- available.length === 0 && /* @__PURE__ */ jsx("p", {
12300
- className: "dy-text-xs dy-text-muted-foreground/50 dy-italic",
12301
- children: "No transitions available from this state."
13020
+ onCompareToLive && /* @__PURE__ */ jsx(Button, {
13021
+ type: "button",
13022
+ variant: "outline",
13023
+ className: "dy-w-full dy-justify-between dy-rounded-xl dy-border-border/60 dy-bg-background/70 dy-text-left",
13024
+ onClick: onCompareToLive,
13025
+ disabled: !compareToLiveEnabled,
13026
+ title: !compareToLiveEnabled && compareToLiveReason ? compareToLiveReason : "Compare draft to live",
13027
+ children: /* @__PURE__ */ jsxs("span", {
13028
+ className: "dy-flex dy-flex-col dy-items-start",
13029
+ children: [/* @__PURE__ */ jsx("span", {
13030
+ className: "dy-text-sm dy-font-semibold dy-text-foreground",
13031
+ children: "Compare to live"
13032
+ }), /* @__PURE__ */ jsx("span", {
13033
+ className: "dy-text-xs dy-font-normal dy-text-muted-foreground",
13034
+ children: compareToLiveEnabled ? "See exactly what will change on publish." : compareToLiveReason ?? "Publish this entry once to unlock live comparisons."
13035
+ })]
13036
+ })
12302
13037
  }),
12303
13038
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
12304
13039
  className: "dy-flex dy-items-center dy-gap-1 dy-text-[11px] dy-font-medium dy-text-muted-foreground/60 hover:dy-text-muted-foreground dy-transition-colors",
@@ -12355,38 +13090,248 @@ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig })
12355
13090
  });
12356
13091
  }
12357
13092
  //#endregion
12358
- //#region src/hooks/useLayoutPreference.ts
12359
- function useLayoutPreference({ key, defaultKeys }) {
12360
- const { client } = useDyrected();
12361
- const queryClient = useQueryClient();
12362
- const { data, isLoading } = useQuery({
12363
- queryKey: ["preferences", key],
12364
- queryFn: async () => {
12365
- if (!client) return null;
12366
- return (await client.getPreference(key)).value;
12367
- },
12368
- enabled: !!client,
12369
- staleTime: 5e3,
12370
- refetchOnWindowFocus: true
12371
- });
12372
- const reconciledLayout = useMemo(() => {
12373
- const isStringArray = defaultKeys.every((k) => typeof k === "string");
12374
- if (!data || !Array.isArray(data)) return defaultKeys;
12375
- if (isStringArray) {
12376
- const normalizedData = data.map((k) => typeof k === "string" ? k : k?.name || "").filter(Boolean);
12377
- const defaultStrings = defaultKeys;
12378
- const validKeys = normalizedData.filter((k) => defaultStrings.includes(k));
12379
- const missingKeys = defaultStrings.filter((k) => !validKeys.includes(k));
12380
- return [...validKeys, ...missingKeys];
12381
- } else {
12382
- const normalizedData = data.map((item) => {
12383
- if (typeof item === "string") return { name: item };
12384
- if (item && typeof item === "object" && "name" in item && typeof item.name === "string") return {
12385
- name: item.name,
12386
- width: item.width
12387
- };
12388
- return { name: "" };
12389
- }).filter((item) => item.name !== "");
13093
+ //#region src/components/ui/card.tsx
13094
+ var Card = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13095
+ ref,
13096
+ className: cn("dy-rounded-lg dy-border dy-bg-card dy-text-card-foreground dy-shadow-sm", className),
13097
+ ...props
13098
+ }));
13099
+ Card.displayName = "Card";
13100
+ var CardHeader = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13101
+ ref,
13102
+ className: cn("dy-flex dy-flex-col dy-space-y-1.5 dy-p-6", className),
13103
+ ...props
13104
+ }));
13105
+ CardHeader.displayName = "CardHeader";
13106
+ var CardTitle = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13107
+ ref,
13108
+ className: cn("dy-text-2xl dy-font-semibold dy-leading-none dy-tracking-tight", className),
13109
+ ...props
13110
+ }));
13111
+ CardTitle.displayName = "CardTitle";
13112
+ var CardDescription = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13113
+ ref,
13114
+ className: cn("dy-text-sm dy-text-muted-foreground", className),
13115
+ ...props
13116
+ }));
13117
+ CardDescription.displayName = "CardDescription";
13118
+ var CardContent = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13119
+ ref,
13120
+ className: cn("dy-p-6 dy-pt-0", className),
13121
+ ...props
13122
+ }));
13123
+ CardContent.displayName = "CardContent";
13124
+ var CardFooter = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13125
+ ref,
13126
+ className: cn("dy-flex dy-items-center dy-p-6 dy-pt-0", className),
13127
+ ...props
13128
+ }));
13129
+ CardFooter.displayName = "CardFooter";
13130
+ //#endregion
13131
+ //#region src/components/workflow/draft-live-compare-sheet.tsx
13132
+ function statusBadgeClass(status) {
13133
+ if (status === "Added") return "dy-bg-emerald-50 dy-text-emerald-700 dy-border-emerald-200";
13134
+ if (status === "Removed") return "dy-bg-rose-50 dy-text-rose-700 dy-border-rose-200";
13135
+ return "dy-bg-amber-50 dy-text-amber-700 dy-border-amber-200";
13136
+ }
13137
+ function DraftLiveCompareSheet({ open, onOpenChange, comparison }) {
13138
+ return /* @__PURE__ */ jsx(Sheet, {
13139
+ open,
13140
+ onOpenChange,
13141
+ children: /* @__PURE__ */ jsxs(SheetContent, {
13142
+ side: "right",
13143
+ className: "dy-w-[96vw] sm:dy-max-w-4xl lg:dy-max-w-6xl dy-border-l dy-border-border/60",
13144
+ children: [/* @__PURE__ */ jsx(SheetHeader, {
13145
+ className: "dy-border-b dy-border-border/50 dy-bg-background/95 dy-px-6 dy-py-5 dy-backdrop-blur",
13146
+ children: /* @__PURE__ */ jsxs("div", {
13147
+ className: "dy-flex dy-flex-wrap dy-items-start dy-justify-between dy-gap-4",
13148
+ children: [/* @__PURE__ */ jsxs("div", {
13149
+ className: "dy-space-y-1",
13150
+ children: [/* @__PURE__ */ jsx(SheetTitle, {
13151
+ className: "dy-text-2xl dy-font-serif dy-font-bold dy-tracking-tight",
13152
+ children: "Compare draft to live"
13153
+ }), /* @__PURE__ */ jsx(SheetDescription, { children: "These changes are in your draft and are not live yet." })]
13154
+ }), /* @__PURE__ */ jsxs("div", {
13155
+ className: "dy-flex dy-flex-wrap dy-gap-2",
13156
+ children: [
13157
+ /* @__PURE__ */ jsx(SummaryChip, { label: `${comparison.fieldChangeCount} fields changed` }),
13158
+ /* @__PURE__ */ jsx(SummaryChip, { label: `${comparison.sectionsAdded} sections added` }),
13159
+ /* @__PURE__ */ jsx(SummaryChip, { label: `${comparison.sectionsRemoved} sections removed` })
13160
+ ]
13161
+ })]
13162
+ })
13163
+ }), /* @__PURE__ */ jsx(ScrollArea, {
13164
+ className: "dy-h-[calc(100vh-132px)]",
13165
+ children: /* @__PURE__ */ jsxs("div", {
13166
+ className: "dy-space-y-8 dy-p-6",
13167
+ children: [!comparison.hasChanges && /* @__PURE__ */ jsx(Card, {
13168
+ className: "dy-rounded-2xl dy-border-border/60",
13169
+ children: /* @__PURE__ */ jsxs(CardContent, {
13170
+ className: "dy-p-8 dy-text-center",
13171
+ children: [/* @__PURE__ */ jsx("p", {
13172
+ className: "dy-text-base dy-font-semibold dy-text-foreground",
13173
+ children: "Draft matches the live site."
13174
+ }), /* @__PURE__ */ jsx("p", {
13175
+ className: "dy-mt-2 dy-text-sm dy-text-muted-foreground",
13176
+ children: "There are no unpublished changes in this document right now."
13177
+ })]
13178
+ })
13179
+ }), comparison.groups.map((group) => /* @__PURE__ */ jsxs("section", {
13180
+ className: "dy-space-y-4",
13181
+ children: [/* @__PURE__ */ jsx("div", {
13182
+ className: "dy-flex dy-items-center dy-justify-between",
13183
+ children: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
13184
+ className: "dy-text-sm dy-font-bold dy-uppercase dy-tracking-[0.18em] dy-text-muted-foreground/70",
13185
+ children: group.title
13186
+ }), /* @__PURE__ */ jsxs("p", {
13187
+ className: "dy-mt-1 dy-text-sm dy-text-muted-foreground",
13188
+ children: [
13189
+ group.cards.length,
13190
+ " ",
13191
+ group.cards.length === 1 ? "change" : "changes"
13192
+ ]
13193
+ })] })
13194
+ }), /* @__PURE__ */ jsx("div", {
13195
+ className: "dy-space-y-4",
13196
+ children: group.cards.map((card) => /* @__PURE__ */ jsx(ChangeCard, { card }, card.id))
13197
+ })]
13198
+ }, group.title))]
13199
+ })
13200
+ })]
13201
+ })
13202
+ });
13203
+ }
13204
+ function SummaryChip({ label }) {
13205
+ return /* @__PURE__ */ jsx("div", {
13206
+ className: "dy-rounded-full dy-border dy-border-border/60 dy-bg-muted/40 dy-px-3 dy-py-1.5 dy-text-xs dy-font-semibold dy-text-foreground/80",
13207
+ children: label
13208
+ });
13209
+ }
13210
+ function ChangeCard({ card }) {
13211
+ return /* @__PURE__ */ jsxs(Card, {
13212
+ className: "dy-overflow-hidden dy-rounded-2xl dy-border-border/60",
13213
+ children: [/* @__PURE__ */ jsx(CardHeader, {
13214
+ className: "dy-gap-3 dy-border-b dy-border-border/50 dy-bg-muted/20 dy-px-5 dy-py-4",
13215
+ children: /* @__PURE__ */ jsxs("div", {
13216
+ className: "dy-flex dy-flex-wrap dy-items-center dy-justify-between dy-gap-3",
13217
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(CardTitle, {
13218
+ className: "dy-text-base dy-font-semibold",
13219
+ children: card.label
13220
+ }), /* @__PURE__ */ jsx("p", {
13221
+ className: "dy-mt-1 dy-text-xs dy-text-muted-foreground",
13222
+ children: card.path
13223
+ })] }), /* @__PURE__ */ jsx(Badge, {
13224
+ variant: "outline",
13225
+ className: cn("dy-rounded-full dy-border dy-px-2.5 dy-py-1 dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-[0.18em]", statusBadgeClass(card.status)),
13226
+ children: card.status
13227
+ })]
13228
+ })
13229
+ }), /* @__PURE__ */ jsxs(CardContent, {
13230
+ className: "dy-grid dy-gap-4 dy-p-5 lg:dy-grid-cols-2",
13231
+ children: [/* @__PURE__ */ jsx(CompareSide, {
13232
+ title: "Live",
13233
+ text: card.liveText,
13234
+ media: card.liveMedia
13235
+ }), /* @__PURE__ */ jsx(CompareSide, {
13236
+ title: "Draft",
13237
+ text: card.draftText,
13238
+ media: card.draftMedia,
13239
+ emphasize: true
13240
+ })]
13241
+ })]
13242
+ });
13243
+ }
13244
+ function CompareSide({ title, text, media, emphasize = false }) {
13245
+ return /* @__PURE__ */ jsxs("div", {
13246
+ className: cn("dy-space-y-3 dy-rounded-2xl dy-border dy-p-4", emphasize ? "dy-border-primary/20 dy-bg-primary/5" : "dy-border-border/60 dy-bg-background"),
13247
+ children: [
13248
+ /* @__PURE__ */ jsx("div", {
13249
+ className: "dy-flex dy-items-center dy-justify-between",
13250
+ children: /* @__PURE__ */ jsx("p", {
13251
+ className: "dy-text-[11px] dy-font-bold dy-uppercase dy-tracking-[0.18em] dy-text-muted-foreground/70",
13252
+ children: title
13253
+ })
13254
+ }),
13255
+ media && /* @__PURE__ */ jsx(MediaPreview, { media }),
13256
+ /* @__PURE__ */ jsx("div", {
13257
+ className: "dy-rounded-xl dy-bg-muted/35 dy-p-3",
13258
+ children: /* @__PURE__ */ jsx("p", {
13259
+ className: "dy-whitespace-pre-wrap dy-break-words dy-text-sm dy-leading-6 dy-text-foreground/90",
13260
+ children: text
13261
+ })
13262
+ })
13263
+ ]
13264
+ });
13265
+ }
13266
+ function MediaPreview({ media }) {
13267
+ return /* @__PURE__ */ jsxs("div", {
13268
+ className: "dy-flex dy-gap-3",
13269
+ children: [/* @__PURE__ */ jsx("div", {
13270
+ className: "dy-flex dy-h-20 dy-w-20 dy-shrink-0 dy-items-center dy-justify-center dy-overflow-hidden dy-rounded-xl dy-border dy-border-border/60 dy-bg-muted/30",
13271
+ children: media.url ? /* @__PURE__ */ jsx("img", {
13272
+ src: media.url,
13273
+ alt: media.alt || media.filename || "Media preview",
13274
+ className: "dy-h-full dy-w-full dy-object-cover"
13275
+ }) : /* @__PURE__ */ jsx("div", {
13276
+ className: "dy-text-[11px] dy-font-semibold dy-text-muted-foreground",
13277
+ children: "No preview"
13278
+ })
13279
+ }), /* @__PURE__ */ jsxs("div", {
13280
+ className: "dy-min-w-0 dy-space-y-1",
13281
+ children: [
13282
+ media.filename && /* @__PURE__ */ jsx("p", {
13283
+ className: "dy-text-sm dy-font-semibold dy-text-foreground dy-break-all",
13284
+ children: media.filename
13285
+ }),
13286
+ media.alt && /* @__PURE__ */ jsx("p", {
13287
+ className: "dy-text-xs dy-text-muted-foreground",
13288
+ children: media.alt
13289
+ }),
13290
+ media.url && /* @__PURE__ */ jsx(Button, {
13291
+ type: "button",
13292
+ variant: "ghost",
13293
+ size: "sm",
13294
+ className: "dy-h-auto dy-justify-start dy-p-0 dy-text-xs dy-text-primary hover:dy-bg-transparent hover:dy-text-primary/80",
13295
+ onClick: () => window.open(media.url, "_blank", "noopener,noreferrer"),
13296
+ children: "Open asset"
13297
+ })
13298
+ ]
13299
+ })]
13300
+ });
13301
+ }
13302
+ //#endregion
13303
+ //#region src/hooks/useLayoutPreference.ts
13304
+ function useLayoutPreference({ key, defaultKeys }) {
13305
+ const { client } = useDyrected();
13306
+ const queryClient = useQueryClient();
13307
+ const { data, isLoading } = useQuery({
13308
+ queryKey: ["preferences", key],
13309
+ queryFn: async () => {
13310
+ if (!client) return null;
13311
+ return (await client.getPreference(key)).value;
13312
+ },
13313
+ enabled: !!client,
13314
+ staleTime: 5e3,
13315
+ refetchOnWindowFocus: true
13316
+ });
13317
+ const reconciledLayout = useMemo(() => {
13318
+ const isStringArray = defaultKeys.every((k) => typeof k === "string");
13319
+ if (!data || !Array.isArray(data)) return defaultKeys;
13320
+ if (isStringArray) {
13321
+ const normalizedData = data.map((k) => typeof k === "string" ? k : k?.name || "").filter(Boolean);
13322
+ const defaultStrings = defaultKeys;
13323
+ const validKeys = normalizedData.filter((k) => defaultStrings.includes(k));
13324
+ const missingKeys = defaultStrings.filter((k) => !validKeys.includes(k));
13325
+ return [...validKeys, ...missingKeys];
13326
+ } else {
13327
+ const normalizedData = data.map((item) => {
13328
+ if (typeof item === "string") return { name: item };
13329
+ if (item && typeof item === "object" && "name" in item && typeof item.name === "string") return {
13330
+ name: item.name,
13331
+ width: item.width
13332
+ };
13333
+ return { name: "" };
13334
+ }).filter((item) => item.name !== "");
12390
13335
  const defaultItems = defaultKeys;
12391
13336
  const defaultNames = defaultItems.map((item) => item.name);
12392
13337
  const defaultMap = new Map(defaultItems.map((item) => [item.name, item]));
@@ -12449,6 +13394,292 @@ function useLayoutPreference({ key, defaultKeys }) {
12449
13394
  };
12450
13395
  }
12451
13396
  //#endregion
13397
+ //#region src/lib/draft-live-compare.ts
13398
+ var GROUP_ORDER = [
13399
+ "Text changes",
13400
+ "Media changes",
13401
+ "Layout / sections changed",
13402
+ "Settings changed"
13403
+ ];
13404
+ var SYSTEM_KEYS = /* @__PURE__ */ new Set([
13405
+ "id",
13406
+ "_workflow",
13407
+ "__workflow",
13408
+ "__published",
13409
+ "createdAt",
13410
+ "createdBy",
13411
+ "updatedAt",
13412
+ "updatedBy",
13413
+ "password",
13414
+ "oldPassword",
13415
+ "newPassword",
13416
+ "confirmPassword"
13417
+ ]);
13418
+ function buildDraftLiveComparison(args) {
13419
+ const draft = args.draft ?? {};
13420
+ const live = args.live ?? {};
13421
+ const cards = [];
13422
+ const counters = {
13423
+ sectionsAdded: 0,
13424
+ sectionsRemoved: 0
13425
+ };
13426
+ compareFields({
13427
+ fields: args.fields,
13428
+ draft,
13429
+ live,
13430
+ pathPrefix: "",
13431
+ cards,
13432
+ counters
13433
+ });
13434
+ const groups = GROUP_ORDER.map((title) => ({
13435
+ title,
13436
+ cards: cards.filter((card) => card.group === title)
13437
+ })).filter((group) => group.cards.length > 0);
13438
+ return {
13439
+ hasPublishedVersion: !isStructurallyEmpty(live),
13440
+ hasChanges: cards.length > 0,
13441
+ fieldChangeCount: cards.length,
13442
+ sectionsAdded: counters.sectionsAdded,
13443
+ sectionsRemoved: counters.sectionsRemoved,
13444
+ groups
13445
+ };
13446
+ }
13447
+ function compareFields(args) {
13448
+ const draftRecord = args.draft ?? {};
13449
+ const liveRecord = args.live ?? {};
13450
+ for (const field of args.fields) {
13451
+ if (!field?.name || SYSTEM_KEYS.has(field.name) || field.admin?.hidden) continue;
13452
+ const path = args.pathPrefix ? `${args.pathPrefix}.${field.name}` : field.name;
13453
+ const draftValue = draftRecord[field.name];
13454
+ const liveValue = liveRecord[field.name];
13455
+ if (field.type === "object" && Array.isArray(field.fields)) {
13456
+ compareFields({
13457
+ fields: field.fields,
13458
+ draft: asRecord(draftValue),
13459
+ live: asRecord(liveValue),
13460
+ pathPrefix: path,
13461
+ cards: args.cards,
13462
+ counters: args.counters
13463
+ });
13464
+ continue;
13465
+ }
13466
+ if (field.type === "array" || field.type === "blocks") {
13467
+ compareArrayField({
13468
+ field,
13469
+ path,
13470
+ draftValue,
13471
+ liveValue,
13472
+ cards: args.cards,
13473
+ counters: args.counters
13474
+ });
13475
+ continue;
13476
+ }
13477
+ if (deepEqual(draftValue, liveValue)) continue;
13478
+ args.cards.push(buildCard({
13479
+ field,
13480
+ label: field.label || humanizeKey(field.name),
13481
+ path,
13482
+ draftValue,
13483
+ liveValue
13484
+ }));
13485
+ }
13486
+ }
13487
+ function compareArrayField(args) {
13488
+ const draftItems = Array.isArray(args.draftValue) ? args.draftValue : [];
13489
+ const liveItems = Array.isArray(args.liveValue) ? args.liveValue : [];
13490
+ const max = Math.max(draftItems.length, liveItems.length);
13491
+ for (let index = 0; index < max; index += 1) {
13492
+ const draftItem = draftItems[index];
13493
+ const liveItem = liveItems[index];
13494
+ if (deepEqual(draftItem, liveItem)) continue;
13495
+ const label = describeArrayItem(args.field, draftItem, liveItem, index);
13496
+ const status = resolveStatus(liveItem, draftItem);
13497
+ if (status === "Added") args.counters.sectionsAdded += 1;
13498
+ if (status === "Removed") args.counters.sectionsRemoved += 1;
13499
+ args.cards.push({
13500
+ id: `${args.path}.${index}`,
13501
+ group: "Layout / sections changed",
13502
+ label,
13503
+ status,
13504
+ path: `${args.path}.${index}`,
13505
+ kind: "array",
13506
+ liveValue: liveItem,
13507
+ draftValue: draftItem,
13508
+ liveText: summarizeArrayItem(liveItem),
13509
+ draftText: summarizeArrayItem(draftItem)
13510
+ });
13511
+ }
13512
+ }
13513
+ function buildCard(args) {
13514
+ const kind = resolveKind(args.field, args.draftValue, args.liveValue);
13515
+ const status = resolveStatus(args.liveValue, args.draftValue);
13516
+ const liveMedia = kind === "media" ? extractMediaPreview(args.liveValue) : null;
13517
+ const draftMedia = kind === "media" ? extractMediaPreview(args.draftValue) : null;
13518
+ return {
13519
+ id: args.path,
13520
+ group: resolveGroup(args.field, kind),
13521
+ label: args.label,
13522
+ status,
13523
+ path: args.path,
13524
+ kind,
13525
+ liveValue: args.liveValue,
13526
+ draftValue: args.draftValue,
13527
+ liveText: formatValueForPreview(args.liveValue, kind),
13528
+ draftText: formatValueForPreview(args.draftValue, kind),
13529
+ liveMedia,
13530
+ draftMedia
13531
+ };
13532
+ }
13533
+ function resolveKind(field, draftValue, liveValue) {
13534
+ if (field.type === "richText" || field.type === "json") return "richText";
13535
+ if (field.type === "image" || isMediaLike(draftValue) || isMediaLike(liveValue)) return "media";
13536
+ if (field.type === "relationship" && (field.hasMany || Array.isArray(draftValue) || Array.isArray(liveValue))) return "array";
13537
+ if (isLikelySettingField(field)) return "setting";
13538
+ return "text";
13539
+ }
13540
+ function resolveGroup(field, kind) {
13541
+ if (kind === "media") return "Media changes";
13542
+ if (kind === "array") return "Layout / sections changed";
13543
+ if (kind === "setting" || isLikelySettingField(field)) return "Settings changed";
13544
+ return "Text changes";
13545
+ }
13546
+ function resolveStatus(liveValue, draftValue) {
13547
+ if (isStructurallyEmpty(liveValue) && !isStructurallyEmpty(draftValue)) return "Added";
13548
+ if (!isStructurallyEmpty(liveValue) && isStructurallyEmpty(draftValue)) return "Removed";
13549
+ return "Changed";
13550
+ }
13551
+ function describeArrayItem(field, draftItem, liveItem, index) {
13552
+ const sample = asRecord(draftItem) || asRecord(liveItem);
13553
+ if (field.type === "blocks") {
13554
+ const blockType = typeof sample?.blockType === "string" ? sample.blockType : null;
13555
+ const blockLabel = blockType ? field.blocks?.find((block) => block.slug === blockType)?.labels?.singular ?? humanizeKey(blockType) : `Section ${index + 1}`;
13556
+ return blockLabel.includes("section") || blockLabel.includes("Section") ? blockLabel : `${blockLabel} section`;
13557
+ }
13558
+ const title = extractTitle(sample);
13559
+ if (title) return title;
13560
+ return `${field.label?.replace(/s$/, "") || "Section"} ${index + 1}`;
13561
+ }
13562
+ function summarizeArrayItem(value) {
13563
+ if (isStructurallyEmpty(value)) return "No content";
13564
+ const record = asRecord(value);
13565
+ if (!record) return formatValueForPreview(value, "text");
13566
+ const summaryParts = [];
13567
+ const title = extractTitle(record);
13568
+ if (title) summaryParts.push(title);
13569
+ const textSnippet = extractTextContent(record);
13570
+ if (textSnippet && textSnippet !== title) summaryParts.push(textSnippet);
13571
+ if (summaryParts.length === 0) {
13572
+ const keys = Object.keys(record).filter((key) => !SYSTEM_KEYS.has(key));
13573
+ if (keys.length > 0) summaryParts.push(`${keys.length} fields`);
13574
+ }
13575
+ return summaryParts.join(" • ") || "No content";
13576
+ }
13577
+ function formatValueForPreview(value, kind) {
13578
+ if (isStructurallyEmpty(value)) return "Empty";
13579
+ if (kind === "media") {
13580
+ const media = extractMediaPreview(value);
13581
+ return media?.filename || media?.url || media?.alt || "Media updated";
13582
+ }
13583
+ if (kind === "richText") return extractTextContent(value) || "Rich text updated";
13584
+ if (Array.isArray(value)) return value.map((item) => summarizeArrayItem(item)).filter(Boolean).slice(0, 3).join("\n") || `${value.length} items`;
13585
+ if (typeof value === "object") return extractTextContent(value) || JSON.stringify(value);
13586
+ return String(value);
13587
+ }
13588
+ function extractMediaPreview(value) {
13589
+ const record = asRecord(value);
13590
+ if (!record) return null;
13591
+ const filename = readString(record.filename) || readString(record.name);
13592
+ const url = readString(record.url) || readString(record.src);
13593
+ const alt = readString(record.alt) || readString(record.caption);
13594
+ if (!filename && !url && !alt) return null;
13595
+ return {
13596
+ filename,
13597
+ url,
13598
+ alt
13599
+ };
13600
+ }
13601
+ function isMediaLike(value) {
13602
+ return !!extractMediaPreview(value);
13603
+ }
13604
+ function isLikelySettingField(field) {
13605
+ const source = `${field.name ?? ""} ${field.label ?? ""}`.toLowerCase();
13606
+ return [
13607
+ "status",
13608
+ "slug",
13609
+ "seo",
13610
+ "settings",
13611
+ "theme",
13612
+ "variant",
13613
+ "url",
13614
+ "path",
13615
+ "template"
13616
+ ].some((token) => source.includes(token));
13617
+ }
13618
+ function extractTitle(value) {
13619
+ if (!value) return "";
13620
+ for (const key of [
13621
+ "title",
13622
+ "heading",
13623
+ "headline",
13624
+ "label",
13625
+ "name",
13626
+ "question",
13627
+ "slug"
13628
+ ]) {
13629
+ const next = readString(value[key]);
13630
+ if (next) return next;
13631
+ }
13632
+ return "";
13633
+ }
13634
+ function extractTextContent(value) {
13635
+ const parts = [];
13636
+ collectText(value, parts);
13637
+ return truncateText(parts.join(" ").replace(/\s+/g, " ").trim(), 220);
13638
+ }
13639
+ function collectText(value, parts) {
13640
+ if (value === null || value === void 0) return;
13641
+ if (typeof value === "string") {
13642
+ const cleaned = value.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").trim();
13643
+ if (cleaned) parts.push(cleaned);
13644
+ return;
13645
+ }
13646
+ if (typeof value === "number" || typeof value === "boolean") {
13647
+ parts.push(String(value));
13648
+ return;
13649
+ }
13650
+ if (Array.isArray(value)) {
13651
+ for (const item of value) collectText(item, parts);
13652
+ return;
13653
+ }
13654
+ if (typeof value === "object") for (const [key, nested] of Object.entries(value)) {
13655
+ if (SYSTEM_KEYS.has(key) || key === "blockType") continue;
13656
+ collectText(nested, parts);
13657
+ }
13658
+ }
13659
+ function truncateText(value, max) {
13660
+ if (value.length <= max) return value;
13661
+ return `${value.slice(0, max - 1).trimEnd()}…`;
13662
+ }
13663
+ function asRecord(value) {
13664
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
13665
+ }
13666
+ function readString(value) {
13667
+ return typeof value === "string" && value.trim() ? value.trim() : null;
13668
+ }
13669
+ function deepEqual(left, right) {
13670
+ return JSON.stringify(left) === JSON.stringify(right);
13671
+ }
13672
+ function isStructurallyEmpty(value) {
13673
+ if (value === null || value === void 0) return true;
13674
+ if (typeof value === "string") return value.trim().length === 0;
13675
+ if (Array.isArray(value)) return value.length === 0;
13676
+ if (typeof value === "object") return Object.keys(value).length === 0;
13677
+ return false;
13678
+ }
13679
+ function humanizeKey(value) {
13680
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
13681
+ }
13682
+ //#endregion
12452
13683
  //#region src/pages/collections/edit-page.tsx
12453
13684
  function SortableFieldItem({ id, label, type, width, onChangeWidth }) {
12454
13685
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
@@ -12522,19 +13753,59 @@ function SortableFieldItem({ id, label, type, width, onChangeWidth }) {
12522
13753
  });
12523
13754
  }
12524
13755
  /**
12525
- * A single top-bar action: a compact square icon button with active / disabled /
12526
- * busy states. Groups into the horizontal action cluster on the right of the
12527
- * editor header (Storyblok-style top bar rather than a far-right vertical rail).
13756
+ * A single top-bar action. It stays icon-only on smaller screens and expands to
13757
+ * an icon + label button on desktop so action meaning is visible without relying
13758
+ * on tooltips.
12528
13759
  */
12529
13760
  function HeaderAction({ icon: Icon, label, onClick, active, disabled, busy, title, className }) {
12530
- return /* @__PURE__ */ jsx("button", {
13761
+ return /* @__PURE__ */ jsxs("button", {
12531
13762
  type: "button",
12532
13763
  onClick,
12533
13764
  disabled,
12534
13765
  title: title || label,
12535
13766
  "aria-label": title || label,
12536
- className: cn("dy-flex dy-h-9 dy-w-9 dy-items-center dy-justify-center dy-rounded-lg dy-transition-all", active ? "dy-bg-muted dy-text-primary" : "dy-text-muted-foreground hover:dy-bg-muted/60 hover:dy-text-foreground", disabled && "dy-opacity-50 dy-pointer-events-none", className),
12537
- children: busy ? /* @__PURE__ */ jsx("span", { className: "dy-h-4 dy-w-4 dy-animate-spin dy-rounded-full dy-border-2 dy-border-current dy-border-t-transparent" }) : /* @__PURE__ */ jsx(Icon, { className: "dy-h-4 dy-w-4" })
13767
+ className: cn("dy-inline-flex dy-h-9 dy-w-9 lg:dy-w-auto dy-items-center dy-justify-center lg:dy-justify-start lg:dy-gap-2 dy-rounded-lg lg:dy-px-3 dy-transition-all", active ? "dy-bg-muted dy-text-primary" : "dy-text-muted-foreground hover:dy-bg-muted/60 hover:dy-text-foreground", disabled && "dy-opacity-50 dy-pointer-events-none", className),
13768
+ children: [busy ? /* @__PURE__ */ jsx("span", { className: "dy-h-4 dy-w-4 dy-animate-spin dy-rounded-full dy-border-2 dy-border-current dy-border-t-transparent" }) : /* @__PURE__ */ jsx(Icon, { className: "dy-h-4 dy-w-4" }), /* @__PURE__ */ jsx("span", {
13769
+ className: "dy-hidden lg:dy-inline dy-text-xs dy-font-medium",
13770
+ children: label
13771
+ })]
13772
+ });
13773
+ }
13774
+ function DraftSaveStatusBadge({ state }) {
13775
+ const presentation = (() => {
13776
+ switch (state) {
13777
+ case "dirty": return {
13778
+ icon: Save,
13779
+ label: "Unsaved changes",
13780
+ className: "dy-text-amber-700 dy-bg-amber-50 dy-border-amber-200"
13781
+ };
13782
+ case "saving": return {
13783
+ icon: Loader2,
13784
+ label: "Saving...",
13785
+ className: "dy-text-sky-700 dy-bg-sky-50 dy-border-sky-200"
13786
+ };
13787
+ case "saved":
13788
+ case "idle": return {
13789
+ icon: Save,
13790
+ label: "Changes saved",
13791
+ className: "dy-text-emerald-700 dy-bg-emerald-50 dy-border-emerald-200"
13792
+ };
13793
+ case "conflict": return {
13794
+ icon: AlertCircle,
13795
+ label: "Refresh required",
13796
+ className: "dy-text-rose-700 dy-bg-rose-50 dy-border-rose-200"
13797
+ };
13798
+ default: return {
13799
+ icon: AlertCircle,
13800
+ label: "Save failed",
13801
+ className: "dy-text-rose-700 dy-bg-rose-50 dy-border-rose-200"
13802
+ };
13803
+ }
13804
+ })();
13805
+ const Icon = presentation.icon;
13806
+ return /* @__PURE__ */ jsxs("div", {
13807
+ className: cn("dy-inline-flex dy-items-center dy-gap-1.5 dy-rounded-full dy-border dy-px-2.5 dy-py-1 dy-text-[11px] dy-font-semibold", presentation.className),
13808
+ children: [/* @__PURE__ */ jsx(Icon, { className: cn("dy-h-3.5 dy-w-3.5", state === "saving" && "dy-animate-spin") }), /* @__PURE__ */ jsx("span", { children: presentation.label })]
12538
13809
  });
12539
13810
  }
12540
13811
  /**
@@ -12595,20 +13866,6 @@ function PreviewPaneWithNav({ previewUrl, data, mode, collectionSlug, documentId
12595
13866
  onFieldFocus: handleFieldFocus
12596
13867
  });
12597
13868
  }
12598
- function getEntryTitle(entry, schema) {
12599
- if (!entry) return "";
12600
- const titleField = schema?.admin?.useAsTitle || "title";
12601
- if (entry[titleField]) return String(entry[titleField]);
12602
- for (const f of [
12603
- "title",
12604
- "name",
12605
- "label",
12606
- "heading",
12607
- "email",
12608
- "subject"
12609
- ]) if (entry[f]) return String(entry[f]);
12610
- return String(entry.id);
12611
- }
12612
13869
  function EditEntryPage() {
12613
13870
  const { slug, id } = useParams();
12614
13871
  const [searchParams] = useSearchParams();
@@ -12620,6 +13877,11 @@ function EditEntryPage() {
12620
13877
  const [mobilePreview, setMobilePreview] = useState(false);
12621
13878
  const [isDirty, setIsDirty] = useState(false);
12622
13879
  const [previewData, setPreviewData] = useState(null);
13880
+ const [workflowAutosaveState, setWorkflowAutosaveState] = useState("idle");
13881
+ const [workflowTransitionPending, setWorkflowTransitionPending] = useState(false);
13882
+ const [compareSheetOpen, setCompareSheetOpen] = useState(false);
13883
+ const formEngineRef = useRef(null);
13884
+ const draftSavePromiseRef = useRef(null);
12623
13885
  const isEdit = !!id;
12624
13886
  const [isConfiguringView, setIsConfiguringView] = useState(false);
12625
13887
  const { data: schemas } = useQuery({
@@ -12702,16 +13964,6 @@ function EditEntryPage() {
12702
13964
  window.addEventListener("beforeunload", handleBeforeUnload);
12703
13965
  return () => window.removeEventListener("beforeunload", handleBeforeUnload);
12704
13966
  }, [isDirty]);
12705
- useEffect(() => {
12706
- const handleSave = (e) => {
12707
- if ((e.metaKey || e.ctrlKey) && e.key === "s") {
12708
- e.preventDefault();
12709
- document.getElementById("dyrected-form-submit")?.click();
12710
- }
12711
- };
12712
- window.addEventListener("keydown", handleSave);
12713
- return () => window.removeEventListener("keydown", handleSave);
12714
- }, []);
12715
13967
  const schemaSlug = schema?.slug;
12716
13968
  const schemaPreviewUrl = schema?.admin?.previewUrl;
12717
13969
  const syncShowPreview = useState(() => (next) => {
@@ -12725,18 +13977,6 @@ function EditEntryPage() {
12725
13977
  schemaPreviewUrl,
12726
13978
  syncShowPreview
12727
13979
  ]);
12728
- const hasWorkflow = !!schema?.workflow;
12729
- const syncShowWorkflow = useState(() => (next) => {
12730
- setActiveTab((prev) => next ? "workflow" : prev === "workflow" ? "edit" : prev);
12731
- })[0];
12732
- useEffect(() => {
12733
- if (!hasWorkflow || schemaPreviewUrl) return;
12734
- syncShowWorkflow(true);
12735
- }, [
12736
- hasWorkflow,
12737
- schemaPreviewUrl,
12738
- syncShowWorkflow
12739
- ]);
12740
13980
  const { data: entry, isLoading: isEntryLoading } = useQuery({
12741
13981
  queryKey: [
12742
13982
  "entry",
@@ -12761,12 +14001,10 @@ function EditEntryPage() {
12761
14001
  slug,
12762
14002
  debouncedSearchQuery
12763
14003
  ],
12764
- queryFn: async () => {
12765
- const displayField = schema?.admin?.useAsTitle || "title";
12766
- let qb = client.collection(slug).find({ limit: 50 });
12767
- if (debouncedSearchQuery) qb = qb.where({ [displayField]: { like: `%${debouncedSearchQuery}%` } });
12768
- return qb.exec();
12769
- },
14004
+ queryFn: () => client.collection(slug).find({
14005
+ limit: 50,
14006
+ search: debouncedSearchQuery || void 0
14007
+ }).exec(),
12770
14008
  enabled: !!client && !!slug && isEdit && switcherOpen
12771
14009
  });
12772
14010
  const syncPreviewData = useState(() => (next) => {
@@ -12780,7 +14018,7 @@ function EditEntryPage() {
12780
14018
  const isSelf = !!user && !!id && (user.id === id || user.sub === id);
12781
14019
  const passwordChangeMode = schema?.auth ? isSelf ? "self" : isAdminUser ? "admin" : null : null;
12782
14020
  const saveMutation = useMutation({
12783
- mutationFn: async (data) => {
14021
+ mutationFn: async ({ data }) => {
12784
14022
  const { oldPassword, newPassword, confirmPassword, ...rest } = data;
12785
14023
  const results = {};
12786
14024
  if (isEdit) {
@@ -12796,7 +14034,7 @@ function EditEntryPage() {
12796
14034
  } else results.doc = await client.collection(slug).create(rest);
12797
14035
  return results;
12798
14036
  },
12799
- onSuccess: (results) => {
14037
+ onSuccess: (results, variables) => {
12800
14038
  setIsDirty(false);
12801
14039
  queryClient.invalidateQueries({ queryKey: ["collection", slug] });
12802
14040
  if (isEdit) queryClient.invalidateQueries({ queryKey: [
@@ -12804,45 +14042,26 @@ function EditEntryPage() {
12804
14042
  slug,
12805
14043
  id
12806
14044
  ] });
12807
- toast.success(isEdit ? "Entry updated successfully" : "Entry created successfully", { description: `${schema?.labels?.singular || schema?.slug} has been saved.` });
12808
- if (results.passwordChanged) toast.success("Password changed successfully");
14045
+ if (variables.mode === "manual") toast.success(isEdit ? "Entry updated successfully" : "Entry created successfully", { description: `${schema?.labels?.singular || schema?.slug} has been saved.` });
14046
+ if (results.passwordChanged && variables.mode === "manual") toast.success("Password changed successfully");
12809
14047
  const doc = results.doc;
12810
14048
  if (!isEdit && doc?.id) navigate(`/collections/${slug}/edit/${doc.id}`, { replace: true });
12811
14049
  },
12812
- onError: (error) => {
14050
+ onError: (error, variables) => {
14051
+ if (variables.mode === "autosave") return;
12813
14052
  toast.error("Failed to save entry", { description: error.message || "An unexpected error occurred." });
12814
14053
  }
12815
14054
  });
12816
- const [sendingReset, setSendingReset] = useState(false);
12817
- const handleSendResetLink = async () => {
12818
- if (!entry?.email) return;
12819
- setSendingReset(true);
12820
- try {
12821
- await client.collection(slug).sendResetLink(entry.email);
12822
- toast.success("Reset link sent", { description: `A password reset email has been sent to ${entry.email}.` });
12823
- } catch (err) {
12824
- toast.error("Failed to send reset link", { description: err instanceof Error ? err.message : "An unexpected error occurred." });
12825
- } finally {
12826
- setSendingReset(false);
12827
- }
12828
- };
12829
- if (!schema) return /* @__PURE__ */ jsx("div", { children: "Collection not found" });
12830
- if (isEdit && isEntryLoading) return /* @__PURE__ */ jsx("div", { children: "Loading entry..." });
12831
- const workflowConfig = schema.workflow ?? null;
14055
+ const workflowConfig = schema?.workflow ?? null;
12832
14056
  const workflowMeta = isEdit && entry ? entry._workflow ?? null : null;
12833
14057
  const hasStatus = schema?.fields.some((f) => f.name === "status");
12834
- const currentStatus = entry?.status || "draft";
12835
- const workflowState = workflowConfig && workflowMeta ? workflowConfig.states?.find((s) => s.name === workflowMeta.state) : null;
12836
- const isPublished = workflowState ? !!workflowState.published : currentStatus === "published";
12837
- const statusBadgeLabel = workflowState ? workflowState.label || workflowState.name : currentStatus === "published" ? "Published" : currentStatus === "draft" ? "Draft" : String(currentStatus);
12838
- const workflowBadgePresentation = workflowState ? getWorkflowBadgePresentation(workflowState.color) : {
12839
- className: isPublished ? WORKFLOW_BADGE_COLORS.success : WORKFLOW_BADGE_COLORS.warning,
12840
- style: void 0
12841
- };
14058
+ const workflowState = resolveWorkflowState(workflowConfig, workflowMeta);
14059
+ const publishingStatus = resolvePublishingStatus(schema, entry ?? {});
14060
+ const workflowStagePresentation = workflowState && publishingStatus?.workflowStateLabel ? getWorkflowBadgePresentation(workflowState.color) : null;
12842
14061
  const showStatusBadge = workflowConfig ? !!workflowMeta : hasStatus;
12843
14062
  const siteUrl = getSiteUrl(schemas?.admin?.siteUrl);
12844
- const previewUrl = resolvePreviewUrl(schema.admin?.previewUrl, previewData || entry, siteUrl);
12845
- const readAccess = schema.access?.read;
14063
+ const previewUrl = resolvePreviewUrl(schema?.admin?.previewUrl, previewData || entry, siteUrl);
14064
+ const readAccess = (schema?.access)?.read;
12846
14065
  let canRead = true;
12847
14066
  if (readAccess === false) canRead = false;
12848
14067
  else if (typeof readAccess === "string") try {
@@ -12853,27 +14072,7 @@ function EditEntryPage() {
12853
14072
  } catch (e) {
12854
14073
  console.warn("Read access eval failed:", e);
12855
14074
  }
12856
- if (!canRead) return /* @__PURE__ */ jsx("div", {
12857
- className: "dy-flex dy-items-center dy-justify-center dy-h-[calc(100vh-200px)]",
12858
- children: /* @__PURE__ */ jsxs("div", {
12859
- className: "dy-text-center dy-space-y-3",
12860
- children: [
12861
- /* @__PURE__ */ jsx("div", {
12862
- className: "dy-p-3 dy-bg-destructive/10 dy-text-destructive dy-rounded-full dy-w-12 dy-h-12 dy-mx-auto dy-flex dy-items-center dy-justify-center",
12863
- children: /* @__PURE__ */ jsx(Archive, { className: "dy-h-6 dy-w-6" })
12864
- }),
12865
- /* @__PURE__ */ jsx("h3", {
12866
- className: "dy-text-lg dy-font-bold",
12867
- children: "Access Denied"
12868
- }),
12869
- /* @__PURE__ */ jsx("p", {
12870
- className: "dy-text-sm dy-text-muted-foreground",
12871
- children: "You do not have permission to view this entry."
12872
- })
12873
- ]
12874
- })
12875
- });
12876
- const createAccess = schema.access?.create;
14075
+ const createAccess = (schema?.access)?.create;
12877
14076
  let canCreate = true;
12878
14077
  if (createAccess === false) canCreate = false;
12879
14078
  else if (typeof createAccess === "string") try {
@@ -12881,7 +14080,7 @@ function EditEntryPage() {
12881
14080
  } catch (e) {
12882
14081
  console.warn("Create access eval failed:", e);
12883
14082
  }
12884
- const updateAccess = schema.access?.update;
14083
+ const updateAccess = (schema?.access)?.update;
12885
14084
  let canUpdate = true;
12886
14085
  if (updateAccess === false) canUpdate = false;
12887
14086
  else if (typeof updateAccess === "string") try {
@@ -12892,7 +14091,7 @@ function EditEntryPage() {
12892
14091
  } catch (e) {
12893
14092
  console.warn("Update access eval failed:", e);
12894
14093
  }
12895
- const auditAccess = schema.access?.readAudit ?? readAccess;
14094
+ const auditAccess = (schema?.access)?.readAudit ?? readAccess;
12896
14095
  let canReadAudit = true;
12897
14096
  if (auditAccess === false) canReadAudit = false;
12898
14097
  else if (typeof auditAccess === "string") try {
@@ -12904,11 +14103,158 @@ function EditEntryPage() {
12904
14103
  canReadAudit = false;
12905
14104
  }
12906
14105
  const workflowAvailable = !!(workflowConfig && isEdit && workflowMeta);
14106
+ const liveSnapshot = entry?.__published && typeof entry.__published === "object" ? entry.__published : null;
14107
+ const compareToLiveEnabled = workflowAvailable && !!liveSnapshot;
14108
+ const compareToLiveReason = !workflowAvailable ? "Workflow is not enabled for this entry." : liveSnapshot ? null : "No live snapshot exists yet. Publish this entry once to compare future draft changes.";
14109
+ const draftLiveComparison = useMemo(() => buildDraftLiveComparison({
14110
+ fields: orderedFields,
14111
+ draft: previewData || entry || null,
14112
+ live: liveSnapshot
14113
+ }), [
14114
+ entry,
14115
+ liveSnapshot,
14116
+ orderedFields,
14117
+ previewData
14118
+ ]);
14119
+ const workflowAutosaveConfig = resolveWorkflowAutosaveSettings(schema);
14120
+ const workflowAutosaveEnabled = Boolean(isEdit && workflowAutosaveConfig.enabled && (schema?.workflow || schema?.drafts) && canUpdate);
14121
+ const documentLabel = useMemo(() => entry ? resolveDocumentTitle({
14122
+ entry,
14123
+ collection: schema,
14124
+ collections: schemas?.collections
14125
+ }) : void 0, [
14126
+ entry,
14127
+ schema,
14128
+ schemas?.collections
14129
+ ]);
14130
+ const persistDraft = useCallback((data, mode) => {
14131
+ const promise = saveMutation.mutateAsync({
14132
+ data,
14133
+ mode
14134
+ });
14135
+ draftSavePromiseRef.current = promise.finally(() => {
14136
+ if (draftSavePromiseRef.current === promise) draftSavePromiseRef.current = null;
14137
+ });
14138
+ return promise;
14139
+ }, [saveMutation]);
14140
+ const handleManualSave = useCallback((data) => {
14141
+ return persistDraft(data, "manual");
14142
+ }, [persistDraft]);
14143
+ const handleWorkflowAutosave = useCallback(async (data) => {
14144
+ await persistDraft(data, "autosave");
14145
+ }, [persistDraft]);
14146
+ const autosaveConfig = useMemo(() => {
14147
+ if (!workflowAutosaveEnabled) return void 0;
14148
+ return {
14149
+ enabled: true,
14150
+ delayMs: workflowAutosaveConfig.delayMs,
14151
+ onSave: handleWorkflowAutosave,
14152
+ onStatusChange: setWorkflowAutosaveState
14153
+ };
14154
+ }, [
14155
+ handleWorkflowAutosave,
14156
+ workflowAutosaveConfig.delayMs,
14157
+ workflowAutosaveEnabled
14158
+ ]);
14159
+ const showWorkflowAutosaveStatus = workflowAutosaveEnabled && activeTab === "edit";
14160
+ const showManualSaveChrome = !workflowAutosaveEnabled;
12907
14161
  const showLivePreview = activeTab === "edit" && showPreview && !!previewUrl;
14162
+ const resolveTransitionContextFromDoc = useCallback((doc) => {
14163
+ const resolvedDocumentId = String(doc?.id ?? id ?? "");
14164
+ const resolvedWorkflowMeta = doc?._workflow ?? workflowMeta ?? null;
14165
+ const resolvedDocumentLabel = doc ? resolveDocumentTitle({
14166
+ entry: doc,
14167
+ collection: schema,
14168
+ collections: schemas?.collections
14169
+ }) : documentLabel;
14170
+ return {
14171
+ documentIds: resolvedDocumentId ? [resolvedDocumentId] : [],
14172
+ documentLabels: resolvedDocumentId ? { [resolvedDocumentId]: resolvedDocumentLabel } : void 0,
14173
+ expectedRevisions: resolvedDocumentId && typeof resolvedWorkflowMeta?.revision === "number" ? { [resolvedDocumentId]: resolvedWorkflowMeta.revision } : void 0,
14174
+ invalidateQueryKeys: resolvedDocumentId ? [
14175
+ [
14176
+ "entry",
14177
+ slug,
14178
+ resolvedDocumentId
14179
+ ],
14180
+ ["collection", slug],
14181
+ [
14182
+ "workflow-history",
14183
+ slug,
14184
+ resolvedDocumentId
14185
+ ]
14186
+ ] : [["collection", slug]]
14187
+ };
14188
+ }, [
14189
+ documentLabel,
14190
+ id,
14191
+ schema,
14192
+ schemas?.collections,
14193
+ slug,
14194
+ workflowMeta
14195
+ ]);
14196
+ const saveCurrentDraftNow = useCallback(async (mode = "manual") => {
14197
+ if (draftSavePromiseRef.current) await draftSavePromiseRef.current;
14198
+ if (!formEngineRef.current) throw new Error("The editor is not ready yet.");
14199
+ const result = await formEngineRef.current.submitCurrentDraft();
14200
+ const savedDoc = result?.doc ?? null;
14201
+ return {
14202
+ result,
14203
+ context: resolveTransitionContextFromDoc(savedDoc),
14204
+ mode
14205
+ };
14206
+ }, [resolveTransitionContextFromDoc]);
14207
+ const prepareWorkflowTransition = useCallback(async (_transition) => {
14208
+ if (draftSavePromiseRef.current) await draftSavePromiseRef.current;
14209
+ if (formEngineRef.current?.isDirty()) {
14210
+ const { context } = await saveCurrentDraftNow("transition");
14211
+ return context;
14212
+ }
14213
+ return resolveTransitionContextFromDoc(entry ?? null);
14214
+ }, [
14215
+ entry,
14216
+ resolveTransitionContextFromDoc,
14217
+ saveCurrentDraftNow
14218
+ ]);
14219
+ const [sendingReset, setSendingReset] = useState(false);
14220
+ const handleSendResetLink = async () => {
14221
+ if (!entry?.email) return;
14222
+ setSendingReset(true);
14223
+ try {
14224
+ await client.collection(slug).sendResetLink(entry.email);
14225
+ toast.success("Reset link sent", { description: `A password reset email has been sent to ${entry.email}.` });
14226
+ } catch (err) {
14227
+ toast.error("Failed to send reset link", { description: err instanceof Error ? err.message : "An unexpected error occurred." });
14228
+ } finally {
14229
+ setSendingReset(false);
14230
+ }
14231
+ };
14232
+ if (!schema) return /* @__PURE__ */ jsx("div", { children: "Collection not found" });
14233
+ if (isEdit && isEntryLoading) return /* @__PURE__ */ jsx("div", { children: "Loading entry..." });
14234
+ if (!canRead) return /* @__PURE__ */ jsx("div", {
14235
+ className: "dy-flex dy-items-center dy-justify-center dy-h-[calc(100vh-200px)]",
14236
+ children: /* @__PURE__ */ jsxs("div", {
14237
+ className: "dy-text-center dy-space-y-3",
14238
+ children: [
14239
+ /* @__PURE__ */ jsx("div", {
14240
+ className: "dy-p-3 dy-bg-destructive/10 dy-text-destructive dy-rounded-full dy-w-12 dy-h-12 dy-mx-auto dy-flex dy-items-center dy-justify-center",
14241
+ children: /* @__PURE__ */ jsx(Archive, { className: "dy-h-6 dy-w-6" })
14242
+ }),
14243
+ /* @__PURE__ */ jsx("h3", {
14244
+ className: "dy-text-lg dy-font-bold",
14245
+ children: "Access Denied"
14246
+ }),
14247
+ /* @__PURE__ */ jsx("p", {
14248
+ className: "dy-text-sm dy-text-muted-foreground",
14249
+ children: "You do not have permission to view this entry."
14250
+ })
14251
+ ]
14252
+ })
14253
+ });
12908
14254
  const docsToDisplay = debouncedSearchQuery ? siblingEntries?.docs || [] : siblingEntries?.docs?.filter((d) => String(d.id) !== id).slice(0, 4) || [];
12909
- return /* @__PURE__ */ jsx(NestedEditorProvider, {
14255
+ return /* @__PURE__ */ jsxs(NestedEditorProvider, {
12910
14256
  drillInEnabled: !!showLivePreview,
12911
- children: /* @__PURE__ */ jsxs("div", {
14257
+ children: [/* @__PURE__ */ jsxs("div", {
12912
14258
  className: cn("dy-flex dy-flex-col dy--mt-6 dy--mb-6 dy--mx-4 lg:dy--mt-10 lg:dy--mb-10 lg:dy--mx-6", showLivePreview ? "dy-h-screen" : ""),
12913
14259
  children: [/* @__PURE__ */ jsxs("div", {
12914
14260
  className: "dy-flex dy-flex-wrap dy-shrink-0 dy-items-center dy-gap-2 dy-border-b dy-border-border/50 dy-bg-background dy-px-3 dy-py-2",
@@ -12933,7 +14279,11 @@ function EditEntryPage() {
12933
14279
  className: "dy-flex dy-items-center dy-gap-1 dy-text-base dy-font-serif dy-font-bold dy-tracking-tight dy-text-foreground hover:dy-bg-muted/80 dy-px-2 dy-py-1 dy-rounded-lg dy-transition-all dy-outline-none dy-min-w-0",
12934
14280
  children: [/* @__PURE__ */ jsx("span", {
12935
14281
  className: "dy-truncate dy-max-w-[150px] sm:dy-max-w-none dy-inline-block",
12936
- children: isEdit && entry ? getEntryTitle(entry, schema) : "..."
14282
+ children: isEdit && entry ? resolveDocumentTitle({
14283
+ entry,
14284
+ collection: schema,
14285
+ collections: schemas?.collections
14286
+ }) : "..."
12937
14287
  }), /* @__PURE__ */ jsx(ChevronDown, { className: "dy-h-4 dy-w-4 dy-text-muted-foreground/80 dy-shrink-0" })]
12938
14288
  }), /* @__PURE__ */ jsx(PopoverContent, {
12939
14289
  align: "start",
@@ -12957,7 +14307,11 @@ function EditEntryPage() {
12957
14307
  className: "dy-cursor-pointer dy-py-2 dy-px-3 dy-rounded-lg",
12958
14308
  children: /* @__PURE__ */ jsx("span", {
12959
14309
  className: "dy-text-sm dy-text-foreground",
12960
- children: getEntryTitle(sibling, schema)
14310
+ children: resolveDocumentTitle({
14311
+ entry: sibling,
14312
+ collection: schema,
14313
+ collections: schemas?.collections
14314
+ })
12961
14315
  })
12962
14316
  }, sibling.id)) }) })]
12963
14317
  })
@@ -12965,12 +14319,12 @@ function EditEntryPage() {
12965
14319
  })] }) : /* @__PURE__ */ jsxs("h1", {
12966
14320
  className: "dy-text-base dy-font-serif dy-font-bold dy-tracking-tight dy-text-foreground dy-truncate",
12967
14321
  children: ["New ", schema?.labels?.singular || schema?.slug]
12968
- }), showStatusBadge && /* @__PURE__ */ jsx(Badge, {
12969
- className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-shrink-0", workflowBadgePresentation.className),
12970
- style: workflowBadgePresentation.style,
14322
+ }), showStatusBadge && /* @__PURE__ */ jsxs(Fragment, { children: [publishingStatus?.workflowStateLabel && workflowStagePresentation && /* @__PURE__ */ jsx(Badge, {
14323
+ className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-semibold dy-tracking-wide dy-shrink-0", workflowStagePresentation.className),
14324
+ style: workflowStagePresentation.style,
12971
14325
  variant: "outline",
12972
- children: statusBadgeLabel
12973
- })]
14326
+ children: publishingStatus.workflowStateLabel
14327
+ }), showWorkflowAutosaveStatus && /* @__PURE__ */ jsx(DraftSaveStatusBadge, { state: workflowAutosaveState })] })]
12974
14328
  }),
12975
14329
  /* @__PURE__ */ jsxs("div", {
12976
14330
  className: "dy-ml-auto dy-flex dy-items-center dy-gap-1",
@@ -12998,9 +14352,9 @@ function EditEntryPage() {
12998
14352
  }),
12999
14353
  workflowAvailable && /* @__PURE__ */ jsx(HeaderAction, {
13000
14354
  icon: Workflow,
13001
- label: "Workflow",
14355
+ label: "Workflow details",
13002
14356
  active: activeTab === "workflow",
13003
- title: activeTab === "workflow" ? "Hide workflow transitions" : "Show workflow transitions",
14357
+ title: activeTab === "workflow" ? "Hide workflow details" : "Show workflow details",
13004
14358
  onClick: () => setActiveTab((tab) => tab === "workflow" ? "edit" : "workflow")
13005
14359
  }),
13006
14360
  schema?.audit && isEdit && canReadAudit && /* @__PURE__ */ jsx(HeaderAction, {
@@ -13068,12 +14422,35 @@ function EditEntryPage() {
13068
14422
  })]
13069
14423
  })] })]
13070
14424
  })] }),
13071
- (isEdit ? canUpdate : canCreate) && activeTab === "edit" && /* @__PURE__ */ jsxs("div", {
14425
+ (isEdit ? canUpdate : canCreate) && activeTab === "edit" && /* @__PURE__ */ jsxs(Fragment, { children: [workflowAvailable && /* @__PURE__ */ jsx(WorkflowTransitionSplitButton, {
14426
+ collection: slug,
14427
+ documentId: id,
14428
+ documentLabel,
14429
+ workflowConfig,
14430
+ workflowMeta,
14431
+ onPendingChange: setWorkflowTransitionPending,
14432
+ onSaveDraft: () => saveCurrentDraftNow("manual").then(() => void 0),
14433
+ saveDraftPending: saveMutation.isPending,
14434
+ prepareTransition: prepareWorkflowTransition,
14435
+ invalidateQueryKeys: [
14436
+ [
14437
+ "entry",
14438
+ slug,
14439
+ id
14440
+ ],
14441
+ ["collection", slug],
14442
+ [
14443
+ "workflow-history",
14444
+ slug,
14445
+ id
14446
+ ]
14447
+ ]
14448
+ }), showManualSaveChrome && /* @__PURE__ */ jsxs("div", {
13072
14449
  className: "dy-hidden md:dy-flex dy-items-center",
13073
14450
  children: [/* @__PURE__ */ jsx("div", { className: "dy-mx-1 dy-h-6 dy-w-px dy-bg-border/60" }), /* @__PURE__ */ jsx(Button, {
13074
14451
  size: "sm",
13075
14452
  className: "dy-h-9 dy-rounded-lg dy-px-4 dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90 dy-shadow-sm dy-shrink-0",
13076
- onClick: () => document.getElementById("dyrected-form-submit")?.click(),
14453
+ onClick: () => void saveCurrentDraftNow("manual"),
13077
14454
  disabled: saveMutation.isPending,
13078
14455
  title: isEdit ? "Save Changes (⌘S)" : "Create Entry (⌘S)",
13079
14456
  children: saveMutation.isPending ? /* @__PURE__ */ jsxs("span", {
@@ -13084,7 +14461,7 @@ function EditEntryPage() {
13084
14461
  children: [/* @__PURE__ */ jsx(Save, { className: "dy-h-3.5 dy-w-3.5" }), isEdit ? "Save" : "Create"]
13085
14462
  })
13086
14463
  })]
13087
- })
14464
+ })] })
13088
14465
  ]
13089
14466
  })
13090
14467
  ]
@@ -13116,7 +14493,13 @@ function EditEntryPage() {
13116
14493
  collection: slug,
13117
14494
  documentId: id,
13118
14495
  workflowMeta,
13119
- workflowConfig
14496
+ workflowConfig,
14497
+ compareToLiveEnabled,
14498
+ compareToLiveReason,
14499
+ onCompareToLive: () => setCompareSheetOpen(true),
14500
+ onSaveDraft: () => saveCurrentDraftNow("manual").then(() => void 0),
14501
+ saveDraftPending: saveMutation.isPending,
14502
+ prepareTransition: prepareWorkflowTransition
13120
14503
  })
13121
14504
  }),
13122
14505
  activeTab === "audit" && /* @__PURE__ */ jsx("div", {
@@ -13293,19 +14676,21 @@ function EditEntryPage() {
13293
14676
  queryParamsDefaults[key] = value;
13294
14677
  });
13295
14678
  return /* @__PURE__ */ jsx(FormEngine, {
14679
+ ref: formEngineRef,
13296
14680
  collection: slug,
13297
14681
  fields: orderedFields,
13298
14682
  defaultValues: isEdit ? entry : {
13299
14683
  ...queryParamsDefaults,
13300
14684
  ...entry
13301
14685
  },
13302
- onSubmit: (data) => saveMutation.mutate(data),
14686
+ onSubmit: handleManualSave,
14687
+ autosave: autosaveConfig,
13303
14688
  onDataChange: (newData) => setPreviewData({
13304
14689
  ...entry,
13305
14690
  ...newData
13306
14691
  }),
13307
14692
  onChange: (dirty) => setIsDirty(dirty),
13308
- isLoading: saveMutation.isPending || isPreferenceLoading,
14693
+ isLoading: saveMutation.isPending || workflowTransitionPending || isPreferenceLoading,
13309
14694
  submitLabel: isEdit ? "Save Changes" : "Create Entry",
13310
14695
  hideSubmit: true,
13311
14696
  readOnly: isEdit ? !canUpdate : !canCreate,
@@ -13320,7 +14705,7 @@ function EditEntryPage() {
13320
14705
  form: "dyrected-edit-form",
13321
14706
  className: "dy-hidden"
13322
14707
  }),
13323
- (isDirty || !isEdit) && (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
14708
+ showManualSaveChrome && (isDirty || !isEdit) && (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
13324
14709
  className: "dy-hidden md:dy-block dy-sticky dy-bottom-0 dy-left-0 dy-right-0 dy-z-20 dy-pointer-events-none",
13325
14710
  children: /* @__PURE__ */ jsx("div", {
13326
14711
  className: "dy-pointer-events-auto dy-mx-auto dy-max-w-2xl dy-px-4 dy-pb-4",
@@ -13332,7 +14717,7 @@ function EditEntryPage() {
13332
14717
  }), /* @__PURE__ */ jsx(Button, {
13333
14718
  size: "sm",
13334
14719
  className: "dy-h-9 dy-px-5 dy-rounded-xl dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90 dy-shadow-sm dy-shrink-0",
13335
- onClick: () => document.getElementById("dyrected-form-submit")?.click(),
14720
+ onClick: () => void saveCurrentDraftNow("manual"),
13336
14721
  disabled: saveMutation.isPending,
13337
14722
  children: saveMutation.isPending ? /* @__PURE__ */ jsxs("div", {
13338
14723
  className: "dy-flex dy-items-center dy-gap-2",
@@ -13345,11 +14730,11 @@ function EditEntryPage() {
13345
14730
  })
13346
14731
  })
13347
14732
  }),
13348
- (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
14733
+ showManualSaveChrome && (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
13349
14734
  className: "md:dy-hidden dy-sticky dy-bottom-0 dy-left-0 dy-right-0 dy-z-20 dy-border-t dy-border-border dy-bg-background/95 dy-backdrop-blur-sm dy-px-4 dy-py-3",
13350
14735
  children: /* @__PURE__ */ jsx(Button, {
13351
14736
  className: "dy-w-full dy-h-11 dy-rounded-xl dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90 dy-shadow-sm",
13352
- onClick: () => document.getElementById("dyrected-form-submit")?.click(),
14737
+ onClick: () => void saveCurrentDraftNow("manual"),
13353
14738
  disabled: saveMutation.isPending || !(isDirty || !isEdit),
13354
14739
  children: saveMutation.isPending ? /* @__PURE__ */ jsxs("span", {
13355
14740
  className: "dy-flex dy-items-center dy-gap-2",
@@ -13366,7 +14751,11 @@ function EditEntryPage() {
13366
14751
  })
13367
14752
  })]
13368
14753
  })]
13369
- }, id || "new")
14754
+ }, id || "new"), workflowAvailable && compareToLiveEnabled && /* @__PURE__ */ jsx(DraftLiveCompareSheet, {
14755
+ open: compareSheetOpen,
14756
+ onOpenChange: setCompareSheetOpen,
14757
+ comparison: draftLiveComparison
14758
+ })]
13370
14759
  });
13371
14760
  }
13372
14761
  function AuditPanel({ collection, documentId }) {
@@ -13497,44 +14886,6 @@ function AuditPanel({ collection, documentId }) {
13497
14886
  });
13498
14887
  }
13499
14888
  //#endregion
13500
- //#region src/components/ui/card.tsx
13501
- var Card = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13502
- ref,
13503
- className: cn("dy-rounded-lg dy-border dy-bg-card dy-text-card-foreground dy-shadow-sm", className),
13504
- ...props
13505
- }));
13506
- Card.displayName = "Card";
13507
- var CardHeader = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13508
- ref,
13509
- className: cn("dy-flex dy-flex-col dy-space-y-1.5 dy-p-6", className),
13510
- ...props
13511
- }));
13512
- CardHeader.displayName = "CardHeader";
13513
- var CardTitle = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13514
- ref,
13515
- className: cn("dy-text-2xl dy-font-semibold dy-leading-none dy-tracking-tight", className),
13516
- ...props
13517
- }));
13518
- CardTitle.displayName = "CardTitle";
13519
- var CardDescription = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13520
- ref,
13521
- className: cn("dy-text-sm dy-text-muted-foreground", className),
13522
- ...props
13523
- }));
13524
- CardDescription.displayName = "CardDescription";
13525
- var CardContent = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13526
- ref,
13527
- className: cn("dy-p-6 dy-pt-0", className),
13528
- ...props
13529
- }));
13530
- CardContent.displayName = "CardContent";
13531
- var CardFooter = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13532
- ref,
13533
- className: cn("dy-flex dy-items-center dy-p-6 dy-pt-0", className),
13534
- ...props
13535
- }));
13536
- CardFooter.displayName = "CardFooter";
13537
- //#endregion
13538
14889
  //#region src/components/ui/aspect-ratio.tsx
13539
14890
  var AspectRatio = AspectRatioPrimitive.Root;
13540
14891
  //#endregion
@@ -14814,7 +16165,7 @@ function SetupPromptUI({ config }) {
14814
16165
  setCopied(true);
14815
16166
  window.setTimeout(() => setCopied(false), 1800);
14816
16167
  }
14817
- const currentVersion = "2.5.65";
16168
+ const currentVersion = "2.6.0";
14818
16169
  const [latestVersion, setLatestVersion] = useState(() => {
14819
16170
  if (typeof window === "undefined") return null;
14820
16171
  return localStorage.getItem("dyrected_latest_release");