@case-framework/survey-core 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/editor.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { I as generateId, R as structuredCloneMethod, l as GroupItemCore, m as ReservedSurveyItemTypes, n as SurveyItemTranslations, t as Survey } from "./survey-DJbgFVPz.mjs";
1
+ import { $ as generateId, a as getContentPlainText, nt as structuredCloneMethod, t as Survey, u as SurveyItemTranslations, v as GroupItemCore } from "./survey-DnLtf19j.mjs";
2
2
  //#region src/editor/ai-context.ts
3
3
  const DEFAULT_SCOPE_LIMITS = {
4
4
  tiny: 40,
@@ -174,10 +174,7 @@ const normalizeSnippetText = (text, maxChars) => {
174
174
  return text.trim().replace(/\s+/g, " ").slice(0, maxChars);
175
175
  };
176
176
  const getContentText = (content, maxChars) => {
177
- if (!content) return null;
178
- const maybeText = content.content;
179
- if (typeof maybeText !== "string") return null;
180
- const normalized = normalizeSnippetText(maybeText, maxChars);
177
+ const normalized = normalizeSnippetText(getContentPlainText(content), maxChars);
181
178
  return normalized.length > 0 ? normalized : null;
182
179
  };
183
180
  function getItemTranslationSnippets(survey, itemId, maxSnippets, textLimit) {
@@ -439,7 +436,7 @@ var ItemCopyPaste = class ItemCopyPaste {
439
436
  */
440
437
  updateItemIdsInData(itemData, idMapping) {
441
438
  const updatedData = JSON.parse(JSON.stringify(itemData));
442
- if (updatedData.itemType === ReservedSurveyItemTypes.Group && updatedData.config) {
439
+ if (updatedData.itemType === "group" && updatedData.config) {
443
440
  const config = updatedData.config;
444
441
  if (config.items) config.items = config.items.map((childId) => idMapping[childId] ?? childId);
445
442
  }
@@ -460,6 +457,11 @@ var ItemCopyPaste = class ItemCopyPaste {
460
457
  if (itemData.validations) Object.values(itemData.validations).forEach((expr) => {
461
458
  if (expr) this.updateExpressionReferences(expr, idMapping);
462
459
  });
460
+ if (itemData.prefills) itemData.prefills.forEach((prefill) => {
461
+ if (prefill.when) this.updateExpressionReferences(prefill.when, idMapping);
462
+ if (prefill.source.type === "expression") this.updateExpressionReferences(prefill.source.expression, idMapping);
463
+ if (prefill.source.type === "previousResponse") prefill.source.ref.itemId = idMapping[prefill.source.ref.itemId] ?? prefill.source.ref.itemId;
464
+ });
463
465
  }
464
466
  /**
465
467
  * Update references in a single expression (recursive)
@@ -528,52 +530,152 @@ var MemoryCalculator = class {
528
530
  return `${size.toFixed(1)} ${units[unitIndex]}`;
529
531
  }
530
532
  };
533
+ function cloneAssets(assets) {
534
+ if (!assets || Object.keys(assets).length === 0) return;
535
+ return structuredCloneMethod(assets);
536
+ }
537
+ function normalizeHistoryEntry(entry) {
538
+ if ("kind" in entry) {
539
+ if (entry.kind === "survey-snapshot") return {
540
+ kind: "survey-snapshot",
541
+ survey: structuredCloneMethod(entry.survey),
542
+ timestamp: entry.timestamp,
543
+ meta: entry.meta,
544
+ memorySize: entry.memorySize
545
+ };
546
+ return {
547
+ kind: "asset-change",
548
+ changes: structuredCloneMethod(entry.changes),
549
+ timestamp: entry.timestamp,
550
+ meta: entry.meta,
551
+ memorySize: entry.memorySize
552
+ };
553
+ }
554
+ return {
555
+ kind: "survey-snapshot",
556
+ survey: structuredCloneMethod(entry.survey),
557
+ timestamp: entry.timestamp,
558
+ meta: entry.meta,
559
+ memorySize: entry.memorySize
560
+ };
561
+ }
531
562
  var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
532
563
  history = [];
533
564
  currentIndex = -1;
534
565
  _config;
566
+ _initialAssets;
535
567
  constructor(initialSurvey, config = {}, meta = {
536
568
  label: "Initial state",
537
569
  source: CommitSource.SYSTEM
538
- }) {
570
+ }, initialAssets) {
539
571
  this._config = {
540
572
  maxTotalMemoryMB: 50,
541
573
  minHistorySize: 10,
542
574
  maxHistorySize: 200,
543
575
  ...config
544
576
  };
577
+ this._initialAssets = cloneAssets(initialAssets);
545
578
  this.saveSnapshot(initialSurvey, meta);
546
579
  }
547
- saveSnapshot(survey, meta) {
548
- const memorySize = MemoryCalculator.calculateSize(survey);
580
+ saveEntry(entry) {
581
+ const payload = entry.kind === "survey-snapshot" ? {
582
+ kind: entry.kind,
583
+ survey: entry.survey
584
+ } : {
585
+ kind: entry.kind,
586
+ changes: entry.changes
587
+ };
588
+ const memorySize = MemoryCalculator.calculateSize(payload);
549
589
  this.history = this.history.slice(0, this.currentIndex + 1);
550
590
  this.history.push({
551
- survey: structuredCloneMethod(survey),
591
+ ...structuredCloneMethod(entry),
552
592
  timestamp: Date.now(),
553
- meta,
554
593
  memorySize
555
594
  });
556
595
  this.currentIndex++;
557
596
  this.cleanupHistory();
558
597
  }
598
+ saveSnapshot(survey, meta) {
599
+ this.saveEntry({
600
+ kind: "survey-snapshot",
601
+ survey: structuredCloneMethod(survey),
602
+ meta
603
+ });
604
+ }
605
+ getBaseAssetsSize() {
606
+ return this._initialAssets ? MemoryCalculator.calculateSize(this._initialAssets) : 0;
607
+ }
559
608
  cleanupHistory() {
560
- let totalMemory = this.getTotalMemoryUsage();
561
609
  const maxMemoryBytes = this._config.maxTotalMemoryMB * 1024 * 1024;
562
- while (this.history.length > this._config.minHistorySize && (totalMemory > maxMemoryBytes || this.history.length > this._config.maxHistorySize)) {
563
- const removedSnapshot = this.history.shift();
564
- this.currentIndex--;
565
- if (removedSnapshot) totalMemory -= removedSnapshot.memorySize;
566
- }
610
+ while (this.history.length > this._config.minHistorySize && (this.getTotalMemoryUsage() > maxMemoryBytes || this.history.length > this._config.maxHistorySize)) if (this.dropOldestState() <= 0) break;
611
+ }
612
+ dropOldestState() {
613
+ if (this.history.length <= 1) return 0;
614
+ const nextStateIndex = 1;
615
+ const nextStateSurvey = this.getStateAtIndex(nextStateIndex);
616
+ const nextStateAssets = this.getAssetsAtIndex(nextStateIndex);
617
+ const nextEntry = this.history[nextStateIndex];
618
+ const newFirstEntry = {
619
+ kind: "survey-snapshot",
620
+ survey: nextStateSurvey,
621
+ meta: nextEntry.meta,
622
+ timestamp: nextEntry.timestamp,
623
+ memorySize: MemoryCalculator.calculateSize({
624
+ kind: "survey-snapshot",
625
+ survey: nextStateSurvey
626
+ })
627
+ };
628
+ const removedBytes = this.history[0].memorySize;
629
+ this._initialAssets = cloneAssets(nextStateAssets);
630
+ this.history = [newFirstEntry, ...this.history.slice(nextStateIndex + 1)];
631
+ this.currentIndex--;
632
+ return removedBytes;
567
633
  }
568
634
  getTotalMemoryUsage() {
569
- return this.history.reduce((total, entry) => total + entry.memorySize, 0);
635
+ return this.getBaseAssetsSize() + this.history.reduce((total, entry) => total + entry.memorySize, 0);
636
+ }
637
+ getStateAtIndex(index) {
638
+ if (index < 0 || index >= this.history.length) throw new Error("Invalid history state");
639
+ for (let i = index; i >= 0; i--) {
640
+ const entry = this.history[i];
641
+ if (entry.kind === "survey-snapshot") return structuredCloneMethod(entry.survey);
642
+ }
643
+ throw new Error("Invalid history state");
644
+ }
645
+ applyAssetPatch(assets, patch) {
646
+ const nextAssets = structuredCloneMethod(assets);
647
+ if (patch.next) nextAssets[patch.assetId] = structuredCloneMethod(patch.next);
648
+ else delete nextAssets[patch.assetId];
649
+ return nextAssets;
650
+ }
651
+ getAssetsAtIndex(index) {
652
+ if (index < 0 || index >= this.history.length) throw new Error("Invalid history state");
653
+ let assets = cloneAssets(this._initialAssets) ?? {};
654
+ for (let i = 1; i <= index; i++) {
655
+ const entry = this.history[i];
656
+ if (entry.kind !== "asset-change") continue;
657
+ for (const patch of entry.changes) assets = this.applyAssetPatch(assets, patch);
658
+ }
659
+ return Object.keys(assets).length > 0 ? assets : void 0;
570
660
  }
571
661
  commit(survey, meta) {
572
662
  this.saveSnapshot(survey, meta);
573
663
  }
664
+ commitAssetChange(changes, meta) {
665
+ if (changes.length === 0) return;
666
+ this.saveEntry({
667
+ kind: "asset-change",
668
+ changes: structuredCloneMethod(changes),
669
+ meta
670
+ });
671
+ }
574
672
  getCurrentState() {
575
673
  if (this.currentIndex < 0 || this.currentIndex >= this.history.length) throw new Error("Invalid history state");
576
- return structuredCloneMethod(this.history[this.currentIndex].survey);
674
+ return this.getStateAtIndex(this.currentIndex);
675
+ }
676
+ getCurrentAssets() {
677
+ if (this.currentIndex < 0 || this.currentIndex >= this.history.length) throw new Error("Invalid history state");
678
+ return this.getAssetsAtIndex(this.currentIndex);
577
679
  }
578
680
  undo() {
579
681
  if (!this.canUndo()) return null;
@@ -614,6 +716,7 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
614
716
  getHistory() {
615
717
  return this.history.map((entry, index) => ({
616
718
  index,
719
+ kind: entry.kind,
617
720
  meta: entry.meta,
618
721
  timestamp: entry.timestamp,
619
722
  memorySize: entry.memorySize,
@@ -621,6 +724,25 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
621
724
  }));
622
725
  }
623
726
  /**
727
+ * Get committed history entries after the initial state up to the current index.
728
+ *
729
+ * Redo-only future entries are intentionally omitted so the returned list describes the active
730
+ * change chain from the initial state to the current editor state.
731
+ */
732
+ getCommitsSinceInitial() {
733
+ return this.history.slice(1, this.currentIndex + 1).map((entry, offset) => {
734
+ const index = offset + 1;
735
+ return {
736
+ index,
737
+ kind: entry.kind,
738
+ meta: entry.meta,
739
+ timestamp: entry.timestamp,
740
+ memorySize: entry.memorySize,
741
+ isCurrent: index === this.currentIndex
742
+ };
743
+ });
744
+ }
745
+ /**
624
746
  * Get the current index in the history
625
747
  */
626
748
  getCurrentIndex() {
@@ -654,14 +776,25 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
654
776
  */
655
777
  serialize() {
656
778
  return {
657
- history: this.history.map((entry) => ({
658
- survey: entry.survey,
659
- timestamp: entry.timestamp,
660
- meta: entry.meta,
661
- memorySize: entry.memorySize
662
- })),
779
+ history: this.history.map((entry) => {
780
+ if (entry.kind === "survey-snapshot") return {
781
+ kind: "survey-snapshot",
782
+ survey: entry.survey,
783
+ timestamp: entry.timestamp,
784
+ meta: entry.meta,
785
+ memorySize: entry.memorySize
786
+ };
787
+ return {
788
+ kind: "asset-change",
789
+ changes: entry.changes,
790
+ timestamp: entry.timestamp,
791
+ meta: entry.meta,
792
+ memorySize: entry.memorySize
793
+ };
794
+ }),
663
795
  currentIndex: this.currentIndex,
664
- config: { ...this._config }
796
+ config: { ...this._config },
797
+ initialAssets: cloneAssets(this._initialAssets)
665
798
  };
666
799
  }
667
800
  /**
@@ -669,23 +802,34 @@ var SurveyEditorUndoRedo = class SurveyEditorUndoRedo {
669
802
  * @param jsonData The serialized undo/redo state
670
803
  * @returns A new SurveyEditorUndoRedo instance with the restored state
671
804
  */
672
- static deserialize(jsonData) {
805
+ static deserialize(jsonData, initialAssetsOverride) {
673
806
  if (!jsonData.history || !Array.isArray(jsonData.history) || jsonData.history.length === 0) throw new Error("Invalid object: history must be an array and must not be empty");
674
807
  if (typeof jsonData.currentIndex !== "number" || jsonData.currentIndex < 0 || jsonData.currentIndex >= jsonData.history.length) throw new Error("Invalid object: currentIndex must be a valid index within the history array");
675
808
  if (!jsonData.config) throw new Error("Invalid object: config is required");
676
- const instance = new SurveyEditorUndoRedo(jsonData.history[0].survey, jsonData.config);
677
- instance.history = jsonData.history.map((entry) => ({
678
- survey: structuredCloneMethod(entry.survey),
679
- timestamp: entry.timestamp,
680
- meta: entry.meta,
681
- memorySize: entry.memorySize
682
- }));
809
+ const normalizedHistory = jsonData.history.map(normalizeHistoryEntry);
810
+ const firstEntry = normalizedHistory[0];
811
+ const instance = new SurveyEditorUndoRedo(firstEntry.kind === "survey-snapshot" ? firstEntry.survey : (() => {
812
+ throw new Error("Invalid object: first history entry must resolve to a survey snapshot");
813
+ })(), jsonData.config, firstEntry.meta, initialAssetsOverride ?? jsonData.initialAssets);
814
+ instance.history = normalizedHistory;
683
815
  instance.currentIndex = jsonData.currentIndex;
816
+ instance._initialAssets = cloneAssets(initialAssetsOverride ?? jsonData.initialAssets);
684
817
  return instance;
685
818
  }
686
819
  };
687
820
  //#endregion
688
821
  //#region src/editor/survey-editor.ts
822
+ function stripAssetsFromSurveySnapshot(survey) {
823
+ const snapshot = structuredCloneMethod(survey);
824
+ delete snapshot.assets;
825
+ return snapshot;
826
+ }
827
+ function mergeSurveySnapshotWithAssets(snapshot, assets) {
828
+ const merged = structuredCloneMethod(snapshot);
829
+ if (assets && Object.keys(assets).length > 0) merged.assets = structuredCloneMethod(assets);
830
+ else delete merged.assets;
831
+ return merged;
832
+ }
689
833
  var SurveyEditor = class SurveyEditor {
690
834
  _survey;
691
835
  _undoRedo;
@@ -695,7 +839,8 @@ var SurveyEditor = class SurveyEditor {
695
839
  this._survey = survey;
696
840
  const { pluginRegistry, ...undoRedoConfig } = config;
697
841
  this._pluginRegistry = pluginRegistry;
698
- this._undoRedo = new SurveyEditorUndoRedo(survey.serialize(), undoRedoConfig, meta);
842
+ const serialized = survey.serialize();
843
+ this._undoRedo = new SurveyEditorUndoRedo(stripAssetsFromSurveySnapshot(serialized), undoRedoConfig, meta, serialized.assets);
699
844
  }
700
845
  /** Returns an immutable copy of the current survey state. */
701
846
  get survey() {
@@ -710,9 +855,13 @@ var SurveyEditor = class SurveyEditor {
710
855
  return this._undoRedo;
711
856
  }
712
857
  commit(meta) {
713
- this._undoRedo.commit(this._survey.serialize(), meta);
858
+ if (!this._hasUncommittedChanges) return;
859
+ this._undoRedo.commit(stripAssetsFromSurveySnapshot(this._survey.serialize()), meta);
714
860
  this._hasUncommittedChanges = false;
715
861
  }
862
+ restoreHistoryState(snapshot, assets) {
863
+ this._survey = Survey.fromJson(mergeSurveySnapshotWithAssets(snapshot, assets), this._pluginRegistry);
864
+ }
716
865
  commitIfNeeded() {
717
866
  if (this._hasUncommittedChanges) this.commit({
718
867
  label: "Latest content changes",
@@ -721,13 +870,13 @@ var SurveyEditor = class SurveyEditor {
721
870
  }
722
871
  undo() {
723
872
  if (this._hasUncommittedChanges) {
724
- this._survey = Survey.fromJson(this._undoRedo.getCurrentState(), this._pluginRegistry);
873
+ this.restoreHistoryState(this._undoRedo.getCurrentState(), this._undoRedo.getCurrentAssets());
725
874
  this._hasUncommittedChanges = false;
726
875
  return true;
727
876
  } else {
728
877
  const previousState = this._undoRedo.undo();
729
878
  if (previousState) {
730
- this._survey = Survey.fromJson(previousState, this._pluginRegistry);
879
+ this.restoreHistoryState(previousState, this._undoRedo.getCurrentAssets());
731
880
  this._hasUncommittedChanges = false;
732
881
  return true;
733
882
  }
@@ -738,7 +887,7 @@ var SurveyEditor = class SurveyEditor {
738
887
  if (this._hasUncommittedChanges) return false;
739
888
  const nextState = this._undoRedo.redo();
740
889
  if (nextState) {
741
- this._survey = Survey.fromJson(nextState, this._pluginRegistry);
890
+ this.restoreHistoryState(nextState, this._undoRedo.getCurrentAssets());
742
891
  this._hasUncommittedChanges = false;
743
892
  return true;
744
893
  }
@@ -751,7 +900,7 @@ var SurveyEditor = class SurveyEditor {
751
900
  if (this._hasUncommittedChanges) return false;
752
901
  const targetState = this._undoRedo.jumpToIndex(targetIndex);
753
902
  if (targetState) {
754
- this._survey = Survey.fromJson(targetState, this._pluginRegistry);
903
+ this.restoreHistoryState(targetState, this._undoRedo.getCurrentAssets());
755
904
  this._hasUncommittedChanges = false;
756
905
  return true;
757
906
  }
@@ -781,6 +930,12 @@ var SurveyEditor = class SurveyEditor {
781
930
  return this._undoRedo.getConfig();
782
931
  }
783
932
  /**
933
+ * Get committed changes after the initial editor state up to the current undo/redo position.
934
+ */
935
+ getCommitsSinceInitial() {
936
+ return this._undoRedo.getCommitsSinceInitial();
937
+ }
938
+ /**
784
939
  * Serialize the SurveyEditor state to JSON
785
940
  * @returns A JSON-serializable object containing the complete editor state
786
941
  */
@@ -804,8 +959,14 @@ var SurveyEditor = class SurveyEditor {
804
959
  if (typeof jsonData.hasUncommittedChanges !== "boolean") throw new Error("Invalid object: hasUncommittedChanges must be a boolean");
805
960
  if (jsonData.version && !jsonData.version.startsWith("1.")) console.warn(`Warning: Loading SurveyEditor with version ${jsonData.version}, current version is 1.0.0`);
806
961
  const editor = new SurveyEditor(Survey.fromJson(jsonData.survey, pluginRegistry), { pluginRegistry });
807
- editor._undoRedo = SurveyEditorUndoRedo.deserialize(jsonData.undoRedo);
808
- editor._hasUncommittedChanges = jsonData.hasUncommittedChanges;
962
+ editor._undoRedo = SurveyEditorUndoRedo.deserialize({
963
+ ...jsonData.undoRedo,
964
+ history: jsonData.undoRedo.history.map((entry) => "survey" in entry ? {
965
+ ...entry,
966
+ survey: stripAssetsFromSurveySnapshot(entry.survey)
967
+ } : entry)
968
+ }, jsonData.survey.assets);
969
+ editor._hasUncommittedChanges = JSON.stringify(stripAssetsFromSurveySnapshot(jsonData.survey)) !== JSON.stringify(editor._undoRedo.getCurrentState());
809
970
  return editor;
810
971
  }
811
972
  markAsModified() {
@@ -919,8 +1080,8 @@ var SurveyEditor = class SurveyEditor {
919
1080
  this.markAsModified();
920
1081
  }
921
1082
  updateItemTranslations(itemId, updatedContent) {
1083
+ if (!this._survey.surveyItems.get(itemId)) return false;
922
1084
  this.markAsModified();
923
- if (!this._survey.surveyItems.get(itemId)) throw new Error(`Item with id '${itemId}' not found`);
924
1085
  this._survey.translations.setItemTranslations(itemId, updatedContent);
925
1086
  return true;
926
1087
  }
@@ -996,6 +1157,49 @@ var SurveyEditor = class SurveyEditor {
996
1157
  this._survey.deleteTemplateValue(key);
997
1158
  }
998
1159
  /**
1160
+ * Get a survey asset immutably.
1161
+ */
1162
+ getAsset(assetId) {
1163
+ const asset = this._survey.getAsset(assetId);
1164
+ return asset ? structuredCloneMethod(asset) : void 0;
1165
+ }
1166
+ /**
1167
+ * Get all survey assets immutably.
1168
+ */
1169
+ getAssets() {
1170
+ return this._survey.getAssets();
1171
+ }
1172
+ /**
1173
+ * Add or replace a survey asset.
1174
+ */
1175
+ setAsset(assetId, asset) {
1176
+ const previousAsset = this._survey.getAsset(assetId);
1177
+ this._survey.setAsset(assetId, structuredCloneMethod(asset));
1178
+ this._undoRedo.commitAssetChange([{
1179
+ assetId,
1180
+ prev: previousAsset,
1181
+ next: structuredCloneMethod(asset)
1182
+ }], {
1183
+ label: `Update asset: ${assetId}`,
1184
+ source: CommitSource.USER
1185
+ });
1186
+ }
1187
+ /**
1188
+ * Remove a survey asset.
1189
+ */
1190
+ removeAsset(assetId) {
1191
+ const previousAsset = this._survey.getAsset(assetId);
1192
+ if (!previousAsset) return;
1193
+ this._survey.deleteAsset(assetId);
1194
+ this._undoRedo.commitAssetChange([{
1195
+ assetId,
1196
+ prev: previousAsset
1197
+ }], {
1198
+ label: `Remove asset: ${assetId}`,
1199
+ source: CommitSource.USER
1200
+ });
1201
+ }
1202
+ /**
999
1203
  * Copy a survey item and all its data to clipboard format
1000
1204
  * @param itemId - The ID of the item to copy
1001
1205
  * @returns Clipboard data that can be serialized to JSON for clipboard