@parallelworks/ui 0.37.0 → 0.38.0

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.
@@ -2,18 +2,19 @@ import { FaRegFileImage } from "../../icons/fa/FaRegFileImage.js";
2
2
  import { FaSearch } from "../../icons/fa/FaSearch.js";
3
3
  import { FaUsers } from "../../icons/fa/FaUsers.js";
4
4
  import { TbEdit } from "../../icons/tb/TbEdit.js";
5
+ import { useRelativeTime } from "../../components/relativeTime.js";
5
6
  import { useChatConfig } from "../core/config.js";
6
7
  import { useChat } from "../core/ChatProvider.js";
7
8
  import { ManageProvidersLink } from "./ComposerChrome.js";
8
9
  import { FOCUS_SIDEBAR_SEARCH_EVENT } from "../core/events.js";
9
- import { SidebarPlaceholder, SidebarRow } from "../ui/sidebar.js";
10
+ import { SidebarPlaceholder, SidebarRow, SidebarRowTitle } from "../ui/sidebar.js";
10
11
  import { ConversationSidebar } from "../ui/ConversationSidebar.js";
11
12
  import { groupConversationsByDate, useConversationActions } from "./conversationActions.js";
12
13
  import cx from "classnames";
13
14
  import { useEffect, useMemo, useRef, useState } from "react";
14
15
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
15
16
  //#region src/ai/components/ChatSidebar.tsx
16
- function ConversationRow({ conv, isCurrent, menuItems, openMenu, onOpen }) {
17
+ function ConversationRow({ conv, isCurrent, updated, menuItems, openMenu, onOpen }) {
17
18
  const { LinkComponent, strings } = useChatConfig();
18
19
  const t = strings.sidebar;
19
20
  return /* @__PURE__ */ jsx(SidebarRow, {
@@ -30,8 +31,8 @@ function ConversationRow({ conv, isCurrent, menuItems, openMenu, onOpen }) {
30
31
  children: [conv.isOwner === false && /* @__PURE__ */ jsx(FaUsers, {
31
32
  className: "h-3 w-3 shrink-0 theme-muted-text",
32
33
  title: t.sharedWithYou
33
- }), /* @__PURE__ */ jsx("span", {
34
- className: "min-w-0 flex-1 truncate text-sm theme-text",
34
+ }), /* @__PURE__ */ jsx(SidebarRowTitle, {
35
+ time: updated,
35
36
  children: conv.title || t.untitled
36
37
  })]
37
38
  })
@@ -43,6 +44,7 @@ function ChatSidebar({ actions = true, top }) {
43
44
  const { adapter, navigation, activeConversationId, conversations, sidebar, sidebarPresentation, drawerOpen, setSidebar, setDrawerOpen, toggleSidebar, isLoading, loadConversations, clearCurrentConversation } = useChat();
44
45
  const attachmentsAvailable = !!adapter.attachments;
45
46
  const { menuItems, openMenu, overlays } = useConversationActions();
47
+ const { compact } = useRelativeTime();
46
48
  const drawer = sidebarPresentation === "drawer";
47
49
  const sidebarCollapsed = !drawer && sidebar !== "expanded";
48
50
  const closeDrawer = () => setDrawerOpen(false);
@@ -106,6 +108,7 @@ function ChatSidebar({ actions = true, top }) {
106
108
  renderRow: (conv) => /* @__PURE__ */ jsx(ConversationRow, {
107
109
  conv,
108
110
  isCurrent: activeConversationId === conv.id,
111
+ updated: compact(conv.updatedAt ?? conv.createdAt),
109
112
  menuItems: menuItems(conv),
110
113
  openMenu,
111
114
  onOpen: closeDrawer
@@ -43,5 +43,5 @@ export * from './types.js';
43
43
  export { ConversationSidebar, type ConversationSidebarGroup, useResizableSidebarWidth, } from './ui/ConversationSidebar.js';
44
44
  export { type DropdownOption, default as Dropdown } from './ui/Dropdown.js';
45
45
  export { NoticeBar, NoticeCard, type NoticeTone, OutputCard } from './ui/Notice.js';
46
- export { RenameDialog, type RowDialogStrings, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs, } from './ui/sidebar.js';
46
+ export { RenameDialog, type RowDialogStrings, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarRowTitle, SidebarToggle, useRowDialogs, } from './ui/sidebar.js';
47
47
  export { formatFileSize, isMac, isOrgModel } from './utils.js';
package/dist/ai/index.js CHANGED
@@ -37,7 +37,7 @@ import ChatEmptyState from "./components/ChatEmptyState.js";
37
37
  import { FOCUS_SIDEBAR_SEARCH_EVENT } from "./core/events.js";
38
38
  import { useKeyboardShortcuts } from "./core/useKeyboardShortcuts.js";
39
39
  import { KeyboardShortcutsProvider } from "./core/KeyboardShortcutsProvider.js";
40
- import { RenameDialog, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs } from "./ui/sidebar.js";
40
+ import { RenameDialog, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarRowTitle, SidebarToggle, useRowDialogs } from "./ui/sidebar.js";
41
41
  import { ConversationSidebar, useResizableSidebarWidth } from "./ui/ConversationSidebar.js";
42
42
  import ShareDialog, { ShareButton } from "./components/ShareDialog.js";
43
43
  import { groupConversationsByDate, useConversationActions } from "./components/conversationActions.js";
@@ -46,4 +46,4 @@ import ChatLayout, { CHAT_COLUMN_SCOPE, ConversationLayout, DRAWER_BELOW_PX } fr
46
46
  import ChatMessage_default from "./components/ChatMessage.js";
47
47
  import ChatMessageList from "./components/ChatMessageList.js";
48
48
  import ChatThread from "./components/ChatThread.js";
49
- export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, CHAT_COLUMN_SCOPE, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatStage, ChatThread, ComposerContext, ComposerControls, ComposerSettings, ComposerUsage, ConnectToolsLink, ConversationLayout, ConversationSidebar, DEFAULT_PERMISSION_MODE, DRAWER_BELOW_PX, DragOverlay, Dropdown, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ManageProvidersLink, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, OutputCard, PermissionPicker, ProviderIssueBanner, RenameDialog, ShareButton, ShareDialog, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarToggle, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, compactCount, computeEditDiff, computeWriteRows, defaultChatStrings, durationToAbsHumanDuration, editSummary, finalizeParts, firstNonEmptyLine, formatElapsed, formatFileSize, fullToolArgs, getGreeting, getProviderKeyFromModelId, groupConversationsByDate, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, pasteInlineMaxBytes, permissionModesUpTo, planFromArgs, plural, providerIssueFor, resolveChatConfig, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, slashMatches, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useConversationActions, useDragDrop, useKeyboardShortcuts, useResizableSidebarWidth, useRowDialogs, wordEmphasisRanges };
49
+ export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, CHAT_COLUMN_SCOPE, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatStage, ChatThread, ComposerContext, ComposerControls, ComposerSettings, ComposerUsage, ConnectToolsLink, ConversationLayout, ConversationSidebar, DEFAULT_PERMISSION_MODE, DRAWER_BELOW_PX, DragOverlay, Dropdown, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ManageProvidersLink, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, OutputCard, PermissionPicker, ProviderIssueBanner, RenameDialog, ShareButton, ShareDialog, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarRowTitle, SidebarToggle, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, compactCount, computeEditDiff, computeWriteRows, defaultChatStrings, durationToAbsHumanDuration, editSummary, finalizeParts, firstNonEmptyLine, formatElapsed, formatFileSize, fullToolArgs, getGreeting, getProviderKeyFromModelId, groupConversationsByDate, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, pasteInlineMaxBytes, permissionModesUpTo, planFromArgs, plural, providerIssueFor, resolveChatConfig, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, slashMatches, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useConversationActions, useDragDrop, useKeyboardShortcuts, useResizableSidebarWidth, useRowDialogs, wordEmphasisRanges };
@@ -43,6 +43,12 @@ export declare function SidebarPlaceholder({ children, testId, }: {
43
43
  children: ReactNode;
44
44
  testId?: string | undefined;
45
45
  }): import("react").JSX.Element;
46
+ /** A row's first line, the same in every list: what it is, and when it last
47
+ * moved, abbreviated, on the right. */
48
+ export declare function SidebarRowTitle({ children, time, }: {
49
+ children: ReactNode;
50
+ time?: string | undefined;
51
+ }): import("react").JSX.Element;
46
52
  /** One entry in the list: the row highlights while it is the open one or
47
53
  * while its menu is up, and the menu opens from its button or a right-click. */
48
54
  export declare function SidebarRow({ selected, menuItems, openMenu, className, children, }: {
@@ -85,6 +85,20 @@ function SidebarPlaceholder({ children, testId }) {
85
85
  children
86
86
  });
87
87
  }
88
+ /** A row's first line, the same in every list: what it is, and when it last
89
+ * moved, abbreviated, on the right. */
90
+ function SidebarRowTitle({ children, time }) {
91
+ return /* @__PURE__ */ jsxs("span", {
92
+ className: "flex min-w-0 flex-1 items-baseline gap-2",
93
+ children: [/* @__PURE__ */ jsx("span", {
94
+ className: "min-w-0 flex-1 truncate text-sm theme-text",
95
+ children
96
+ }), time && /* @__PURE__ */ jsx("span", {
97
+ className: "min-w-8 shrink-0 text-[11px] tabular-nums theme-muted-text",
98
+ children: time
99
+ })]
100
+ });
101
+ }
88
102
  /** One entry in the list: the row highlights while it is the open one or
89
103
  * while its menu is up, and the menu opens from its button or a right-click. */
90
104
  function SidebarRow({ selected, menuItems, openMenu, className, children }) {
@@ -191,4 +205,4 @@ function useRowDialogs({ strings, nameOf, onDelete, onRename }) {
191
205
  };
192
206
  }
193
207
  //#endregion
194
- export { RenameDialog, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs };
208
+ export { RenameDialog, SidebarDrawer, SidebarGroupHeading, SidebarPanel, SidebarPlaceholder, SidebarRailItem, SidebarRow, SidebarRowTitle, SidebarToggle, useRowDialogs };
@@ -27,6 +27,7 @@ function FieldWrapper({ label, tooltipComponent, description, labelPosition = "l
27
27
  children: [/* @__PURE__ */ jsxs("label", {
28
28
  htmlFor: id,
29
29
  id: labelId,
30
+ "data-field-label": true,
30
31
  className: cx("relative", labelPosition === "left" ? "form-label" : "form-label-top", spaceCompact && "form-label-compact"),
31
32
  children: [
32
33
  label,
package/dist/form/Form.js CHANGED
@@ -14,6 +14,7 @@ import { FormLayout, LayoutCSSContext } from "./FormLayout.js";
14
14
  import { useFieldControlProps, useFieldRequired } from "./fieldContext.js";
15
15
  import { getValueUsingPath } from "./utils/getValueUsingPath.js";
16
16
  import { FieldRegistryContext, Registry } from "./fieldRegistry.js";
17
+ import { EditingScope, useFormEditing } from "./formEditing.js";
17
18
  import { useParsedOpts } from "./useParsedOpts.js";
18
19
  import { usePrevious } from "./usePrevious.js";
19
20
  import { getParentValue } from "./utils/getParentValue.js";
@@ -209,6 +210,7 @@ function GroupHeader({ children, open, setOpen, title }) {
209
210
  children: [/* @__PURE__ */ jsxs("button", {
210
211
  type: "button",
211
212
  "aria-expanded": open,
213
+ "data-field-label": true,
212
214
  className: "w-full flex items-center text-left transform ease-in transition cursor-pointer",
213
215
  onClick: () => setOpen((open) => !open),
214
216
  children: [/* @__PURE__ */ jsx(FaAngleRight, { className: cx(open && "transform rotate-90 ", "px-1 text-[1.25rem] transition ease-in ") }), /* @__PURE__ */ jsx("span", { children: title })]
@@ -393,6 +395,8 @@ function useFieldParse(optionsField, values, arrayIndex) {
393
395
  }
394
396
  var FormField = React.memo(function FormField({ optionsField, fieldName, values, setFormDirty, setFieldValue, setFieldTouched, parentInfo, labelPosition = "left", missingFields = [], spaceCompact = false, workflowForm = false }) {
395
397
  const parsedField = useFieldParse(optionsField, values, parentInfo?.arrayIndex);
398
+ const editing = useFormEditing();
399
+ const path = editing ? [...editing.parent, fieldName] : null;
396
400
  const field = asSchemaField(parentInfo ? parsedField : optionsField);
397
401
  if (field === null) return null;
398
402
  const fieldNamePrefix = parentInfo?.fieldNamePrefix || "";
@@ -405,7 +409,11 @@ var FormField = React.memo(function FormField({ optionsField, fieldName, values,
405
409
  if (!fieldObj.name || fieldName === "v3") return null;
406
410
  if (field.hidden === true || !field.type) {
407
411
  if (resolvedFlag(field.ignore) && !workflowForm) deleteFieldByPath(values, fieldObj.name);
408
- return null;
412
+ return editing && path && fieldName !== "$meta" ? /* @__PURE__ */ jsx(editing.Row, {
413
+ path,
414
+ instance: fieldObj.name,
415
+ hidden: true
416
+ }) : null;
409
417
  }
410
418
  let onChange;
411
419
  if (field.one_must_be_true && parentInfo?.onChange) {
@@ -421,7 +429,11 @@ var FormField = React.memo(function FormField({ optionsField, fieldName, values,
421
429
  if (shouldShowField && field.show_if_not !== void 0) shouldShowField = !getParentValue(index, field.show_if_not, values, field);
422
430
  if (!shouldShowField) {
423
431
  if (!workflowForm) deleteFieldByPath(values, fieldObj.name);
424
- return null;
432
+ return editing && path ? /* @__PURE__ */ jsx(editing.Row, {
433
+ path,
434
+ instance: fieldObj.name,
435
+ hidden: true
436
+ }) : null;
425
437
  }
426
438
  }
427
439
  if (field.depends_on && parentInfo?.arrayIndex !== void 0) fieldObj.depends_on = field.depends_on.replace("[index]", `[${parentInfo.arrayIndex}]`);
@@ -436,7 +448,7 @@ var FormField = React.memo(function FormField({ optionsField, fieldName, values,
436
448
  const hideHeader = field.noCollapse;
437
449
  const HeaderElement = hideHeader ? React.Fragment : UncontrolledCollapsiblePanel;
438
450
  const meta = resolveMetaOverrides(field.options, labelPosition, spaceCompact);
439
- return /* @__PURE__ */ jsx("div", {
451
+ const group = /* @__PURE__ */ jsx("div", {
440
452
  className: "flex flex-col w-full",
441
453
  children: /* @__PURE__ */ jsx(HeaderElement, {
442
454
  ...!hideHeader && {
@@ -449,21 +461,30 @@ var FormField = React.memo(function FormField({ optionsField, fieldName, values,
449
461
  },
450
462
  children: /* @__PURE__ */ jsx("div", {
451
463
  className: "w-full",
452
- children: /* @__PURE__ */ jsx(FieldsFromOptions, {
453
- options: field.options ?? {},
454
- setFormDirty,
455
- setFieldValue,
456
- setFieldTouched,
457
- values,
458
- parentInfo,
459
- workflowForm,
460
- labelPosition: meta.labelPosition,
461
- spaceCompact: meta.spaceCompact,
462
- missingFields
464
+ children: /* @__PURE__ */ jsx(EditingScope, {
465
+ editing,
466
+ path,
467
+ children: /* @__PURE__ */ jsx(FieldsFromOptions, {
468
+ options: field.options ?? {},
469
+ setFormDirty,
470
+ setFieldValue,
471
+ setFieldTouched,
472
+ values,
473
+ parentInfo,
474
+ workflowForm,
475
+ labelPosition: meta.labelPosition,
476
+ spaceCompact: meta.spaceCompact,
477
+ missingFields
478
+ })
463
479
  })
464
480
  })
465
481
  })
466
482
  });
483
+ return editing && path ? /* @__PURE__ */ jsx(editing.Row, {
484
+ path,
485
+ instance: fieldObj.name,
486
+ children: group
487
+ }) : group;
467
488
  }
468
489
  if (typeof field.type === "object" && field.depends_on) {
469
490
  const parentValue = getValueUsingPath(values, field.depends_on);
@@ -489,7 +510,7 @@ var FormField = React.memo(function FormField({ optionsField, fieldName, values,
489
510
  if (Array.isArray(field.secondaryField)) fieldObj.secondaryField = field.secondaryField.map((secondaryField) => fieldNamePrefix + secondaryField);
490
511
  else fieldObj.secondaryField = fieldNamePrefix + field.secondaryField;
491
512
  }
492
- return /* @__PURE__ */ jsx("div", {
513
+ const input = /* @__PURE__ */ jsx("div", {
493
514
  className: cx("flex w-full", labelPosition === "left" ? "mb-[15px]" : "mb-[5px]"),
494
515
  children: /* @__PURE__ */ jsx(InputField, {
495
516
  field: fieldObj,
@@ -508,6 +529,17 @@ var FormField = React.memo(function FormField({ optionsField, fieldName, values,
508
529
  workflowForm
509
530
  })
510
531
  });
532
+ if (!editing || !path) return input;
533
+ const holds = fieldObj.type === "object" || fieldObj.type === "list";
534
+ return /* @__PURE__ */ jsx(editing.Row, {
535
+ path,
536
+ instance: fieldObj.name,
537
+ children: /* @__PURE__ */ jsx(EditingScope, {
538
+ editing,
539
+ path: holds ? path : null,
540
+ children: input
541
+ })
542
+ });
511
543
  }, (prev, next) => {
512
544
  if (!deepEqual(prev.optionsField, next.optionsField)) return false;
513
545
  if (prev.values !== next.values) return false;
@@ -519,6 +551,7 @@ var FormField = React.memo(function FormField({ optionsField, fieldName, values,
519
551
  return true;
520
552
  });
521
553
  function FieldsFromOptions({ options = {}, values, setFormDirty, setFieldValue, setFieldTouched, parentInfo, labelPosition = "left", missingFields = [], spaceCompact = false, workflowForm = false }) {
554
+ const editing = useFormEditing();
522
555
  const chosen = useChosenLabelPosition(options);
523
556
  const wizard = parseWizardConfig(options);
524
557
  if (wizard) return /* @__PURE__ */ jsx(ChosenLabelPosition.Provider, {
@@ -556,22 +589,27 @@ function FieldsFromOptions({ options = {}, values, setFormDirty, setFieldValue,
556
589
  spaceCompact,
557
590
  workflowForm
558
591
  }, fieldName);
592
+ const add = editing ? /* @__PURE__ */ jsx(editing.Add, { parent: editing.parent }) : null;
559
593
  const layout = resolveFormLayout(asRecord(options["$meta"])["layout"], names.filter((name) => !name.startsWith("$") && asSchemaField(options[name])));
560
594
  if (layout.layout) return /* @__PURE__ */ jsx(ChosenLabelPosition.Provider, {
561
595
  value: chosen,
562
596
  children: /* @__PURE__ */ jsxs("div", {
563
597
  className: "flex min-w-0 flex-col gap-4",
564
- children: [/* @__PURE__ */ jsx(FormLayout, {
565
- layout: layout.layout,
566
- renderField: (name) => fieldOf(name, true)
567
- }), layout.remaining.map((name) => fieldOf(name, true))]
598
+ children: [
599
+ /* @__PURE__ */ jsx(FormLayout, {
600
+ layout: layout.layout,
601
+ renderField: (name) => fieldOf(name, true)
602
+ }),
603
+ layout.remaining.map((name) => fieldOf(name, true)),
604
+ add
605
+ ]
568
606
  })
569
607
  });
570
608
  return /* @__PURE__ */ jsx(ChosenLabelPosition.Provider, {
571
609
  value: chosen,
572
- children: flows ? /* @__PURE__ */ jsx("div", {
610
+ children: flows ? /* @__PURE__ */ jsxs("div", {
573
611
  className: "@container/inputs -mx-2 flex flex-wrap items-start",
574
- children: columns.map((column) => {
612
+ children: [columns.map((column) => {
575
613
  const width = inputWidth(asRecord(options[column.head])["width"]);
576
614
  return /* @__PURE__ */ jsx("div", {
577
615
  "data-input-cell": true,
@@ -587,8 +625,11 @@ function FieldsFromOptions({ options = {}, values, setFormDirty, setFieldValue,
587
625
  }, name);
588
626
  })
589
627
  }, column.head);
590
- })
591
- }) : names.map((name) => fieldOf(name))
628
+ }), add && /* @__PURE__ */ jsx("div", {
629
+ className: "order-2 w-full px-2",
630
+ children: add
631
+ })]
632
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [names.map((name) => fieldOf(name, void 0)), add] })
592
633
  });
593
634
  }
594
635
  var noop = () => {};
@@ -2,16 +2,21 @@ import { useStrings, useWorkflowEngine } from "../../components/Provider.js";
2
2
  import { copyBounds, stepText } from "./utils.js";
3
3
  import { initializeValues } from "../lib.js";
4
4
  import { getValueUsingPath } from "../utils/getValueUsingPath.js";
5
+ import { useFormEditing } from "../formEditing.js";
5
6
  import { useWizardState } from "./useWizardState.js";
6
7
  import { WizardNavigation } from "./WizardNavigation.js";
7
8
  import { WizardStepContent } from "./WizardStepContent.js";
8
9
  import { WizardStepIndicator } from "./WizardStepIndicator.js";
9
- import { useCallback, useEffect, useMemo, useRef } from "react";
10
+ import { WizardPagesContext } from "./wizardPages.js";
11
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
10
12
  import { jsx, jsxs } from "react/jsx-runtime";
11
13
  import { useFormikContext } from "formik";
12
14
  //#region src/form/Wizard/WizardContainer.tsx
13
15
  function WizardContainer({ wizardConfig, values, onChange, onSubmit, className = "", labelPosition = "left", missingFields = [], spaceCompact = false, workflowForm = false, nested = false, fieldNamePrefix = "", setFieldValue, setFieldTouched }) {
14
16
  const { validateForm, setTouched, touched, initialValues } = useFormikContext();
17
+ const editing = useFormEditing();
18
+ const pages = useContext(WizardPagesContext);
19
+ const [ownPage, setOwnPage] = useState(0);
15
20
  const { form: strings } = useStrings();
16
21
  const engine = useWorkflowEngine();
17
22
  const { config, steps, stepOrder } = wizardConfig;
@@ -197,19 +202,39 @@ function WizardContainer({ wizardConfig, values, onChange, onSubmit, className =
197
202
  touched,
198
203
  onSubmit
199
204
  ]);
205
+ const last = Math.max(shown.order.length - 1, 0);
206
+ const wizardKey = editing ? JSON.stringify(editing.parent) : "";
207
+ const editIndex = Math.min(Math.max(pages ? pages.page(wizardKey) : ownPage, 0), last);
208
+ const turn = (to) => {
209
+ const next = Math.max(to, 0);
210
+ if (pages) pages.setPage(wizardKey, next);
211
+ else setOwnPage(next);
212
+ };
213
+ const drawn = shown.order.length;
214
+ const setCount = pages?.setCount;
215
+ useEffect(() => {
216
+ setCount?.(wizardKey, drawn);
217
+ }, [
218
+ setCount,
219
+ wizardKey,
220
+ drawn
221
+ ]);
200
222
  const shownBefore = useRef(shown.order);
201
223
  useEffect(() => {
202
224
  shownBefore.current = shown.order;
203
225
  });
204
- const pageKey = shown.byKey[currentStep] ? currentStep : nearestPage(currentStep, shown.order, shownBefore.current);
226
+ const runKey = shown.byKey[currentStep] ? currentStep : nearestPage(currentStep, shown.order, shownBefore.current);
205
227
  useEffect(() => {
206
- if (pageKey && pageKey !== currentStep) showStep(pageKey);
228
+ if (!editing && runKey && runKey !== currentStep) showStep(runKey);
207
229
  }, [
208
- pageKey,
230
+ editing,
231
+ runKey,
209
232
  currentStep,
210
233
  showStep
211
234
  ]);
235
+ const pageKey = editing ? shown.order[editIndex] ?? "" : runKey;
212
236
  const page = shown.byKey[pageKey];
237
+ const show = (key, index) => editing ? turn(index) : showStep(key);
213
238
  const copiesOf = (step) => {
214
239
  const rows = getValueUsingPath(values, `${fieldNamePrefix}${step}`);
215
240
  return Array.isArray(rows) ? rows : [];
@@ -220,13 +245,15 @@ function WizardContainer({ wizardConfig, values, onChange, onSubmit, className =
220
245
  const kept = Array.from({ length: shownPage.count ?? rows.length }, (_, i) => rows[i] ?? initializeValues(options, {}) ?? {});
221
246
  const aside = setAside.current[`${fieldNamePrefix}${shownPage.step}`] ?? [];
222
247
  setFieldValue(`${fieldNamePrefix}${shownPage.step}`, [...kept, takeAside(aside, kept.length) ?? initializeValues(options, {}) ?? {}]);
223
- showStep(`${shownPage.step}[${kept.length}]`);
248
+ const at = shown.order.indexOf(pageKey) + (shownPage.copy === void 0 ? 0 : 1);
249
+ show(`${shownPage.step}[${kept.length}]`, at);
224
250
  };
225
251
  const removeCopy = (shownPage) => {
226
252
  const copy = shownPage.copy ?? 0;
227
253
  const rows = copiesOf(shownPage.step).filter((_, i) => i !== copy);
228
254
  setFieldValue(`${fieldNamePrefix}${shownPage.step}`, rows);
229
- showStep(rows.length > 0 ? `${shownPage.step}[${Math.max(copy - 1, 0)}]` : shownPage.step);
255
+ const at = shown.order.indexOf(pageKey) - (copy > 0 ? 1 : 0);
256
+ show(rows.length > 0 ? `${shownPage.step}[${Math.max(copy - 1, 0)}]` : shownPage.step, at);
230
257
  };
231
258
  const rawTitle = page ? steps[page.step]?.title : void 0;
232
259
  const titledPerCopy = Array.isArray(rawTitle) || typeof rawTitle === "string" && rawTitle.includes("${{");
@@ -264,6 +291,39 @@ function WizardContainer({ wizardConfig, values, onChange, onSubmit, className =
264
291
  setFieldTouched
265
292
  }), copies]
266
293
  }) : null;
294
+ if (editing) return /* @__PURE__ */ jsxs("div", {
295
+ className,
296
+ children: [
297
+ navigation.showSteps !== false && /* @__PURE__ */ jsx(WizardStepIndicator, {
298
+ stepOrder: shown.order,
299
+ currentStep: pageKey,
300
+ steps: configs,
301
+ visitedSteps: new Set(shown.order),
302
+ invalidSteps: /* @__PURE__ */ new Set(),
303
+ onStepClick: (key) => {
304
+ turn(shown.order.indexOf(key));
305
+ return true;
306
+ },
307
+ allowJump: true,
308
+ hideStepNumbers: navigation.hideStepNumbers
309
+ }),
310
+ content,
311
+ /* @__PURE__ */ jsx(WizardNavigation, {
312
+ isLastStep: editIndex === last,
313
+ canGoBack: editIndex > 0,
314
+ isCurrentStepValid: true,
315
+ nextLabel: page?.config.nextLabel,
316
+ prevLabel: page?.config.prevLabel,
317
+ submitLabel: config.submitLabel || "Execute",
318
+ onNext: async () => {
319
+ turn(editIndex + 1);
320
+ return true;
321
+ },
322
+ onPrevious: () => turn(editIndex - 1),
323
+ onSubmit: nested ? void 0 : async () => {}
324
+ })
325
+ ]
326
+ });
267
327
  return /* @__PURE__ */ jsxs("div", {
268
328
  className,
269
329
  children: [
@@ -1,9 +1,12 @@
1
1
  import { stepText } from "./utils.js";
2
+ import { EditingScope, useFormEditing } from "../formEditing.js";
2
3
  import { FieldsFromOptions } from "../Form.js";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  //#region src/form/Wizard/WizardStepContent.tsx
5
6
  function WizardStepContent({ currentStep, stepConfig, flatten = true, values, setFormDirty, setFieldValue, setFieldTouched, fieldNamePrefix = "", copy, ...fieldsProps }) {
7
+ const editing = useFormEditing();
6
8
  if (!stepConfig.options) return null;
9
+ const path = editing ? [...editing.parent, currentStep] : null;
7
10
  const parentInfo = copy !== void 0 ? {
8
11
  parentName: currentStep,
9
12
  fieldNamePrefix: `${fieldNamePrefix}${currentStep}[${copy}].`,
@@ -15,7 +18,7 @@ function WizardStepContent({ currentStep, stepConfig, flatten = true, values, se
15
18
  parentName: fieldNamePrefix.slice(0, -1),
16
19
  fieldNamePrefix
17
20
  } : void 0;
18
- return /* @__PURE__ */ jsxs("div", {
21
+ const content = /* @__PURE__ */ jsxs("div", {
19
22
  className: "mb-4 w-full",
20
23
  children: [
21
24
  stepText(stepConfig.title) && /* @__PURE__ */ jsx("h2", {
@@ -28,17 +31,27 @@ function WizardStepContent({ currentStep, stepConfig, flatten = true, values, se
28
31
  style: { color: "var(--theme-muted-text-color)" },
29
32
  children: stepText(stepConfig.description)
30
33
  }),
31
- /* @__PURE__ */ jsx(FieldsFromOptions, {
32
- options: stepConfig.options,
33
- values,
34
- setFormDirty,
35
- setFieldValue,
36
- setFieldTouched,
37
- ...parentInfo ? { parentInfo } : {},
38
- ...fieldsProps
39
- }, copy === void 0 ? currentStep : `${currentStep}[${copy}]`)
34
+ /* @__PURE__ */ jsx(EditingScope, {
35
+ editing,
36
+ path,
37
+ children: /* @__PURE__ */ jsx(FieldsFromOptions, {
38
+ options: stepConfig.options,
39
+ values,
40
+ setFormDirty,
41
+ setFieldValue,
42
+ setFieldTouched,
43
+ ...parentInfo ? { parentInfo } : {},
44
+ ...fieldsProps
45
+ }, copy === void 0 ? currentStep : `${currentStep}[${copy}]`)
46
+ })
40
47
  ]
41
48
  });
49
+ const instance = `${fieldNamePrefix}${currentStep}${copy === void 0 ? "" : `[${copy}]`}`;
50
+ return editing && path ? /* @__PURE__ */ jsx(editing.Row, {
51
+ path,
52
+ instance,
53
+ children: content
54
+ }) : content;
42
55
  }
43
56
  //#endregion
44
57
  export { WizardStepContent };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The page each wizard of a form being edited shows, keyed by its list's path as JSON (`[]` for the
3
+ * form's own), so the editor can turn its pages as well as the wizard's own controls.
4
+ */
5
+ export interface WizardPages {
6
+ page: (wizard: string) => number;
7
+ setPage: (wizard: string, index: number) => void;
8
+ /** How many pages the wizard draws, a repeated page's copies included, once it has drawn. */
9
+ count: (wizard: string) => number | undefined;
10
+ setCount: (wizard: string, count: number) => void;
11
+ }
12
+ export declare const WizardPagesContext: import("react").Context<WizardPages | null>;
@@ -0,0 +1,5 @@
1
+ import { createContext } from "react";
2
+ //#region src/form/Wizard/wizardPages.ts
3
+ var WizardPagesContext = createContext(null);
4
+ //#endregion
5
+ export { WizardPagesContext };
@@ -0,0 +1,25 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ /** Editing controls a form builder wraps around rendered fields. */
3
+ export interface FormEditing {
4
+ /** Input path of the fields being rendered; empty at the top level. */
5
+ parent: string[];
6
+ Row: ComponentType<{
7
+ path: string[];
8
+ /** The field isn't drawn, such as one marked hidden. */
9
+ hidden?: boolean;
10
+ /** The field's name in the form's values, which tells one input's copies in a list's rows apart. */
11
+ instance?: string;
12
+ children?: ReactNode;
13
+ }>;
14
+ Add: ComponentType<{
15
+ parent: string[];
16
+ }>;
17
+ }
18
+ export declare const FormEditingContext: import("react").Context<FormEditing | null>;
19
+ export declare function useFormEditing(): FormEditing | null;
20
+ /** Fields rendered inside `children` belong to the input at `path`. */
21
+ export declare function EditingScope({ editing, path, children, }: {
22
+ editing: FormEditing | null;
23
+ path: string[] | null;
24
+ children: ReactNode;
25
+ }): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/form/formEditing.tsx
4
+ var FormEditingContext = createContext(null);
5
+ function useFormEditing() {
6
+ return useContext(FormEditingContext);
7
+ }
8
+ /** Fields rendered inside `children` belong to the input at `path`. */
9
+ function EditingScope({ editing, path, children }) {
10
+ if (!editing) return children;
11
+ return /* @__PURE__ */ jsx(FormEditingContext.Provider, {
12
+ value: path ? {
13
+ ...editing,
14
+ parent: path
15
+ } : null,
16
+ children
17
+ });
18
+ }
19
+ //#endregion
20
+ export { EditingScope, FormEditingContext, useFormEditing };