@alpaca-editor/core 1.0.3955 → 1.0.3959

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 (247) hide show
  1. package/build.css +1 -1
  2. package/dist/components/ui/badge.d.ts +9 -0
  3. package/dist/components/ui/badge.js +23 -0
  4. package/dist/components/ui/badge.js.map +1 -0
  5. package/dist/components/ui/button.js +3 -3
  6. package/dist/components/ui/button.js.map +1 -1
  7. package/dist/components/ui/command.d.ts +18 -0
  8. package/dist/components/ui/command.js +35 -0
  9. package/dist/components/ui/command.js.map +1 -0
  10. package/dist/components/ui/dialog.d.ts +15 -0
  11. package/dist/components/ui/dialog.js +37 -0
  12. package/dist/components/ui/dialog.js.map +1 -0
  13. package/dist/components/ui/dropdown-menu.d.ts +25 -0
  14. package/dist/components/ui/dropdown-menu.js +52 -0
  15. package/dist/components/ui/dropdown-menu.js.map +1 -0
  16. package/dist/components/ui/input.d.ts +3 -0
  17. package/dist/components/ui/input.js +7 -0
  18. package/dist/components/ui/input.js.map +1 -0
  19. package/dist/components/ui/menubar.d.ts +26 -0
  20. package/dist/components/ui/menubar.js +55 -0
  21. package/dist/components/ui/menubar.js.map +1 -0
  22. package/dist/components/ui/popover.d.ts +9 -0
  23. package/dist/components/ui/popover.js +63 -0
  24. package/dist/components/ui/popover.js.map +1 -0
  25. package/dist/components/ui/switch.d.ts +4 -0
  26. package/dist/components/ui/switch.js +9 -0
  27. package/dist/components/ui/switch.js.map +1 -0
  28. package/dist/components/ui/tooltip.d.ts +7 -0
  29. package/dist/components/ui/tooltip.js +18 -0
  30. package/dist/components/ui/tooltip.js.map +1 -0
  31. package/dist/config/config.js +79 -63
  32. package/dist/config/config.js.map +1 -1
  33. package/dist/config/types.d.ts +3 -3
  34. package/dist/editor/ContentTree.js +1 -1
  35. package/dist/editor/ContentTree.js.map +1 -1
  36. package/dist/editor/Editor.js +6 -2
  37. package/dist/editor/Editor.js.map +1 -1
  38. package/dist/editor/FieldList.js +1 -1
  39. package/dist/editor/FieldList.js.map +1 -1
  40. package/dist/editor/FieldListField.js +1 -1
  41. package/dist/editor/FieldListField.js.map +1 -1
  42. package/dist/editor/ImageEditor.js +16 -6
  43. package/dist/editor/ImageEditor.js.map +1 -1
  44. package/dist/editor/MainLayout.js +4 -4
  45. package/dist/editor/MainLayout.js.map +1 -1
  46. package/dist/editor/MobileLayout.js +3 -3
  47. package/dist/editor/MobileLayout.js.map +1 -1
  48. package/dist/editor/PictureEditor.js +29 -15
  49. package/dist/editor/PictureEditor.js.map +1 -1
  50. package/dist/editor/Titlebar.js +6 -11
  51. package/dist/editor/Titlebar.js.map +1 -1
  52. package/dist/editor/ai/GhostWriter.js +1 -1
  53. package/dist/editor/ai/GhostWriter.js.map +1 -1
  54. package/dist/editor/client/EditorClient.d.ts +4 -2
  55. package/dist/editor/client/EditorClient.js +32 -11
  56. package/dist/editor/client/EditorClient.js.map +1 -1
  57. package/dist/editor/client/editContext.d.ts +4 -1
  58. package/dist/editor/client/editContext.js.map +1 -1
  59. package/dist/editor/client/operations.js +2 -2
  60. package/dist/editor/client/pageModelBuilder.js +3 -6
  61. package/dist/editor/client/pageModelBuilder.js.map +1 -1
  62. package/dist/editor/commands/itemCommands.d.ts +2 -0
  63. package/dist/editor/commands/itemCommands.js +180 -0
  64. package/dist/editor/commands/itemCommands.js.map +1 -1
  65. package/dist/editor/field-types/MultiLineText.js +1 -1
  66. package/dist/editor/field-types/MultiLineText.js.map +1 -1
  67. package/dist/editor/field-types/SingleLineText.js +1 -1
  68. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  69. package/dist/editor/menubar/ActiveUsers.js +98 -4
  70. package/dist/editor/menubar/ActiveUsers.js.map +1 -1
  71. package/dist/editor/menubar/{ActionsMenu.d.ts → ItemActionsMenu.d.ts} +1 -1
  72. package/dist/editor/menubar/ItemActionsMenu.js +23 -0
  73. package/dist/editor/menubar/ItemActionsMenu.js.map +1 -0
  74. package/dist/editor/menubar/ItemLanguageVersion.js +2 -2
  75. package/dist/editor/menubar/ItemLanguageVersion.js.map +1 -1
  76. package/dist/editor/menubar/LanguageSelector.d.ts +1 -2
  77. package/dist/editor/menubar/LanguageSelector.js +23 -23
  78. package/dist/editor/menubar/LanguageSelector.js.map +1 -1
  79. package/dist/editor/menubar/PageSelector.js +7 -8
  80. package/dist/editor/menubar/PageSelector.js.map +1 -1
  81. package/dist/editor/menubar/PageViewerControls.js +22 -19
  82. package/dist/editor/menubar/PageViewerControls.js.map +1 -1
  83. package/dist/editor/menubar/PreviewSecondaryControls.js +2 -3
  84. package/dist/editor/menubar/PreviewSecondaryControls.js.map +1 -1
  85. package/dist/editor/menubar/User.js +1 -1
  86. package/dist/editor/menubar/User.js.map +1 -1
  87. package/dist/editor/menubar/VersionSelector.js +36 -31
  88. package/dist/editor/menubar/VersionSelector.js.map +1 -1
  89. package/dist/editor/menubar/WorkflowButton.d.ts +1 -0
  90. package/dist/editor/menubar/WorkflowButton.js +41 -0
  91. package/dist/editor/menubar/WorkflowButton.js.map +1 -0
  92. package/dist/editor/page-editor-chrome/FrameMenu.js +5 -5
  93. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  94. package/dist/editor/page-editor-chrome/SuggestionHighlightings.js +2 -2
  95. package/dist/editor/page-editor-chrome/SuggestionHighlightings.js.map +1 -1
  96. package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
  97. package/dist/editor/page-viewer/EditorForm.js +61 -49
  98. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  99. package/dist/editor/page-viewer/PageViewer.d.ts +2 -1
  100. package/dist/editor/page-viewer/PageViewer.js +28 -44
  101. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  102. package/dist/editor/page-viewer/PageViewerFrame.js +1 -1
  103. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  104. package/dist/editor/reviews/Comments.js +9 -9
  105. package/dist/editor/reviews/Comments.js.map +1 -1
  106. package/dist/editor/reviews/SuggestedEdit.js +3 -3
  107. package/dist/editor/services/contentService.d.ts +18 -0
  108. package/dist/editor/services/contentService.js +6 -0
  109. package/dist/editor/services/contentService.js.map +1 -1
  110. package/dist/editor/services/editService.d.ts +5 -0
  111. package/dist/editor/services/editService.js +4 -0
  112. package/dist/editor/services/editService.js.map +1 -1
  113. package/dist/editor/services/systemService.d.ts +2 -1
  114. package/dist/editor/services/systemService.js +4 -1
  115. package/dist/editor/services/systemService.js.map +1 -1
  116. package/dist/editor/sidebar/ComponentTree.js +26 -10
  117. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  118. package/dist/editor/sidebar/Divider.d.ts +6 -0
  119. package/dist/editor/sidebar/Divider.js +6 -0
  120. package/dist/editor/sidebar/Divider.js.map +1 -0
  121. package/dist/editor/sidebar/LeftToolbar.d.ts +1 -0
  122. package/dist/editor/sidebar/LeftToolbar.js +16 -0
  123. package/dist/editor/sidebar/LeftToolbar.js.map +1 -0
  124. package/dist/editor/sidebar/SEOInfo.d.ts +1 -0
  125. package/dist/editor/sidebar/SEOInfo.js +169 -0
  126. package/dist/editor/sidebar/SEOInfo.js.map +1 -0
  127. package/dist/editor/sidebar/Sidebar.js +1 -1
  128. package/dist/editor/sidebar/Sidebar.js.map +1 -1
  129. package/dist/editor/sidebar/SidebarView.d.ts +3 -2
  130. package/dist/editor/sidebar/SidebarView.js +22 -60
  131. package/dist/editor/sidebar/SidebarView.js.map +1 -1
  132. package/dist/editor/sidebar/ViewSelector.js +66 -20
  133. package/dist/editor/sidebar/ViewSelector.js.map +1 -1
  134. package/dist/editor/ui/Icons.d.ts +4 -0
  135. package/dist/editor/ui/Icons.js +15 -3
  136. package/dist/editor/ui/Icons.js.map +1 -1
  137. package/dist/editor/ui/Section.js +1 -1
  138. package/dist/editor/ui/Section.js.map +1 -1
  139. package/dist/editor/ui/SimpleIconButton.d.ts +1 -2
  140. package/dist/editor/ui/SimpleIconButton.js +8 -13
  141. package/dist/editor/ui/SimpleIconButton.js.map +1 -1
  142. package/dist/editor/ui/SimpleTabs.js +2 -2
  143. package/dist/editor/ui/SimpleTabs.js.map +1 -1
  144. package/dist/editor/ui/SimpleToolbar.js +1 -1
  145. package/dist/editor/ui/SimpleToolbar.js.map +1 -1
  146. package/dist/editor/ui/Splitter.d.ts +4 -0
  147. package/dist/editor/ui/Splitter.js +6 -7
  148. package/dist/editor/ui/Splitter.js.map +1 -1
  149. package/dist/editor/views/CompareView.js +16 -4
  150. package/dist/editor/views/CompareView.js.map +1 -1
  151. package/dist/editor/views/SingleEditView.d.ts +2 -1
  152. package/dist/editor/views/SingleEditView.js +2 -2
  153. package/dist/editor/views/SingleEditView.js.map +1 -1
  154. package/dist/page-wizard/steps/ContentStep.js +1 -1
  155. package/dist/page-wizard/steps/ContentStep.js.map +1 -1
  156. package/dist/revision.d.ts +2 -2
  157. package/dist/revision.js +2 -2
  158. package/dist/splash-screen/NewPage.js +8 -6
  159. package/dist/splash-screen/NewPage.js.map +1 -1
  160. package/dist/splash-screen/RecentPages.js +3 -8
  161. package/dist/splash-screen/RecentPages.js.map +1 -1
  162. package/dist/styles.css +1519 -543
  163. package/dist/tour/Tour.js +79 -10
  164. package/dist/tour/Tour.js.map +1 -1
  165. package/dist/tour/default-tour.js +55 -45
  166. package/dist/tour/default-tour.js.map +1 -1
  167. package/dist/types.d.ts +19 -1
  168. package/package.json +13 -5
  169. package/src/components/ui/badge.tsx +46 -0
  170. package/src/components/ui/button.tsx +3 -3
  171. package/src/components/ui/command.tsx +184 -0
  172. package/src/components/ui/dialog.tsx +143 -0
  173. package/src/components/ui/dropdown-menu.tsx +257 -0
  174. package/src/components/ui/input.tsx +21 -0
  175. package/src/components/ui/menubar.tsx +276 -0
  176. package/src/components/ui/popover.tsx +113 -0
  177. package/src/components/ui/switch.tsx +31 -0
  178. package/src/components/ui/tooltip.tsx +61 -0
  179. package/src/config/config.tsx +102 -65
  180. package/src/config/types.ts +3 -3
  181. package/src/editor/ContentTree.tsx +1 -1
  182. package/src/editor/Editor.tsx +8 -2
  183. package/src/editor/FieldList.tsx +2 -2
  184. package/src/editor/FieldListField.tsx +1 -1
  185. package/src/editor/ImageEditor.tsx +44 -21
  186. package/src/editor/MainLayout.tsx +21 -16
  187. package/src/editor/MobileLayout.tsx +3 -2
  188. package/src/editor/PictureEditor.tsx +74 -45
  189. package/src/editor/Titlebar.tsx +12 -24
  190. package/src/editor/ai/GhostWriter.tsx +1 -1
  191. package/src/editor/client/EditorClient.tsx +55 -13
  192. package/src/editor/client/editContext.ts +5 -0
  193. package/src/editor/client/operations.ts +2 -2
  194. package/src/editor/client/pageModelBuilder.ts +3 -7
  195. package/src/editor/commands/itemCommands.tsx +272 -0
  196. package/src/editor/field-types/MultiLineText.tsx +1 -1
  197. package/src/editor/field-types/SingleLineText.tsx +1 -1
  198. package/src/editor/menubar/ActiveUsers.tsx +271 -5
  199. package/src/editor/menubar/ItemActionsMenu.tsx +89 -0
  200. package/src/editor/menubar/ItemLanguageVersion.tsx +7 -5
  201. package/src/editor/menubar/LanguageSelector.tsx +105 -134
  202. package/src/editor/menubar/PageSelector.tsx +25 -27
  203. package/src/editor/menubar/PageViewerControls.tsx +126 -78
  204. package/src/editor/menubar/PreviewSecondaryControls.tsx +0 -2
  205. package/src/editor/menubar/User.tsx +2 -2
  206. package/src/editor/menubar/VersionSelector.tsx +124 -99
  207. package/src/editor/menubar/WorkflowButton.tsx +115 -0
  208. package/src/editor/page-editor-chrome/FrameMenu.tsx +5 -5
  209. package/src/editor/page-editor-chrome/SuggestionHighlightings.tsx +2 -2
  210. package/src/editor/page-viewer/EditorForm.tsx +112 -87
  211. package/src/editor/page-viewer/PageViewer.tsx +75 -92
  212. package/src/editor/page-viewer/PageViewerFrame.tsx +1 -1
  213. package/src/editor/reviews/Comments.tsx +19 -20
  214. package/src/editor/reviews/SuggestedEdit.tsx +3 -3
  215. package/src/editor/services/contentService.ts +28 -0
  216. package/src/editor/services/editService.ts +12 -0
  217. package/src/editor/services/systemService.ts +5 -2
  218. package/src/editor/sidebar/ComponentTree.tsx +34 -12
  219. package/src/editor/sidebar/Divider.tsx +22 -0
  220. package/src/editor/sidebar/LeftToolbar.tsx +36 -0
  221. package/src/editor/sidebar/SEOInfo.tsx +265 -0
  222. package/src/editor/sidebar/Sidebar.tsx +1 -0
  223. package/src/editor/sidebar/SidebarView.tsx +77 -111
  224. package/src/editor/sidebar/ViewSelector.tsx +211 -43
  225. package/src/editor/ui/Icons.tsx +155 -10
  226. package/src/editor/ui/Section.tsx +1 -1
  227. package/src/editor/ui/SimpleIconButton.tsx +30 -28
  228. package/src/editor/ui/SimpleTabs.tsx +3 -3
  229. package/src/editor/ui/SimpleToolbar.tsx +1 -1
  230. package/src/editor/ui/Splitter.tsx +14 -7
  231. package/src/editor/views/CompareView.tsx +23 -11
  232. package/src/editor/views/SingleEditView.tsx +3 -0
  233. package/src/page-wizard/steps/ContentStep.tsx +0 -1
  234. package/src/revision.ts +2 -2
  235. package/src/splash-screen/NewPage.tsx +18 -13
  236. package/src/splash-screen/RecentPages.tsx +4 -10
  237. package/src/tour/Tour.tsx +125 -34
  238. package/src/tour/default-tour.tsx +55 -45
  239. package/src/types.ts +21 -1
  240. package/styles.css +301 -1
  241. package/dist/editor/menubar/ActionsMenu.js +0 -49
  242. package/dist/editor/menubar/ActionsMenu.js.map +0 -1
  243. package/dist/editor/menubar/SecondaryControls.d.ts +0 -1
  244. package/dist/editor/menubar/SecondaryControls.js +0 -17
  245. package/dist/editor/menubar/SecondaryControls.js.map +0 -1
  246. package/src/editor/menubar/ActionsMenu.tsx +0 -94
  247. package/src/editor/menubar/SecondaryControls.tsx +0 -45
@@ -2,6 +2,12 @@ import { Command, CommandContext, CommandData } from "./commands";
2
2
  import { FullItem, ItemDescriptor } from "../pageModel";
3
3
 
4
4
  import { ItemNameDialog, ItemNameDialogProps } from "../ui/ItemNameDialogNew";
5
+ import {
6
+ exportItems as exportItemsService,
7
+ importItems as importItemsService,
8
+ ExportItemsRequest,
9
+ ImportItemsRequest,
10
+ } from "../services/contentService";
5
11
 
6
12
  import {
7
13
  CopyMoveTargetSelectorDialog,
@@ -349,3 +355,269 @@ export const publishItemCommand: ItemCommand = {
349
355
  await context.editContext.switchView("publish");
350
356
  },
351
357
  };
358
+
359
+ export const exportItemsCommand: ItemCommand = {
360
+ id: "exportItem",
361
+ label: "Export",
362
+ icon: "pi pi-download",
363
+ disabled: (context: ItemCommandContext) =>
364
+ !context.data?.items || context.data.items.length === 0 || false,
365
+ execute: async (context: ItemCommandContext) => {
366
+ const items = context.data?.items;
367
+ if (!items || items.length === 0) return;
368
+
369
+ try {
370
+ // Get export options from user
371
+ const exportOptions = await new Promise<{
372
+ versionOption: "latest" | "all";
373
+ } | null>((resolve) => {
374
+ context.editContext.confirm({
375
+ message: (
376
+ <div>
377
+ <div>Export {items.length} item(s) to YAML?</div>
378
+ <div style={{ marginTop: "10px" }}>
379
+ <label>Language: </label>
380
+ <select id="export-language" defaultValue="en">
381
+ <option value="en">English</option>
382
+ <option value="current">Current Language</option>
383
+ </select>
384
+ </div>
385
+ <div style={{ marginTop: "10px" }}>
386
+ <label>Version: </label>
387
+ <select id="export-version" defaultValue="latest">
388
+ <option value="latest">Latest</option>
389
+ <option value="published">Published</option>
390
+ </select>
391
+ </div>
392
+ </div>
393
+ ),
394
+ header: "Export Items",
395
+ icon: "pi pi-download",
396
+ accept: () => {
397
+ const language =
398
+ (document.getElementById("export-language") as HTMLSelectElement)
399
+ ?.value || "en";
400
+ const versionOption =
401
+ (document.getElementById("export-version") as HTMLSelectElement)
402
+ ?.value || "latest";
403
+ resolve({ versionOption: versionOption as "latest" | "all" });
404
+ },
405
+ reject: () => resolve(null),
406
+ showCancel: true,
407
+ });
408
+ });
409
+
410
+ if (!exportOptions) return;
411
+
412
+ const request: ExportItemsRequest = {
413
+ items: items.map((item) => item.descriptor),
414
+ versionOption: exportOptions.versionOption as "latest" | "all",
415
+ };
416
+
417
+ const response = await exportItemsService(request);
418
+
419
+ if (response.type !== "success") {
420
+ throw new Error(response.details || "Export failed");
421
+ }
422
+
423
+ const result = response.data;
424
+
425
+ if (result && result.yaml) {
426
+ // Copy YAML content to clipboard
427
+ try {
428
+ await navigator.clipboard.writeText(result.yaml);
429
+
430
+ context.editContext.showToast({
431
+ severity: "success",
432
+ summary: "Export Complete",
433
+ detail: `Successfully exported ${result.itemCount} item(s) to clipboard`,
434
+ life: 3000,
435
+ });
436
+
437
+ return true;
438
+ } catch (clipboardError) {
439
+ // Fallback: Show YAML in a dialog if clipboard access fails
440
+ context.editContext.confirm({
441
+ message: (
442
+ <div>
443
+ <div>Export successful! Copy the YAML below:</div>
444
+ <textarea
445
+ readOnly
446
+ value={result.yaml}
447
+ style={{
448
+ width: "100%",
449
+ height: "300px",
450
+ marginTop: "10px",
451
+ fontFamily: "monospace",
452
+ fontSize: "12px",
453
+ }}
454
+ onClick={(e) => {
455
+ (e.target as HTMLTextAreaElement).select();
456
+ }}
457
+ />
458
+ </div>
459
+ ),
460
+ header: "Export Results",
461
+ icon: "pi pi-copy",
462
+ accept: () => {},
463
+ rejectLabel: "Close",
464
+ showCancel: false,
465
+ });
466
+
467
+ return true;
468
+ }
469
+ }
470
+ } catch (error) {
471
+ context.editContext.showToast({
472
+ severity: "error",
473
+ summary: "Export Failed",
474
+ detail:
475
+ error instanceof Error ? error.message : "Unknown error occurred",
476
+ life: 5000,
477
+ });
478
+ }
479
+
480
+ return false;
481
+ },
482
+ };
483
+
484
+ export const importItemsCommand: ItemCommand = {
485
+ id: "importItems",
486
+ label: "Import",
487
+ icon: "pi pi-upload",
488
+ disabled: (context: ItemCommandContext) =>
489
+ !context.data?.items ||
490
+ context.data.items.length !== 1 ||
491
+ !context.data.items[0]?.canLock ||
492
+ false,
493
+ execute: async (context: ItemCommandContext) => {
494
+ const targetItem = context.data?.items[0];
495
+ if (!targetItem) return;
496
+
497
+ try {
498
+ // Get YAML content from user
499
+ const yamlContent = await new Promise<string | null>((resolve) => {
500
+ const dialogContent = (
501
+ <div>
502
+ <div>
503
+ Import items into <em>{targetItem.name}</em>
504
+ </div>
505
+ <div style={{ marginTop: "10px" }}>
506
+ <label htmlFor="yaml-input">YAML Content:</label>
507
+ <textarea
508
+ id="yaml-input"
509
+ rows={15}
510
+ cols={60}
511
+ placeholder="Paste your YAML content here..."
512
+ style={{
513
+ width: "100%",
514
+ marginTop: "5px",
515
+ fontFamily: "monospace",
516
+ fontSize: "12px",
517
+ resize: "vertical",
518
+ }}
519
+ />
520
+ </div>
521
+ <div style={{ marginTop: "10px", fontSize: "12px", color: "#666" }}>
522
+ <button
523
+ type="button"
524
+ onClick={async () => {
525
+ try {
526
+ const clipboardText = await navigator.clipboard.readText();
527
+ const textarea = document.getElementById(
528
+ "yaml-input",
529
+ ) as HTMLTextAreaElement;
530
+ if (textarea) {
531
+ textarea.value = clipboardText;
532
+ }
533
+ } catch (error) {
534
+ console.warn("Could not read from clipboard:", error);
535
+ }
536
+ }}
537
+ style={{
538
+ padding: "5px 10px",
539
+ fontSize: "12px",
540
+ cursor: "pointer",
541
+ }}
542
+ >
543
+ 📋 Paste from Clipboard
544
+ </button>
545
+ <span style={{ marginLeft: "10px" }}>
546
+ Tip: Use Ctrl+V to paste YAML content directly into the text
547
+ area
548
+ </span>
549
+ </div>
550
+ </div>
551
+ );
552
+
553
+ context.editContext.confirm({
554
+ message: dialogContent,
555
+ header: "Import Items",
556
+ icon: "pi pi-upload",
557
+ accept: () => {
558
+ const textarea = document.getElementById(
559
+ "yaml-input",
560
+ ) as HTMLTextAreaElement;
561
+ const yamlValue = textarea?.value?.trim();
562
+ resolve(yamlValue || null);
563
+ },
564
+ reject: () => resolve(null),
565
+ showCancel: true,
566
+ });
567
+ });
568
+
569
+ if (!yamlContent) return;
570
+
571
+ const request: ImportItemsRequest = {
572
+ targetItem: targetItem.descriptor,
573
+ yaml: yamlContent,
574
+ };
575
+
576
+ const response = await importItemsService(request);
577
+
578
+ if (response.type !== "success") {
579
+ throw new Error(response.details || "Import failed");
580
+ }
581
+
582
+ const result = response.data;
583
+
584
+ if (result && result.createdItems) {
585
+ context.editContext.showToast({
586
+ severity: "success",
587
+ summary: "Import Complete",
588
+ detail: `Successfully imported ${result.itemCount} item(s)`,
589
+ life: 3000,
590
+ });
591
+
592
+ // Refresh by requesting a refresh
593
+ context.editContext.requestRefresh("immediate");
594
+
595
+ return result.createdItems;
596
+ }
597
+ } catch (error) {
598
+ const errorMessage =
599
+ error instanceof Error ? error.message : "Unknown error occurred";
600
+
601
+ context.editContext.showToast({
602
+ severity: "error",
603
+ summary: "Import Failed",
604
+ detail: errorMessage,
605
+ life: 5000,
606
+ });
607
+
608
+ // Handle specific error cases
609
+ if (errorMessage.includes("already exist")) {
610
+ context.editContext.confirm({
611
+ message: `Some items already exist in the target location. ${errorMessage}`,
612
+ header: "Import Conflict",
613
+ icon: "pi pi-exclamation-triangle",
614
+ accept: () => {},
615
+ reject: () => {},
616
+ showCancel: false,
617
+ });
618
+ }
619
+ }
620
+
621
+ return false;
622
+ },
623
+ };
@@ -70,7 +70,7 @@ export function MultiLineText({
70
70
  key={fieldItem.id + field.id + fieldItem.language + fieldItem.version}
71
71
  value={value}
72
72
  disabled={readOnly}
73
- className="focus-shadow p-2 text-sm"
73
+ className="focus-shadow bg-gray-5 p-2 text-sm"
74
74
  style={{ width: "100%" }}
75
75
  rows={12}
76
76
  onChange={(e) => {
@@ -164,7 +164,7 @@ export function SingleLineText({
164
164
  key={fieldItem.id + field.id + fieldItem.language + fieldItem.version}
165
165
  value={value || ""}
166
166
  disabled={readOnly}
167
- className="focus-shadow p-1.5 text-sm"
167
+ className="focus-shadow bg-gray-5 p-1.5 text-sm"
168
168
  style={{ width: "100%" }}
169
169
  onChange={handleChange}
170
170
  onSelect={handleSelect}
@@ -1,17 +1,283 @@
1
1
  import { useEditContext } from "../client/editContext";
2
2
  import { User } from "./User";
3
+ import {
4
+ Popover,
5
+ PopoverContent,
6
+ PopoverTrigger,
7
+ } from "../../components/ui/popover";
8
+ import { useEffect, useState } from "react";
9
+ import { getItemVisitors, ItemVisitor } from "../services/editService";
10
+ import { VerticalDotsIcon } from "../ui/Icons";
11
+ import { AboutDialog } from "../client/AboutDialog";
12
+
13
+ type UserListItem = {
14
+ type: "active" | "visitor";
15
+ sessionId?: string;
16
+ user?: {
17
+ displayName?: string;
18
+ name: string;
19
+ email?: string;
20
+ };
21
+ visitedAt?: string;
22
+ isCurrentUser?: boolean;
23
+ };
3
24
 
4
25
  export function ActiveUsers() {
5
26
  const editContext = useEditContext();
27
+ const [visitors, setVisitors] = useState<ItemVisitor[]>([]);
28
+ const [isLoadingVisitors, setIsLoadingVisitors] = useState(false);
29
+
6
30
  const mySession = editContext?.activeSessions.find(
7
31
  (x) => x.sessionId === editContext?.sessionId,
8
32
  );
9
33
 
34
+ const activeSessions =
35
+ editContext?.activeSessions.filter(
36
+ (x) => x.item?.id === mySession?.item?.id,
37
+ ) || [];
38
+
39
+ // Fetch visitors when current item changes
40
+ useEffect(() => {
41
+ if (editContext?.currentItemDescriptor) {
42
+ setIsLoadingVisitors(true);
43
+ getItemVisitors(editContext.currentItemDescriptor)
44
+ .then((result) => {
45
+ if (result.type === "success" && result.data) {
46
+ setVisitors(result.data);
47
+ } else {
48
+ console.warn("Failed to fetch visitors:", result);
49
+ setVisitors([]);
50
+ }
51
+ })
52
+ .catch((error) => {
53
+ console.error("Error fetching visitors:", error);
54
+ setVisitors([]);
55
+ })
56
+ .finally(() => {
57
+ setIsLoadingVisitors(false);
58
+ });
59
+ }
60
+ }, [
61
+ editContext?.currentItemDescriptor?.id,
62
+ editContext?.currentItemDescriptor?.language,
63
+ editContext?.currentItemDescriptor?.version,
64
+ ]);
65
+
66
+ // Convert active sessions to unified format
67
+ const activeUsers: UserListItem[] = activeSessions.map((session) => ({
68
+ type: "active",
69
+ sessionId: session.sessionId,
70
+ user: session.user,
71
+ isCurrentUser: session.sessionId === editContext?.sessionId,
72
+ }));
73
+
74
+ // Convert visitors to unified format, excluding those who are currently active
75
+ const activeUserNames = new Set(
76
+ activeSessions.map((s) => s.user.name.toLowerCase()),
77
+ );
78
+ const recentVisitors: UserListItem[] = visitors
79
+ .filter((visitor) => !activeUserNames.has(visitor.userName.toLowerCase()))
80
+ .slice(0, 10) // Limit to 10 recent visitors
81
+ .map((visitor) => ({
82
+ type: "visitor",
83
+ user: {
84
+ name: visitor.userName,
85
+ displayName: visitor.userName,
86
+ },
87
+ visitedAt: visitor.visitedAt,
88
+ }));
89
+
90
+ const allUsers = [...activeUsers, ...recentVisitors];
91
+ const visibleUsers = activeSessions.slice(0, 3); // Only show active users in the avatar stack
92
+ const remainingActiveCount = Math.max(0, activeSessions.length - 3);
93
+ const totalCount = activeSessions.length + recentVisitors.length;
94
+
95
+ const handleAbout = () => {
96
+ editContext?.openDialog(AboutDialog, {});
97
+ };
98
+
99
+ const handleLogout = () => {
100
+ window.location.href = "/sitecore/login?action=logout";
101
+ };
102
+
10
103
  return (
11
- <div className="flex flex-wrap items-center justify-end gap-2 border-gray-200">
12
- {editContext?.activeSessions
13
- .filter((x) => x.item?.id === mySession?.item?.id)
14
- .map((s) => <User key={s.sessionId} session={s} />)}
15
- </div>
104
+ <Popover>
105
+ <PopoverTrigger asChild>
106
+ <div
107
+ className="flex cursor-pointer items-center gap-2"
108
+ title={`${activeSessions.length} active user${activeSessions.length !== 1 ? "s" : ""}, ${recentVisitors.length} recent visitor${recentVisitors.length !== 1 ? "s" : ""}`}
109
+ >
110
+ {/* Overlapping user avatars */}
111
+ <div className="flex items-center -space-x-2">
112
+ {visibleUsers.map((session, index) => (
113
+ <div
114
+ key={session.sessionId}
115
+ className="relative rounded-full border-2 border-white"
116
+ style={{ zIndex: visibleUsers.length - index }}
117
+ >
118
+ <User session={session} />
119
+ </div>
120
+ ))}
121
+
122
+ {/* Show count if more than 3 active users or if there are visitors */}
123
+ {(remainingActiveCount > 0 || recentVisitors.length > 0) && (
124
+ <div
125
+ className="relative flex h-7 w-7 items-center justify-center rounded-full border-2 border-white bg-gray-600 text-xs font-medium text-white"
126
+ style={{ zIndex: 0 }}
127
+ >
128
+ +{remainingActiveCount + recentVisitors.length}
129
+ </div>
130
+ )}
131
+ </div>
132
+
133
+ <VerticalDotsIcon />
134
+ </div>
135
+ </PopoverTrigger>
136
+
137
+ <PopoverContent className="w-80 p-2" align="end">
138
+ {isLoadingVisitors && (
139
+ <div className="mb-2 text-xs text-gray-500">
140
+ Loading recent visitors...
141
+ </div>
142
+ )}
143
+
144
+ <div className="max-h-96 space-y-1 overflow-y-auto">
145
+ {/* Active Users Section */}
146
+ {activeSessions.length > 0 && (
147
+ <>
148
+ {activeSessions.map((session) => {
149
+ const userName = session.user.displayName || session.user.name;
150
+ const idxBackslash = userName.indexOf("\\");
151
+ const displayName =
152
+ idxBackslash >= 0
153
+ ? userName.substring(idxBackslash + 1)
154
+ : userName;
155
+
156
+ return (
157
+ <div
158
+ key={session.sessionId}
159
+ className="flex items-center gap-3 rounded p-2 hover:bg-gray-50"
160
+ >
161
+ <div className="relative">
162
+ <User session={session} />
163
+ <div className="absolute -right-0.5 -bottom-0.5 h-3 w-3 rounded-full border-2 border-white bg-green-500"></div>
164
+ </div>
165
+ <div className="min-w-0 flex-1">
166
+ <div className="truncate text-sm font-medium text-gray-900">
167
+ {displayName}
168
+ </div>
169
+ <div className="truncate text-xs text-gray-500">
170
+ {session.user.email}
171
+ </div>
172
+ </div>
173
+ {session.sessionId === editContext?.sessionId && (
174
+ <div className="text-xs font-medium text-blue-600">
175
+ You
176
+ </div>
177
+ )}
178
+ </div>
179
+ );
180
+ })}
181
+ </>
182
+ )}
183
+
184
+ {/* Recent Visitors Section */}
185
+ {recentVisitors.length > 0 && (
186
+ <>
187
+ <div className="mt-3 mb-1 px-2 text-xs font-medium text-gray-600">
188
+ Recent Visitors ({recentVisitors.length})
189
+ </div>
190
+ {recentVisitors.map((visitor, index) => {
191
+ const userName =
192
+ visitor.user?.displayName || visitor.user?.name || "";
193
+ const idxBackslash = userName.indexOf("\\");
194
+ const displayName =
195
+ idxBackslash >= 0
196
+ ? userName.substring(idxBackslash + 1)
197
+ : userName;
198
+
199
+ const visitedAt = visitor.visitedAt
200
+ ? new Date(visitor.visitedAt)
201
+ : null;
202
+ const timeAgo = visitedAt ? formatTimeAgo(visitedAt) : "";
203
+
204
+ return (
205
+ <div
206
+ key={`visitor-${index}`}
207
+ className="flex items-center gap-3 rounded p-2 opacity-75 hover:bg-gray-50"
208
+ >
209
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-300 text-xs font-medium text-gray-600">
210
+ {displayName.charAt(0).toUpperCase()}
211
+ </div>
212
+ <div className="min-w-0 flex-1">
213
+ <div className="truncate text-sm font-medium text-gray-700">
214
+ {displayName}
215
+ </div>
216
+ <div className="truncate text-xs text-gray-500">
217
+ {timeAgo}
218
+ </div>
219
+ </div>
220
+ </div>
221
+ );
222
+ })}
223
+ </>
224
+ )}
225
+
226
+ {/* Separator before actions */}
227
+ {(activeSessions.length > 0 || recentVisitors.length > 0) && (
228
+ <div className="my-2 border-t border-gray-200"></div>
229
+ )}
230
+
231
+ {/* Action Items */}
232
+ <div className="space-y-1">
233
+ <div
234
+ className="flex cursor-pointer items-center gap-3 rounded p-2 hover:bg-gray-50"
235
+ onClick={handleAbout}
236
+ >
237
+ <div className="flex h-8 w-8 items-center justify-center">
238
+ <i
239
+ className="pi pi-info-circle text-gray-600"
240
+ style={{ fontSize: "1rem" }}
241
+ />
242
+ </div>
243
+ <div className="min-w-0 flex-1">
244
+ <div className="text-sm font-medium text-gray-900">About</div>
245
+ </div>
246
+ </div>
247
+ <div
248
+ className="flex cursor-pointer items-center gap-3 rounded p-2 hover:bg-gray-50"
249
+ onClick={handleLogout}
250
+ >
251
+ <div className="flex h-8 w-8 items-center justify-center">
252
+ <i
253
+ className="pi pi-sign-out text-gray-600"
254
+ style={{ fontSize: "1rem" }}
255
+ />
256
+ </div>
257
+ <div className="min-w-0 flex-1">
258
+ <div className="text-sm font-medium text-gray-900">Log Out</div>
259
+ </div>
260
+ </div>
261
+ </div>
262
+
263
+ {allUsers.length === 0 && !isLoadingVisitors && (
264
+ <div className="p-2 text-sm text-gray-500">No users found</div>
265
+ )}
266
+ </div>
267
+ </PopoverContent>
268
+ </Popover>
16
269
  );
17
270
  }
271
+
272
+ function formatTimeAgo(date: Date): string {
273
+ const now = new Date();
274
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
275
+
276
+ if (diffInSeconds < 60) return "Just now";
277
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
278
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
279
+ if (diffInSeconds < 604800)
280
+ return `${Math.floor(diffInSeconds / 86400)}d ago`;
281
+
282
+ return date.toLocaleDateString();
283
+ }
@@ -0,0 +1,89 @@
1
+ import { useEditContext } from "../client/editContext";
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ DropdownMenuSeparator,
7
+ DropdownMenuTrigger,
8
+ } from "../../components/ui/dropdown-menu";
9
+ import { MenuItemGroup } from "../../config/types";
10
+ import { VerticalDotsIcon } from "../ui/Icons";
11
+
12
+ export function ItemActionsMenu({ isMobile }: { isMobile: boolean }) {
13
+ const editContext = useEditContext();
14
+ if (!editContext) return null;
15
+
16
+ const itemGroups: MenuItemGroup[] =
17
+ editContext.configuration.editor.itemActionsMenu?.itemsFactory(
18
+ editContext,
19
+ ) || [];
20
+
21
+ if (isMobile) {
22
+ return (
23
+ <div className="text-sm text-gray-200">
24
+ {itemGroups.map((group, groupIndex) => (
25
+ <div
26
+ key={`group-${group.id || groupIndex}`}
27
+ className="mb-3 flex flex-row flex-wrap items-center justify-end gap-3"
28
+ >
29
+ {group.items
30
+ .filter((x) => !x.disabled)
31
+ .map((item) => (
32
+ <div
33
+ key={item.id}
34
+ className="flex items-center gap-1"
35
+ onClick={() => {
36
+ item.command?.({});
37
+ }}
38
+ >
39
+ {typeof item.icon === "string" ? (
40
+ <i className={item.icon} style={{ fontSize: "1rem" }} />
41
+ ) : (
42
+ <>{item.icon}</>
43
+ )}
44
+ {item.label}
45
+ </div>
46
+ ))}
47
+ </div>
48
+ ))}
49
+ </div>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <DropdownMenu>
55
+ <DropdownMenuTrigger asChild className="cursor-pointer">
56
+ <div className="p-1">
57
+ <VerticalDotsIcon />
58
+ </div>
59
+ </DropdownMenuTrigger>
60
+ <DropdownMenuContent className="w-64" align="start">
61
+ {itemGroups.map((group, groupIndex) => (
62
+ <div key={`group-${group.id || groupIndex}`}>
63
+ {group.items
64
+ .filter((x) => !x.disabled)
65
+ .map((item) => (
66
+ <DropdownMenuItem
67
+ key={item.id}
68
+ onClick={() => {
69
+ item.command?.({});
70
+ }}
71
+ className="flex items-center gap-2"
72
+ >
73
+ {typeof item.icon === "string" ? (
74
+ <i className={item.icon} style={{ fontSize: "1rem" }} />
75
+ ) : (
76
+ <>{item.icon}</>
77
+ )}
78
+ {item.label}
79
+ </DropdownMenuItem>
80
+ ))}
81
+ {groupIndex > 0 && groupIndex < itemGroups.length - 1 && (
82
+ <DropdownMenuSeparator />
83
+ )}
84
+ </div>
85
+ ))}
86
+ </DropdownMenuContent>
87
+ </DropdownMenu>
88
+ );
89
+ }