@alpaca-editor/core 1.0.4103 → 1.0.4104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/components/ui/context-menu.d.ts +1 -1
  2. package/dist/components/ui/context-menu.js +8 -8
  3. package/dist/components/ui/context-menu.js.map +1 -1
  4. package/dist/config/config.js +8 -1
  5. package/dist/config/config.js.map +1 -1
  6. package/dist/config/types.d.ts +1 -0
  7. package/dist/editor/ContentTree.js +15 -16
  8. package/dist/editor/ContentTree.js.map +1 -1
  9. package/dist/editor/ContextMenu.js +37 -6
  10. package/dist/editor/ContextMenu.js.map +1 -1
  11. package/dist/editor/MainLayout.js +1 -1
  12. package/dist/editor/MainLayout.js.map +1 -1
  13. package/dist/editor/ai/AgentHistory.js +1 -1
  14. package/dist/editor/ai/AgentTerminal.js +187 -13
  15. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  16. package/dist/editor/ai/AiResponseMessage.d.ts +1 -1
  17. package/dist/editor/ai/types.d.ts +25 -0
  18. package/dist/editor/ai/types.js +2 -0
  19. package/dist/editor/ai/types.js.map +1 -0
  20. package/dist/editor/client/EditorShell.js +29 -0
  21. package/dist/editor/client/EditorShell.js.map +1 -1
  22. package/dist/editor/client/editContext.d.ts +3 -0
  23. package/dist/editor/client/editContext.js.map +1 -1
  24. package/dist/editor/commands/agentCommands.d.ts +9 -0
  25. package/dist/editor/commands/agentCommands.js +30 -0
  26. package/dist/editor/commands/agentCommands.js.map +1 -0
  27. package/dist/editor/commands/itemCommands.js +14 -14
  28. package/dist/editor/commands/itemCommands.js.map +1 -1
  29. package/dist/editor/component-designer/aiContext.d.ts +1 -1
  30. package/dist/editor/context-menu/InsertMenu.d.ts +2 -1
  31. package/dist/editor/context-menu/InsertMenu.js +20 -15
  32. package/dist/editor/context-menu/InsertMenu.js.map +1 -1
  33. package/dist/editor/field-types/NameValueListEditor.d.ts +7 -0
  34. package/dist/editor/field-types/NameValueListEditor.js +99 -0
  35. package/dist/editor/field-types/NameValueListEditor.js.map +1 -0
  36. package/dist/editor/fieldTypes.d.ts +1 -1
  37. package/dist/editor/page-editor-chrome/FrameMenu.js +47 -10
  38. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  39. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +3 -1
  40. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +30 -4
  41. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  42. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +44 -5
  43. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
  44. package/dist/editor/services/agentService.d.ts +22 -2
  45. package/dist/editor/services/agentService.js +26 -1
  46. package/dist/editor/services/agentService.js.map +1 -1
  47. package/dist/editor/services/aiService.d.ts +2 -1
  48. package/dist/editor/services/aiService.js.map +1 -1
  49. package/dist/editor/sidebar/GraphQL.js +47 -90
  50. package/dist/editor/sidebar/GraphQL.js.map +1 -1
  51. package/dist/editor/ui/DragPreview.d.ts +13 -0
  52. package/dist/editor/ui/DragPreview.js +35 -0
  53. package/dist/editor/ui/DragPreview.js.map +1 -0
  54. package/dist/editor/ui/ItemNameDialogNew.js +2 -2
  55. package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
  56. package/dist/editor/ui/PerfectTree.js +3 -15
  57. package/dist/editor/ui/PerfectTree.js.map +1 -1
  58. package/dist/revision.d.ts +2 -2
  59. package/dist/revision.js +2 -2
  60. package/dist/styles.css +37 -9
  61. package/dist/tour/default-tour.js +34 -38
  62. package/dist/tour/default-tour.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/components/ui/context-menu.tsx +31 -19
  65. package/src/config/config.tsx +9 -1
  66. package/src/config/types.ts +1 -0
  67. package/src/editor/ContentTree.tsx +13 -18
  68. package/src/editor/ContextMenu.tsx +112 -19
  69. package/src/editor/MainLayout.tsx +1 -1
  70. package/src/editor/ai/AgentHistory.tsx +2 -2
  71. package/src/editor/ai/AgentTerminal.tsx +226 -15
  72. package/src/editor/ai/AiResponseMessage.tsx +1 -1
  73. package/src/editor/ai/types.ts +27 -0
  74. package/src/editor/client/EditorShell.tsx +31 -0
  75. package/src/editor/client/editContext.ts +12 -1
  76. package/src/editor/commands/agentCommands.tsx +49 -0
  77. package/src/editor/commands/itemCommands.tsx +26 -14
  78. package/src/editor/component-designer/aiContext.ts +1 -1
  79. package/src/editor/context-menu/InsertMenu.tsx +64 -39
  80. package/src/editor/field-types/NameValueListEditor.tsx +197 -0
  81. package/src/editor/fieldTypes.ts +1 -1
  82. package/src/editor/page-editor-chrome/FrameMenu.tsx +61 -13
  83. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +82 -20
  84. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +77 -24
  85. package/src/editor/services/agentService.ts +53 -2
  86. package/src/editor/services/aiService.ts +3 -1
  87. package/src/editor/sidebar/GraphQL.tsx +50 -99
  88. package/src/editor/ui/DragPreview.tsx +44 -0
  89. package/src/editor/ui/ItemNameDialogNew.tsx +7 -3
  90. package/src/editor/ui/PerfectTree.tsx +2 -17
  91. package/src/revision.ts +2 -2
  92. package/src/tour/default-tour.tsx +34 -46
  93. package/dist/editor/ai/AiTerminal.d.ts +0 -47
  94. package/dist/editor/ai/AiTerminal.js +0 -300
  95. package/dist/editor/ai/AiTerminal.js.map +0 -1
  96. package/src/editor/ai/AiTerminal.tsx +0 -570
  97. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx_ +0 -11
@@ -1621,6 +1621,16 @@ export function EditorShell({
1621
1621
 
1622
1622
  const editContext = useMemo<EditContextType>(() => {
1623
1623
  // console.log('🔄 EditContext useMemo is being recalculated');
1624
+ // Simple registry for per-view context factories (e.g., GraphQL context)
1625
+ // Stored outside the EditContext object via ref to avoid re-renders
1626
+ if (!(globalThis as any).__editorContextFactoriesRef) {
1627
+ (globalThis as any).__editorContextFactoriesRef = {
1628
+ map: new Map<string, () => Promise<any> | any>(),
1629
+ };
1630
+ }
1631
+ const factoriesRef = (globalThis as any).__editorContextFactoriesRef as {
1632
+ map: Map<string, () => Promise<any> | any>;
1633
+ };
1624
1634
  const context = {
1625
1635
  operations: operationsContext.ops,
1626
1636
  itemsRepository,
@@ -2069,6 +2079,27 @@ export function EditorShell({
2069
2079
  setCurrentWizardId,
2070
2080
  favorites,
2071
2081
  loadFavorites,
2082
+ // Context factory registry methods
2083
+ registerContextFactory: (
2084
+ name: string,
2085
+ factory: () => Promise<any> | any,
2086
+ ) => {
2087
+ try {
2088
+ factoriesRef.map.set(name, factory);
2089
+ } catch {}
2090
+ },
2091
+ unregisterContextFactory: (name: string) => {
2092
+ try {
2093
+ factoriesRef.map.delete(name);
2094
+ } catch {}
2095
+ },
2096
+ getContextFactory: (name: string) => {
2097
+ try {
2098
+ return factoriesRef.map.get(name);
2099
+ } catch {
2100
+ return undefined;
2101
+ }
2102
+ },
2072
2103
  };
2073
2104
 
2074
2105
  return context as unknown as EditContextType;
@@ -176,7 +176,9 @@ export type EditContextType = {
176
176
  showToast: (message: string) => void;
177
177
  sessionId: string;
178
178
  openSplashScreen: () => void;
179
- getComponentCommands: (components: Component[]) => Promise<ComponentCommand[]>;
179
+ getComponentCommands: (
180
+ components: Component[],
181
+ ) => Promise<ComponentCommand[]>;
180
182
  selectMedia: ({
181
183
  selectedIdPath,
182
184
  mode,
@@ -372,6 +374,15 @@ export type EditContextType = {
372
374
  loadFavorites: () => Promise<void>;
373
375
  currentWizardId: string | null;
374
376
  setCurrentWizardId: (wizardId: string | null) => void;
377
+
378
+ // Context factory registry (optional): panels can register named factories
379
+ // that produce structured context objects for agent prompts.
380
+ registerContextFactory?: (
381
+ name: string,
382
+ factory: () => Promise<any> | any,
383
+ ) => void;
384
+ unregisterContextFactory?: (name: string) => void;
385
+ getContextFactory?: (name: string) => (() => Promise<any> | any) | undefined;
375
386
  };
376
387
 
377
388
  const EditContext = React.createContext<EditContextType | undefined>(undefined);
@@ -0,0 +1,49 @@
1
+ import React from "react";
2
+ import { Command, CommandContext } from "./commands";
3
+ import { Wand2 } from "lucide-react";
4
+
5
+ export type CreateAgentCommandData = {
6
+ profileName?: string;
7
+ profileId?: string;
8
+ contextFactory?: string;
9
+ initialPrompt?: string;
10
+ additionalData?: Record<string, any>;
11
+ };
12
+
13
+ export const createAgentCommand: Command<CreateAgentCommandData> = {
14
+ id: "createAgent",
15
+ label: "Start Agent",
16
+ icon: <Wand2 strokeWidth={1} className="text-violet-600" />,
17
+ execute: async (context: CommandContext<CreateAgentCommandData>) => {
18
+ const edit = context.editContext;
19
+ try {
20
+ edit.setShowAgentsPanel?.(true);
21
+
22
+ const {
23
+ profileName,
24
+ profileId,
25
+ contextFactory,
26
+ initialPrompt,
27
+ additionalData,
28
+ } = context.data || {};
29
+
30
+ const metadata = {
31
+ profile: profileName,
32
+ additionalData: {
33
+ ...(additionalData || {}),
34
+ profileName,
35
+ profileId,
36
+ contextFactory,
37
+ initialPrompt,
38
+ },
39
+ } as any;
40
+
41
+ window.dispatchEvent(
42
+ new CustomEvent("editor:addNewAgent", { detail: { metadata } }),
43
+ );
44
+ } catch (e) {
45
+ console.error("Failed to start agent", e);
46
+ }
47
+ },
48
+ disabled: () => false,
49
+ };
@@ -16,7 +16,19 @@ import {
16
16
  } from "../ui/CopyMoveTargetSelectorDialog";
17
17
  import { defaultTranslateAll } from "./localizeItem/LocalizeItemUtils";
18
18
  import { TranslationStatus } from "../../config/types";
19
- import { TriangleAlert } from "lucide-react";
19
+ import {
20
+ TriangleAlert,
21
+ Trash2,
22
+ Pencil,
23
+ Plus,
24
+ ArrowRight,
25
+ Copy,
26
+ UploadCloud,
27
+ Download,
28
+ Upload,
29
+ Globe,
30
+ CopyPlus,
31
+ } from "lucide-react";
20
32
 
21
33
  export type ItemCommandData = CommandData & {
22
34
  items: FullItem[];
@@ -29,7 +41,7 @@ export type ItemCommand = Command<ItemCommandData>;
29
41
  export const deleteItemCommand: ItemCommand = {
30
42
  id: "deleteItem",
31
43
  label: "Delete",
32
- icon: "pi pi-times",
44
+ icon: <Trash2 strokeWidth={1} />,
33
45
  disabled: (context: ItemCommandContext) =>
34
46
  !context.data?.items || !context.data?.items[0]?.canDelete || false,
35
47
  execute: async (context: ItemCommandContext) => {
@@ -81,7 +93,7 @@ export const deleteItemCommand: ItemCommand = {
81
93
  export const renameItemCommand: ItemCommand = {
82
94
  id: "renameItem",
83
95
  label: "Rename",
84
- icon: "pi pi-pencil",
96
+ icon: <Pencil strokeWidth={1} />,
85
97
  keyBinding: "F2",
86
98
  disabled: (context: ItemCommandContext) =>
87
99
  !context.data?.items ||
@@ -118,7 +130,7 @@ export type InsertItemCommand = Command<InsertItemCommandData>;
118
130
  export const insertItemCommand: ItemCommand = {
119
131
  id: "insertItem",
120
132
  label: "Insert Item",
121
- icon: "pi pi-plus",
133
+ icon: <Plus strokeWidth={1} />,
122
134
  disabled: (context: ItemCommandContext) =>
123
135
  !context.data?.items ||
124
136
  context.data.items.length !== 1 ||
@@ -175,7 +187,7 @@ export type MoveCopyItemsCommand = Command<MoveCopyItemsCommandData>;
175
187
  export const moveItemsCommand: MoveCopyItemsCommand = {
176
188
  id: "moveItems",
177
189
  label: "Move Item(s)",
178
- icon: "pi pi-arrow-right",
190
+ icon: <ArrowRight strokeWidth={1} />,
179
191
  disabled: (context: MoveCopyItemsCommandContext) =>
180
192
  !context.data?.items ||
181
193
  context.data.items.some((x) => !x.canDelete) ||
@@ -215,7 +227,7 @@ export const moveItemsCommand: MoveCopyItemsCommand = {
215
227
  export const copyItemsCommand: MoveCopyItemsCommand = {
216
228
  id: "copyItems",
217
229
  label: "Copy Item(s)",
218
- icon: "pi pi-copy",
230
+ icon: <Copy strokeWidth={1} />,
219
231
  disabled: (context: MoveCopyItemsCommandContext) =>
220
232
  !context.data?.items || false,
221
233
  execute: async (context: MoveCopyItemsCommandContext) => {
@@ -259,7 +271,7 @@ export const copyItemsCommand: MoveCopyItemsCommand = {
259
271
  export const duplicateItemCommand: MoveCopyItemsCommand = {
260
272
  id: "duplicateItem",
261
273
  label: "Duplicate Item",
262
- icon: "pi pi-clone",
274
+ icon: <CopyPlus strokeWidth={1} />,
263
275
  disabled: (context: MoveCopyItemsCommandContext) =>
264
276
  !context.data?.items || context.data.items.length !== 1 || false,
265
277
  execute: async (context: MoveCopyItemsCommandContext) => {
@@ -316,7 +328,7 @@ export type LocalizeItemCommand = Command<LocalizeItemCommandData>;
316
328
  export const localizeItemCommand: LocalizeItemCommand = {
317
329
  id: "localizeItem",
318
330
  label: "Localize",
319
- icon: "pi pi-globe",
331
+ icon: <Globe strokeWidth={1} />,
320
332
  disabled: (context: LocalizeItemCommandContext) =>
321
333
  !context.data?.items || context.data.items.length === 0 || false,
322
334
  execute: async (context: LocalizeItemCommandContext) => {
@@ -344,7 +356,7 @@ export const localizeItemCommand: LocalizeItemCommand = {
344
356
  export const publishItemCommand: ItemCommand = {
345
357
  id: "publishItem",
346
358
  label: "Publish",
347
- icon: "pi pi-cloud-upload",
359
+ icon: <UploadCloud strokeWidth={1} />,
348
360
  disabled: (context: ItemCommandContext) =>
349
361
  !context.data?.items ||
350
362
  context.data.items.length !== 1 ||
@@ -360,7 +372,7 @@ export const publishItemCommand: ItemCommand = {
360
372
  export const exportItemsCommand: ItemCommand = {
361
373
  id: "exportItem",
362
374
  label: "Export",
363
- icon: "pi pi-download",
375
+ icon: <Download strokeWidth={1} />,
364
376
  disabled: (context: ItemCommandContext) =>
365
377
  !context.data?.items || context.data.items.length === 0 || false,
366
378
  execute: async (context: ItemCommandContext) => {
@@ -393,7 +405,7 @@ export const exportItemsCommand: ItemCommand = {
393
405
  </div>
394
406
  ),
395
407
  header: "Export Items",
396
- icon: "pi pi-download",
408
+ icon: <Download strokeWidth={1} />,
397
409
  accept: () => {
398
410
  const language =
399
411
  (document.getElementById("export-language") as HTMLSelectElement)
@@ -456,7 +468,7 @@ export const exportItemsCommand: ItemCommand = {
456
468
  </div>
457
469
  ),
458
470
  header: "Export Results",
459
- icon: "pi pi-copy",
471
+ icon: <Copy strokeWidth={1} />,
460
472
  accept: () => {},
461
473
  rejectLabel: "Close",
462
474
  showCancel: false,
@@ -478,7 +490,7 @@ export const exportItemsCommand: ItemCommand = {
478
490
  export const importItemsCommand: ItemCommand = {
479
491
  id: "importItems",
480
492
  label: "Import",
481
- icon: "pi pi-upload",
493
+ icon: <Upload strokeWidth={1} />,
482
494
  disabled: (context: ItemCommandContext) =>
483
495
  !context.data?.items ||
484
496
  context.data.items.length !== 1 ||
@@ -547,7 +559,7 @@ export const importItemsCommand: ItemCommand = {
547
559
  context.editContext.confirm({
548
560
  message: dialogContent,
549
561
  header: "Import Items",
550
- icon: "pi pi-upload",
562
+ icon: <Upload strokeWidth={1} />,
551
563
  accept: () => {
552
564
  const textarea = document.getElementById(
553
565
  "yaml-input",
@@ -1,4 +1,4 @@
1
- import { AiContext } from "../ai/AiTerminal";
1
+ import { AiContext } from "../ai/types";
2
2
  //import { configuration } from "../config/config";
3
3
  import { EditContextType } from "../client/editContext";
4
4
  import { getItemDescriptor } from "../utils";
@@ -9,15 +9,18 @@ import { SimpleTabs, Tab } from "../ui/SimpleTabs";
9
9
  import ItemSearch, { ResultItem } from "../ui/ItemSearch";
10
10
  import { templatesRootItemId } from "../../config/config";
11
11
  import { Wizard } from "../../page-wizard/PageWizard";
12
+ import { ArrowRight, Plus } from "lucide-react";
12
13
 
13
14
  export const InsertMenuTemplate = ({
14
15
  insertOptions,
15
16
  item,
16
17
  commandCallback,
18
+ mode = "overlay",
17
19
  }: {
18
20
  insertOptions: ItemTreeNodeData[];
19
21
  item: FullItem;
20
22
  commandCallback?: (command: ItemCommand, result: any) => void;
23
+ mode?: "overlay" | "inline";
21
24
  }) => {
22
25
  const editContext = useEditContext();
23
26
  const [filter, setFilter] = useState("");
@@ -155,7 +158,6 @@ export const InsertMenuTemplate = ({
155
158
  id: x.id,
156
159
  icon: (
157
160
  <img
158
- className="p-menuitem-icon"
159
161
  src={x.icon}
160
162
  style={{ height: "16px" }}
161
163
  width="16"
@@ -171,38 +173,47 @@ export const InsertMenuTemplate = ({
171
173
  label: "Options",
172
174
  content: (
173
175
  <div className="flex h-full flex-col gap-2">
174
- <FilterInput
175
- ref={filterRef}
176
- className="w-full text-sm"
177
- placeholder="Filter"
178
- value={filter}
179
- onChange={setFilter}
180
- />
181
- <div className="relative flex-1">
182
- <div className="absolute inset-0 overflow-auto">
183
- {options
184
- ?.filter(
185
- (x) =>
186
- x.label.toLowerCase().indexOf(filter.toLowerCase()) > -1,
187
- )
188
- .map((option: any) => (
189
- <div
190
- key={option.id}
191
- className="flex cursor-pointer items-center gap-2 p-1 text-xs hover:bg-gray-100"
192
- onClick={(ev) => {
193
- ev.stopPropagation();
194
- ev.preventDefault();
195
- option.command(ev);
196
- }}
197
- >
198
- {option.icon}
199
- <span className="flex-1">
200
- {highlightMatch(option.label, filter)}
201
- </span>
202
- </div>
203
- ))}
176
+ {options ? (
177
+ <>
178
+ <FilterInput
179
+ ref={filterRef}
180
+ className="w-full text-xs"
181
+ placeholder="Filter"
182
+ value={filter}
183
+ onChange={setFilter}
184
+ />
185
+ <div className="relative flex-1">
186
+ <div className="absolute inset-0 overflow-auto">
187
+ {options
188
+ .filter(
189
+ (x) =>
190
+ x.label.toLowerCase().indexOf(filter.toLowerCase()) >
191
+ -1,
192
+ )
193
+ .map((option: any) => (
194
+ <div
195
+ key={option.id}
196
+ className="flex cursor-pointer items-center gap-2 p-1 text-xs hover:bg-gray-100"
197
+ onClick={(ev) => {
198
+ ev.stopPropagation();
199
+ ev.preventDefault();
200
+ option.command(ev);
201
+ }}
202
+ >
203
+ {option.icon}
204
+ <span className="flex-1">
205
+ {highlightMatch(option.label, filter)}
206
+ </span>
207
+ </div>
208
+ ))}
209
+ </div>
210
+ </div>
211
+ </>
212
+ ) : (
213
+ <div className="p-2 text-xs text-gray-500">
214
+ No options available.
204
215
  </div>
205
- </div>
216
+ )}
206
217
  </div>
207
218
  ),
208
219
  id: "options",
@@ -212,7 +223,7 @@ export const InsertMenuTemplate = ({
212
223
  content: (
213
224
  <div className="flex h-full flex-col">
214
225
  {recentItems.length === 0 ? (
215
- <div className="p-2 text-sm text-gray-500">No recent templates</div>
226
+ <div className="p-2 text-xs text-gray-500">No recent templates</div>
216
227
  ) : (
217
228
  recentItems.map((template) => (
218
229
  <div
@@ -225,7 +236,6 @@ export const InsertMenuTemplate = ({
225
236
  }}
226
237
  >
227
238
  <img
228
- className="p-menuitem-icon"
229
239
  src={template.icon}
230
240
  style={{ height: "16px" }}
231
241
  width="16"
@@ -311,7 +321,6 @@ export const InsertMenuTemplate = ({
311
321
  }}
312
322
  >
313
323
  <img
314
- className="p-menuitem-icon"
315
324
  src={wizard.icon}
316
325
  style={{ height: "16px" }}
317
326
  width="16"
@@ -329,9 +338,25 @@ export const InsertMenuTemplate = ({
329
338
 
330
339
  const isAnyTabLoading = isLoadingRecent || isLoadingWizards;
331
340
 
341
+ if (mode === "inline") {
342
+ return (
343
+ <div className="min-h-[380px] min-w-[450px] p-2">
344
+ <div className="flex flex-col text-xs">
345
+ <SimpleTabs
346
+ className="border-gray-3 mb-2 border-b"
347
+ tabs={tabs}
348
+ setActiveTab={setActiveTab}
349
+ activeTab={activeTab}
350
+ isLoading={isAnyTabLoading}
351
+ />
352
+ </div>
353
+ </div>
354
+ );
355
+ }
356
+
332
357
  return (
333
358
  <div
334
- className="p-2"
359
+ className="flex items-center gap-2 text-xs"
335
360
  style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
336
361
  onMouseEnter={() => {
337
362
  if (hideTimeoutRef.current) {
@@ -350,9 +375,9 @@ export const InsertMenuTemplate = ({
350
375
  ev.preventDefault();
351
376
  }}
352
377
  >
353
- <i className="pi pi-plus mr-2"></i>
354
- <span className="p-menuitem-text">Insert</span>
355
- {/* The OverlayPanel will appear on hover or click. */}
378
+ <Plus strokeWidth={1} size={16} className="text-gray-2" />
379
+ <span>Insert</span>
380
+ <ArrowRight strokeWidth={1} size={16} className="text-gray-2 ml-auto" />
356
381
  <div
357
382
  ref={opRef}
358
383
  className="absolute z-50 rounded-md bg-white p-2 shadow-lg"
@@ -0,0 +1,197 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { useEditContext } from "../client/editContext";
6
+ import { Button } from "../../components/ui/button";
7
+ import { Input } from "../../components/ui/input";
8
+ import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
9
+ import { EnhancedNameValueListField, KeyValuePair } from "../fieldTypes";
10
+
11
+ type Props = {
12
+ field: EnhancedNameValueListField;
13
+ readOnly?: boolean;
14
+ };
15
+
16
+ function parseUrlEncoded(raw?: string | null): KeyValuePair[] {
17
+ if (!raw) return [];
18
+ const parts = raw.split("&").filter((p) => p.length > 0);
19
+ const pairs: KeyValuePair[] = [];
20
+ for (const part of parts) {
21
+ const eqIndex = part.indexOf("=");
22
+ const rawKey = eqIndex >= 0 ? part.substring(0, eqIndex) : part;
23
+ const rawValue = eqIndex >= 0 ? part.substring(eqIndex + 1) : "";
24
+ // Convert + to space before decode to align with Sitecore's HttpUtility behavior
25
+ const key = decodeURIComponent(rawKey.replace(/\+/g, " "));
26
+ const value = decodeURIComponent(rawValue.replace(/\+/g, " "));
27
+ pairs.push({ key, value });
28
+ }
29
+ return pairs;
30
+ }
31
+
32
+ function toUrlEncoded(pairs: KeyValuePair[]): string {
33
+ const filtered = pairs.filter((p) => (p.key || "").trim().length > 0);
34
+ const segments = filtered.map((p) => {
35
+ const encKey = encodeURIComponent(p.key);
36
+ const encValue = encodeURIComponent(p.value || "");
37
+ // Replace %20 with + for compatibility with typical form encoding in Sitecore tools
38
+ return `${encKey.replace(/%20/g, "+")}=${encValue.replace(/%20/g, "+")}`;
39
+ });
40
+ return segments.join("&");
41
+ }
42
+
43
+ export function NameValueListEditor({ field, readOnly }: Props) {
44
+ const editContext = useEditContext();
45
+ const [pairs, setPairs] = useState<KeyValuePair[]>([]);
46
+
47
+ useEffect(() => {
48
+ // Prefer typed value, fallback to raw
49
+ const fromValue = (
50
+ Array.isArray(field.value) ? (field.value as KeyValuePair[]) : undefined
51
+ ) as KeyValuePair[] | undefined;
52
+ if (fromValue && fromValue.length >= 0) {
53
+ setPairs(
54
+ fromValue.map((x) => ({ key: x.key || "", value: x.value || "" })),
55
+ );
56
+ } else {
57
+ setPairs(parseUrlEncoded(field.rawValue));
58
+ }
59
+ }, [field.value, field.rawValue, field.descriptor.item.id, field.id]);
60
+
61
+ if (!editContext) return null;
62
+
63
+ const commit = async (next: KeyValuePair[]) => {
64
+ const cleaned = next.filter((p) => (p.key || "").trim().length > 0);
65
+ const raw = toUrlEncoded(cleaned);
66
+ setPairs(next);
67
+ await editContext.operations.editField({
68
+ field: field.descriptor,
69
+ value: cleaned,
70
+ rawValue: raw,
71
+ refresh: "waitForQuietPeriod",
72
+ });
73
+ };
74
+
75
+ const addRow = async () => {
76
+ if (readOnly) return;
77
+ const next = [...pairs, { key: "", value: "" }];
78
+ setPairs(next);
79
+ };
80
+
81
+ const updateKey = async (index: number, key: string) => {
82
+ if (readOnly) return;
83
+ const next = pairs.map((p, i) => (i === index ? { ...p, key } : p));
84
+ setPairs(next);
85
+ await commit(next);
86
+ };
87
+
88
+ const updateValue = async (index: number, value: string) => {
89
+ if (readOnly) return;
90
+ const next = pairs.map((p, i) => (i === index ? { ...p, value } : p));
91
+ setPairs(next);
92
+ await commit(next);
93
+ };
94
+
95
+ const removeRow = async (index: number) => {
96
+ if (readOnly) return;
97
+ const next = pairs.filter((_, i) => i !== index);
98
+ await commit(next);
99
+ };
100
+
101
+ const moveRow = async (index: number, offset: number) => {
102
+ if (readOnly) return;
103
+ const newIndex = index + offset;
104
+ if (newIndex < 0 || newIndex >= pairs.length) return;
105
+ const next = [...pairs];
106
+ const [item] = next.splice(index, 1);
107
+ next.splice(newIndex, 0, item);
108
+ await commit(next);
109
+ };
110
+
111
+ return (
112
+ <div
113
+ className={`focus-shadow rounded-sm border border-gray-200 ${readOnly ? "bg-gray-5" : "bg-white"}`}
114
+ >
115
+ <div className="flex items-center justify-between border-b border-gray-200 px-2 py-1.5">
116
+ <Button
117
+ type="button"
118
+ size="sm"
119
+ variant="ghost"
120
+ disabled={readOnly}
121
+ onClick={addRow}
122
+ className="h-7 px-2"
123
+ aria-label="Add entry"
124
+ >
125
+ <Plus className="h-4 w-4" strokeWidth={1} />
126
+ </Button>
127
+ </div>
128
+ <div>
129
+ {pairs.length === 0 && (
130
+ <div className="p-2 text-center text-xs text-gray-500">
131
+ No entries
132
+ </div>
133
+ )}
134
+ {pairs.map((pair, index) => (
135
+ <div
136
+ key={index}
137
+ className="flex items-center gap-1 border-b border-gray-100 p-1 last:border-b-0"
138
+ >
139
+ <Input
140
+ value={pair.key}
141
+ placeholder="Name"
142
+ disabled={readOnly}
143
+ className="bg-gray-5 h-7 p-1 text-xs"
144
+ onChange={(e) => updateKey(index, e.target.value)}
145
+ />
146
+ <span className="px-1 text-gray-400">=</span>
147
+ <Input
148
+ value={pair.value}
149
+ placeholder="Value"
150
+ disabled={readOnly}
151
+ className="bg-gray-5 h-7 p-1 text-xs"
152
+ onChange={(e) => updateValue(index, e.target.value)}
153
+ />
154
+ <div className="ml-auto flex items-center gap-0.5">
155
+ <Button
156
+ type="button"
157
+ size="icon"
158
+ variant="ghost"
159
+ disabled={readOnly || index === 0}
160
+ onClick={() => moveRow(index, -1)}
161
+ className="h-7 w-7"
162
+ aria-label="Move up"
163
+ title="Move up"
164
+ >
165
+ <ArrowUp className="h-4 w-4" strokeWidth={1} />
166
+ </Button>
167
+ <Button
168
+ type="button"
169
+ size="icon"
170
+ variant="ghost"
171
+ disabled={readOnly || index === pairs.length - 1}
172
+ onClick={() => moveRow(index, 1)}
173
+ className="h-7 w-7"
174
+ aria-label="Move down"
175
+ title="Move down"
176
+ >
177
+ <ArrowDown className="h-4 w-4" strokeWidth={1} />
178
+ </Button>
179
+ <Button
180
+ type="button"
181
+ size="icon"
182
+ variant="ghost"
183
+ disabled={readOnly}
184
+ onClick={() => removeRow(index)}
185
+ className="h-7 w-7 text-red-600 hover:text-red-700"
186
+ aria-label="Delete"
187
+ title="Delete"
188
+ >
189
+ <Trash2 className="h-4 w-4" strokeWidth={1} />
190
+ </Button>
191
+ </div>
192
+ </div>
193
+ ))}
194
+ </div>
195
+ </div>
196
+ );
197
+ }
@@ -95,7 +95,7 @@ export interface KeyValuePair {
95
95
  value: string;
96
96
  }
97
97
 
98
- export type EnhancedNameValueListField = {
98
+ export type EnhancedNameValueListField = Field & {
99
99
  value: KeyValuePair[];
100
100
  };
101
101