@burdenoff/microfe-planmagnet 2026.1002.1 → 2026.1002.3

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 (218) hide show
  1. package/dist/generated/wspace-operations.d.ts +2533 -546
  2. package/dist/generated/wspace-operations.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +5654 -71
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/index.js +8 -8
  6. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  7. package/dist/planmagnet/PlanMagnetRoot.js +180 -162
  8. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  9. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  10. package/dist/planmagnet/PlanMagnetRoutes.js +100 -84
  11. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  12. package/dist/planmagnet/board/BulkActions.d.ts.map +1 -1
  13. package/dist/planmagnet/board/BulkActions.js +106 -55
  14. package/dist/planmagnet/board/BulkActions.js.map +1 -1
  15. package/dist/planmagnet/controls/Pickers.d.ts +10 -5
  16. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  17. package/dist/planmagnet/controls/Pickers.js +10 -9
  18. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  19. package/dist/planmagnet/dashboards/DashboardUI.d.ts +0 -4
  20. package/dist/planmagnet/dashboards/DashboardUI.d.ts.map +1 -1
  21. package/dist/planmagnet/dashboards/DashboardUI.js +6 -13
  22. package/dist/planmagnet/dashboards/DashboardUI.js.map +1 -1
  23. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
  24. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +70 -59
  25. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
  26. package/dist/planmagnet/forecast/ResourceForecastCard.js +1 -1
  27. package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -1
  28. package/dist/planmagnet/index.d.ts +1 -1
  29. package/dist/planmagnet/index.d.ts.map +1 -1
  30. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts +3 -3
  31. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts.map +1 -1
  32. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +26 -19
  33. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js.map +1 -1
  34. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts +2 -2
  35. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts.map +1 -1
  36. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +18 -12
  37. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js.map +1 -1
  38. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts +2 -0
  39. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts.map +1 -0
  40. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js +251 -0
  41. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js.map +1 -0
  42. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +4 -4
  43. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
  44. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  45. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +133 -131
  46. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  47. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  48. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +5 -5
  49. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  50. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +6 -6
  51. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  52. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  53. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +107 -82
  54. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  55. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts +2 -0
  56. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts.map +1 -0
  57. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js +174 -0
  58. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js.map +1 -0
  59. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts +2 -0
  60. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts.map +1 -0
  61. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js +350 -0
  62. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js.map +1 -0
  63. package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts +2 -0
  64. package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts.map +1 -0
  65. package/dist/planmagnet/pages/ProductsProductLinesPagePage.js +168 -0
  66. package/dist/planmagnet/pages/ProductsProductLinesPagePage.js.map +1 -0
  67. package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
  68. package/dist/planmagnet/pages/ProductsProductsPagePage.js +1 -1
  69. package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
  70. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
  71. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +116 -114
  72. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
  73. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  74. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +174 -159
  75. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  76. package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
  77. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -145
  78. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
  79. package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
  80. package/dist/planmagnet/pages/TasksEditTaskPagePage.js +161 -143
  81. package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
  82. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  83. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +160 -156
  84. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  85. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.d.ts.map +1 -1
  86. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +97 -99
  87. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js.map +1 -1
  88. package/dist/planmagnet/product/FeatureLinkPanels.d.ts +29 -0
  89. package/dist/planmagnet/product/FeatureLinkPanels.d.ts.map +1 -0
  90. package/dist/planmagnet/product/FeatureLinkPanels.js +373 -0
  91. package/dist/planmagnet/product/FeatureLinkPanels.js.map +1 -0
  92. package/dist/planmagnet/product/ProductControls.js +1 -1
  93. package/dist/planmagnet/product/ProductControls.js.map +1 -1
  94. package/dist/planmagnet/product/ProductFeatureControls.d.ts +39 -0
  95. package/dist/planmagnet/product/ProductFeatureControls.d.ts.map +1 -0
  96. package/dist/planmagnet/product/ProductFeatureControls.js +315 -0
  97. package/dist/planmagnet/product/ProductFeatureControls.js.map +1 -0
  98. package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts +4 -0
  99. package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts.map +1 -0
  100. package/dist/planmagnet/product/ProductFeatureLinkCard.js +192 -0
  101. package/dist/planmagnet/product/ProductFeatureLinkCard.js.map +1 -0
  102. package/dist/planmagnet/product/ProductLineControls.d.ts +52 -0
  103. package/dist/planmagnet/product/ProductLineControls.d.ts.map +1 -0
  104. package/dist/planmagnet/product/ProductLineControls.js +417 -0
  105. package/dist/planmagnet/product/ProductLineControls.js.map +1 -0
  106. package/dist/planmagnet/product/ProductTeamsPanel.d.ts +4 -0
  107. package/dist/planmagnet/product/ProductTeamsPanel.d.ts.map +1 -0
  108. package/dist/planmagnet/product/ProductTeamsPanel.js +129 -0
  109. package/dist/planmagnet/product/ProductTeamsPanel.js.map +1 -0
  110. package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts +4 -0
  111. package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts.map +1 -0
  112. package/dist/planmagnet/product/WorkItemProductFeatureChip.js +41 -0
  113. package/dist/planmagnet/product/WorkItemProductFeatureChip.js.map +1 -0
  114. package/dist/planmagnet/product/hierarchy.d.ts +127 -0
  115. package/dist/planmagnet/product/hierarchy.d.ts.map +1 -0
  116. package/dist/planmagnet/product/hierarchy.js +173 -0
  117. package/dist/planmagnet/product/hierarchy.js.map +1 -0
  118. package/dist/planmagnet/product/hierarchyOperations.d.ts +35 -0
  119. package/dist/planmagnet/product/hierarchyOperations.d.ts.map +1 -0
  120. package/dist/planmagnet/product/hierarchyOperations.js +440 -0
  121. package/dist/planmagnet/product/hierarchyOperations.js.map +1 -0
  122. package/dist/planmagnet/product/refusal.d.ts +14 -0
  123. package/dist/planmagnet/product/refusal.d.ts.map +1 -0
  124. package/dist/planmagnet/product/refusal.js +10 -0
  125. package/dist/planmagnet/product/refusal.js.map +1 -0
  126. package/dist/planmagnet/product/routes.d.ts +10 -0
  127. package/dist/planmagnet/product/routes.d.ts.map +1 -1
  128. package/dist/planmagnet/product/routes.js +2 -2
  129. package/dist/planmagnet/product/routes.js.map +1 -1
  130. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts +20 -0
  131. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts.map +1 -0
  132. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js +55 -0
  133. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js.map +1 -0
  134. package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts +30 -0
  135. package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts.map +1 -0
  136. package/dist/planmagnet/satisfaction/SatisfactionFigures.js +282 -0
  137. package/dist/planmagnet/satisfaction/SatisfactionFigures.js.map +1 -0
  138. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts +9 -0
  139. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts.map +1 -0
  140. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js +390 -0
  141. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js.map +1 -0
  142. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts +17 -0
  143. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts.map +1 -0
  144. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js +77 -0
  145. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js.map +1 -0
  146. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts +18 -0
  147. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts.map +1 -0
  148. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js +87 -0
  149. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js.map +1 -0
  150. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts +13 -0
  151. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts.map +1 -0
  152. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js +326 -0
  153. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js.map +1 -0
  154. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts +23 -0
  155. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts.map +1 -0
  156. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js +258 -0
  157. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js.map +1 -0
  158. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts +6 -0
  159. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts.map +1 -0
  160. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js +56 -0
  161. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js.map +1 -0
  162. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts +16 -0
  163. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts.map +1 -0
  164. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js +102 -0
  165. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js.map +1 -0
  166. package/dist/planmagnet/satisfaction/satisfaction.d.ts +154 -0
  167. package/dist/planmagnet/satisfaction/satisfaction.d.ts.map +1 -0
  168. package/dist/planmagnet/satisfaction/satisfaction.js +210 -0
  169. package/dist/planmagnet/satisfaction/satisfaction.js.map +1 -0
  170. package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts +108 -0
  171. package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts.map +1 -0
  172. package/dist/planmagnet/satisfaction/satisfactionCsv.js +211 -0
  173. package/dist/planmagnet/satisfaction/satisfactionCsv.js.map +1 -0
  174. package/dist/planmagnet/satisfaction/satisfactionForm.d.ts +90 -0
  175. package/dist/planmagnet/satisfaction/satisfactionForm.d.ts.map +1 -0
  176. package/dist/planmagnet/satisfaction/satisfactionForm.js +90 -0
  177. package/dist/planmagnet/satisfaction/satisfactionForm.js.map +1 -0
  178. package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts +116 -0
  179. package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts.map +1 -0
  180. package/dist/planmagnet/satisfaction/satisfactionOperations.js +86 -0
  181. package/dist/planmagnet/satisfaction/satisfactionOperations.js.map +1 -0
  182. package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts +20 -0
  183. package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts.map +1 -0
  184. package/dist/planmagnet/satisfaction/useSatisfactionAccess.js +31 -0
  185. package/dist/planmagnet/satisfaction/useSatisfactionAccess.js.map +1 -0
  186. package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts +35 -0
  187. package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts.map +1 -0
  188. package/dist/planmagnet/skills/RequiredSkillsEditor.js +124 -0
  189. package/dist/planmagnet/skills/RequiredSkillsEditor.js.map +1 -0
  190. package/dist/planmagnet/skills/SkillChips.d.ts +30 -0
  191. package/dist/planmagnet/skills/SkillChips.d.ts.map +1 -0
  192. package/dist/planmagnet/skills/SkillChips.js +57 -0
  193. package/dist/planmagnet/skills/SkillChips.js.map +1 -0
  194. package/dist/planmagnet/skills/SkillCoverageCard.d.ts +24 -0
  195. package/dist/planmagnet/skills/SkillCoverageCard.d.ts.map +1 -0
  196. package/dist/planmagnet/skills/SkillCoverageCard.js +256 -0
  197. package/dist/planmagnet/skills/SkillCoverageCard.js.map +1 -0
  198. package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts +10 -0
  199. package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts.map +1 -0
  200. package/dist/planmagnet/skills/WorkItemRequiredSkills.js +65 -0
  201. package/dist/planmagnet/skills/WorkItemRequiredSkills.js.map +1 -0
  202. package/dist/planmagnet/skills/skillOperations.d.ts +52 -0
  203. package/dist/planmagnet/skills/skillOperations.d.ts.map +1 -0
  204. package/dist/planmagnet/skills/skillOperations.js +44 -0
  205. package/dist/planmagnet/skills/skillOperations.js.map +1 -0
  206. package/dist/planmagnet/skills/skillTags.d.ts +103 -0
  207. package/dist/planmagnet/skills/skillTags.d.ts.map +1 -0
  208. package/dist/planmagnet/skills/skillTags.js +115 -0
  209. package/dist/planmagnet/skills/skillTags.js.map +1 -0
  210. package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts +28 -0
  211. package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts.map +1 -0
  212. package/dist/planmagnet/skills/useRequiredSkillsDraft.js +30 -0
  213. package/dist/planmagnet/skills/useRequiredSkillsDraft.js.map +1 -0
  214. package/dist/shared/utils/i18n.d.ts +3 -0
  215. package/dist/shared/utils/i18n.d.ts.map +1 -1
  216. package/dist/shared/utils/i18n.js +1 -1
  217. package/dist/shared/utils/i18n.js.map +1 -1
  218. package/package.json +4 -4
@@ -0,0 +1,211 @@
1
+ import { COMMENT_MAX as e, RESPONDED_AT_EARLIEST as t, SCORE_SCALE as n, isOnScale as r, looksLikeEmail as i, respondedAtProblem as a } from "./satisfaction.js";
2
+ //#region src/planmagnet/satisfaction/satisfactionCsv.ts
3
+ var o = [
4
+ "externalId",
5
+ "metricType",
6
+ "score",
7
+ "respondedAt",
8
+ "productId",
9
+ "releaseId",
10
+ "projectId",
11
+ "isAnonymous",
12
+ "respondentName",
13
+ "respondentEmail",
14
+ "comment"
15
+ ], s = [
16
+ "externalId",
17
+ "metricType",
18
+ "score",
19
+ "respondedAt"
20
+ ], c = 5 * 1024 * 1024, l = "";
21
+ function u(e) {
22
+ let t = {
23
+ ",": 0,
24
+ ";": 0,
25
+ " ": 0
26
+ }, n = !1;
27
+ for (let r of e) if (r === "\"") n = !n;
28
+ else if (!n && (r === "\n" || r === "\r")) break;
29
+ else !n && r in t && (t[r] += 1);
30
+ let r = ",";
31
+ for (let e of [";", " "]) t[e] > t[r] && (r = e);
32
+ return r;
33
+ }
34
+ function d(e) {
35
+ let t = e.startsWith(l) ? e.slice(1) : e, n = u(t), r = [], i = [], a = "", o = !1, s = !1, c = () => {
36
+ i.push(a), a = "", s = !1;
37
+ }, d = () => {
38
+ c(), i.some((e) => e.trim() !== "") && r.push(i), i = [];
39
+ };
40
+ for (let e = 0; e < t.length; e++) {
41
+ let r = t[e];
42
+ if (o) {
43
+ r === "\"" ? t[e + 1] === "\"" ? (a += "\"", e++) : o = !1 : a += r;
44
+ continue;
45
+ }
46
+ r === "\"" && !s ? (o = !0, s = !0) : r === n ? c() : r === "\r" || r === "\n" ? (r === "\r" && t[e + 1] === "\n" && e++, d()) : (a += r, s = !0);
47
+ }
48
+ return o ? {
49
+ records: r,
50
+ unterminated: !0
51
+ } : ((a !== "" || i.length > 0) && d(), {
52
+ records: r,
53
+ unterminated: !1
54
+ });
55
+ }
56
+ function f(e) {
57
+ return e.trim().toLowerCase().replace(/[\s_-]+/g, "");
58
+ }
59
+ var p = new Map(o.map((e) => [f(e), e]));
60
+ function m(e) {
61
+ let { records: t, unterminated: n } = d(e), r = {
62
+ columns: {},
63
+ ignoredColumns: [],
64
+ records: [],
65
+ problem: null
66
+ };
67
+ if (n) return {
68
+ ...r,
69
+ problem: { code: "unterminatedQuote" }
70
+ };
71
+ let [i, ...a] = t;
72
+ if (!i) return {
73
+ ...r,
74
+ problem: { code: "empty" }
75
+ };
76
+ let o = {}, c = [], l = null;
77
+ i.forEach((e, t) => {
78
+ let n = p.get(f(e));
79
+ if (!n) {
80
+ e.trim() && c.push(e.trim());
81
+ return;
82
+ }
83
+ o[n] === void 0 ? o[n] = t : l ??= {
84
+ code: "duplicateColumn",
85
+ column: n
86
+ };
87
+ });
88
+ let u = s.filter((e) => o[e] === void 0);
89
+ return u.length > 0 && (l ??= {
90
+ code: "missingColumns",
91
+ columns: u
92
+ }), a.length === 0 && (l ??= { code: "noRows" }), a.length > 500 && (l ??= {
93
+ code: "tooManyRows",
94
+ count: a.length,
95
+ max: 500
96
+ }), {
97
+ columns: o,
98
+ ignoredColumns: c,
99
+ records: a,
100
+ problem: l
101
+ };
102
+ }
103
+ var h = new Set([
104
+ "true",
105
+ "yes",
106
+ "y",
107
+ "1"
108
+ ]), g = new Set([
109
+ "false",
110
+ "no",
111
+ "n",
112
+ "0",
113
+ ""
114
+ ]);
115
+ function _(e) {
116
+ return h.has(e.trim().toLowerCase());
117
+ }
118
+ var v = /^[+-]?\d+(?:\.0+)?$/;
119
+ function y(e, t) {
120
+ let n = {};
121
+ for (let r of o) {
122
+ let i = e.columns[r];
123
+ n[r] = i === void 0 ? "" : (t[i] ?? "").trim();
124
+ }
125
+ return n;
126
+ }
127
+ function b(o, s, c) {
128
+ let l = /* @__PURE__ */ new Map(), u = !!(s.productId || s.releaseId || s.projectId);
129
+ return o.records.map((d, f) => {
130
+ let p = f + 1, m = y(o, d), h = [], b = m.externalId;
131
+ if (!b) h.push({ code: "externalIdRequired" });
132
+ else if (b.length > 200) h.push({
133
+ code: "externalIdTooLong",
134
+ params: { max: 200 }
135
+ });
136
+ else {
137
+ let e = l.get(b);
138
+ e === void 0 ? l.set(b, p) : h.push({
139
+ code: "externalIdDuplicate",
140
+ params: { row: e }
141
+ });
142
+ }
143
+ let x = m.metricType.toUpperCase(), S = x === "CSAT" || x === "NPS" ? x : null;
144
+ m.metricType ? S || h.push({ code: "metricTypeInvalid" }) : h.push({ code: "metricTypeRequired" });
145
+ let C = null;
146
+ if (!m.score) h.push({ code: "scoreRequired" });
147
+ else if (!v.test(m.score)) h.push({ code: "scoreNotWholeNumber" });
148
+ else if (C = Number(m.score), S && !r(S, C)) {
149
+ let { min: e, max: t } = n[S];
150
+ h.push({
151
+ code: "scoreOutOfRange",
152
+ params: {
153
+ metric: S,
154
+ min: e,
155
+ max: t
156
+ }
157
+ });
158
+ }
159
+ let w = a(m.respondedAt, c);
160
+ w === "required" ? h.push({ code: "respondedAtRequired" }) : w === "format" ? h.push({ code: "respondedAtFormat" }) : w === "future" ? h.push({ code: "respondedAtInFuture" }) : w === "tooOld" && h.push({
161
+ code: "respondedAtTooOld",
162
+ params: { min: t }
163
+ });
164
+ let T = _(m.isAnonymous);
165
+ !T && !g.has(m.isAnonymous.toLowerCase()) && h.push({ code: "anonymousInvalid" });
166
+ let E = m.respondentName, D = m.respondentEmail;
167
+ E.length > 200 && h.push({
168
+ code: "respondentNameTooLong",
169
+ params: { max: 200 }
170
+ }), D && (D.length > 254 || !i(D)) && h.push({
171
+ code: "respondentEmailInvalid",
172
+ params: { max: 254 }
173
+ }), T && (E || D) && h.push({ code: "anonymousWithIdentity" });
174
+ let O = m.comment;
175
+ O.length > 5e3 && h.push({
176
+ code: "commentTooLong",
177
+ params: { max: e }
178
+ });
179
+ let k = !!(m.productId || m.releaseId || m.projectId), A = !k && u, j = k ? {
180
+ productId: m.productId,
181
+ releaseId: m.releaseId,
182
+ projectId: m.projectId
183
+ } : s;
184
+ return !j.productId && !j.releaseId && !j.projectId && h.push({ code: "linkRequired" }), {
185
+ row: p,
186
+ cells: m,
187
+ usesDefaultLinks: A,
188
+ input: h.length === 0 && S && C !== null ? {
189
+ externalId: b,
190
+ metricType: S,
191
+ score: C,
192
+ respondedAt: m.respondedAt,
193
+ ...j.productId ? { productId: j.productId } : {},
194
+ ...j.releaseId ? { releaseId: j.releaseId } : {},
195
+ ...j.projectId ? { projectId: j.projectId } : {},
196
+ isAnonymous: T,
197
+ ...E ? { respondentName: E } : {},
198
+ ...D ? { respondentEmail: D } : {},
199
+ ...O ? { comment: O } : {}
200
+ } : null,
201
+ problems: h
202
+ };
203
+ });
204
+ }
205
+ function x() {
206
+ return `${l}${o.join(",")}\r\n`;
207
+ }
208
+ //#endregion
209
+ export { o as IMPORT_COLUMNS, c as MAX_FILE_BYTES, s as REQUIRED_COLUMNS, _ as cellIsTrue, m as parseImportFile, b as previewRows, x as templateCsv };
210
+
211
+ //# sourceMappingURL=satisfactionCsv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"satisfactionCsv.js","names":[],"sources":["../../../src/planmagnet/satisfaction/satisfactionCsv.ts"],"sourcesContent":["import {\n COMMENT_MAX,\n EXTERNAL_ID_MAX,\n MAX_IMPORT_ROWS,\n RESPONDED_AT_EARLIEST,\n RESPONDENT_EMAIL_MAX,\n RESPONDENT_NAME_MAX,\n SCORE_SCALE,\n isOnScale,\n looksLikeEmail,\n respondedAtProblem,\n type SatisfactionMetricType,\n} from './satisfaction';\n\n/**\n * ANR-21 CSV import, read in the browser: parse the file, map its header, and turn each row into exactly the\n * `PlanmagnetCustomerSatisfactionImportRow` the service takes — with every problem the service would refuse it for,\n * so the preview can show them by row before anything is sent. The service re-checks every row (and whether each\n * linked product, release and project exists) and is the only gate; the import is all or nothing there.\n *\n * Rows are numbered from 1 in the order they are sent, which is the order of the data rows in the file (blank lines\n * dropped) — the same numbering the service uses in `extensions.rowErrors`. No React here: tests/satisfactionCsv.test.ts.\n */\n\n/** The columns of an import file, in template order. */\nexport const IMPORT_COLUMNS = [\n 'externalId',\n 'metricType',\n 'score',\n 'respondedAt',\n 'productId',\n 'releaseId',\n 'projectId',\n 'isAnonymous',\n 'respondentName',\n 'respondentEmail',\n 'comment',\n] as const;\n\nexport type ImportColumn = (typeof IMPORT_COLUMNS)[number];\n\nexport const REQUIRED_COLUMNS: readonly ImportColumn[] = [\n 'externalId',\n 'metricType',\n 'score',\n 'respondedAt',\n];\n\n/** A file larger than this is refused before it is read: 500 rows of 5000-character comments still fit. */\nexport const MAX_FILE_BYTES = 5 * 1024 * 1024;\n\nconst UTF8_BOM = String.fromCharCode(0xfeff);\n\n// ─── CSV ────────────────────────────────────────────────────────────────────\n\n/**\n * The delimiter of a file: the one of comma, semicolon (spreadsheets in many locales) or tab that occurs most often in\n * its first line outside quotes. Comma when there is none.\n */\nexport function detectDelimiter(text: string): string {\n const counts: Record<string, number> = { ',': 0, ';': 0, '\\t': 0 };\n let quoted = false;\n for (const c of text) {\n if (c === '\"') quoted = !quoted;\n else if (!quoted && (c === '\\n' || c === '\\r')) break;\n else if (!quoted && c in counts) counts[c] += 1;\n }\n let best = ',';\n for (const candidate of [';', '\\t']) if (counts[candidate] > counts[best]) best = candidate;\n return best;\n}\n\n/**\n * RFC 4180 records: quoted fields may hold the delimiter, line breaks and doubled quotes; CRLF, LF and CR all end a\n * record. A leading byte-order mark is dropped. Lines with nothing but delimiters and spaces are dropped. `unterminated`\n * is true when the file ends inside a quoted field — the file is malformed and nothing in it can be trusted.\n */\nexport function parseCsv(input: string): { records: string[][]; unterminated: boolean } {\n const text = input.startsWith(UTF8_BOM) ? input.slice(1) : input;\n const delimiter = detectDelimiter(text);\n const records: string[][] = [];\n let record: string[] = [];\n let field = '';\n let quoted = false;\n let fieldStarted = false;\n\n const endField = () => {\n record.push(field);\n field = '';\n fieldStarted = false;\n };\n const endRecord = () => {\n endField();\n if (record.some((cell) => cell.trim() !== '')) records.push(record);\n record = [];\n };\n\n for (let i = 0; i < text.length; i++) {\n const c = text[i];\n if (quoted) {\n if (c === '\"') {\n if (text[i + 1] === '\"') {\n field += '\"';\n i++;\n } else {\n quoted = false;\n }\n } else {\n field += c;\n }\n continue;\n }\n if (c === '\"' && !fieldStarted) {\n quoted = true;\n fieldStarted = true;\n } else if (c === delimiter) {\n endField();\n } else if (c === '\\r' || c === '\\n') {\n if (c === '\\r' && text[i + 1] === '\\n') i++;\n endRecord();\n } else {\n // A quote inside an unquoted field is kept as written.\n field += c;\n fieldStarted = true;\n }\n }\n if (quoted) return { records, unterminated: true };\n if (field !== '' || record.length > 0) endRecord();\n return { records, unterminated: false };\n}\n\n/** A header cell compared without case, spaces, dashes or underscores: \"External ID\" and \"external_id\" both match. */\nfunction headerKey(cell: string): string {\n return cell\n .trim()\n .toLowerCase()\n .replace(/[\\s_-]+/g, '');\n}\n\nconst COLUMN_BY_KEY = new Map<string, ImportColumn>(IMPORT_COLUMNS.map((c) => [headerKey(c), c]));\n\nexport type FileProblem =\n | { code: 'unterminatedQuote' }\n | { code: 'empty' }\n | { code: 'missingColumns'; columns: ImportColumn[] }\n | { code: 'duplicateColumn'; column: ImportColumn }\n | { code: 'noRows' }\n | { code: 'tooManyRows'; count: number; max: number };\n\nexport interface ParsedImportFile {\n /** The position of each column the header names. */\n columns: Partial<Record<ImportColumn, number>>;\n /** Header cells that name no column: ignored, and listed so a misspelt header is visible. */\n ignoredColumns: string[];\n /** The data records, header excluded, in file order. */\n records: string[][];\n /** Why nothing in the file can be imported, or null. */\n problem: FileProblem | null;\n}\n\n/** Parse a whole import file and check its shape: a header with the required columns, then 1-500 rows. */\nexport function parseImportFile(text: string): ParsedImportFile {\n const { records, unterminated } = parseCsv(text);\n const empty: ParsedImportFile = { columns: {}, ignoredColumns: [], records: [], problem: null };\n if (unterminated) return { ...empty, problem: { code: 'unterminatedQuote' } };\n const [header, ...rows] = records;\n if (!header) return { ...empty, problem: { code: 'empty' } };\n\n const columns: Partial<Record<ImportColumn, number>> = {};\n const ignoredColumns: string[] = [];\n let problem: FileProblem | null = null;\n header.forEach((cell, index) => {\n const column = COLUMN_BY_KEY.get(headerKey(cell));\n if (!column) {\n if (cell.trim()) ignoredColumns.push(cell.trim());\n return;\n }\n if (columns[column] !== undefined) problem ??= { code: 'duplicateColumn', column };\n else columns[column] = index;\n });\n const missing = REQUIRED_COLUMNS.filter((column) => columns[column] === undefined);\n if (missing.length > 0) problem ??= { code: 'missingColumns', columns: missing };\n if (rows.length === 0) problem ??= { code: 'noRows' };\n if (rows.length > MAX_IMPORT_ROWS) {\n problem ??= { code: 'tooManyRows', count: rows.length, max: MAX_IMPORT_ROWS };\n }\n return { columns, ignoredColumns, records: rows, problem };\n}\n\n// ─── Rows ───────────────────────────────────────────────────────────────────\n\n/** Exactly the fields of `PlanmagnetCustomerSatisfactionImportRow` — nothing else may ride in the variables. */\nexport interface ImportRowInput {\n externalId: string;\n metricType: SatisfactionMetricType;\n score: number;\n respondedAt: string;\n productId?: string;\n releaseId?: string;\n projectId?: string;\n isAnonymous: boolean;\n respondentName?: string;\n respondentEmail?: string;\n comment?: string;\n}\n\nexport type RowProblemCode =\n | 'externalIdRequired'\n | 'externalIdTooLong'\n | 'externalIdDuplicate'\n | 'metricTypeRequired'\n | 'metricTypeInvalid'\n | 'scoreRequired'\n | 'scoreNotWholeNumber'\n | 'scoreOutOfRange'\n | 'respondedAtRequired'\n | 'respondedAtFormat'\n | 'respondedAtInFuture'\n | 'respondedAtTooOld'\n | 'anonymousInvalid'\n | 'anonymousWithIdentity'\n | 'respondentNameTooLong'\n | 'respondentEmailInvalid'\n | 'commentTooLong'\n | 'linkRequired';\n\nexport interface RowProblem {\n code: RowProblemCode;\n /** Values for the message ({{max}}, {{row}}, {{metric}}, {{min}}). */\n params?: Record<string, string | number>;\n}\n\n/** The links a row without any of its own gets ('' = none). Set in the import dialog, never inferred. */\nexport interface DefaultLinks {\n productId: string;\n releaseId: string;\n projectId: string;\n}\n\nexport interface PreviewRow {\n /** 1-based, in sending order — the number the service reports a refused row by. */\n row: number;\n /** Each column's text, trimmed ('' when the file has no such column). */\n cells: Record<ImportColumn, string>;\n /** True when the row names no product, release or project and takes the dialog's default links. */\n usesDefaultLinks: boolean;\n /** What would be sent for this row; null while it has a problem. */\n input: ImportRowInput | null;\n problems: RowProblem[];\n}\n\nconst TRUE_WORDS = new Set(['true', 'yes', 'y', '1']);\nconst FALSE_WORDS = new Set(['false', 'no', 'n', '0', '']);\n\n/** True when an isAnonymous cell says yes (true / yes / y / 1, any case). */\nexport function cellIsTrue(text: string): boolean {\n return TRUE_WORDS.has(text.trim().toLowerCase());\n}\nconst WHOLE_NUMBER = /^[+-]?\\d+(?:\\.0+)?$/;\n\nfunction cellsOf(file: ParsedImportFile, record: string[]): Record<ImportColumn, string> {\n const cells = {} as Record<ImportColumn, string>;\n for (const column of IMPORT_COLUMNS) {\n const index = file.columns[column];\n cells[column] = index === undefined ? '' : (record[index] ?? '').trim();\n }\n return cells;\n}\n\n/**\n * Every data row as it would be sent, with the problems the service would refuse it for. `defaults` fill the links of\n * a row that names no product, release or project at all — a row with any link of its own keeps exactly its own, so a\n * default product can never contradict a release the file names.\n */\nexport function previewRows(\n file: ParsedImportFile,\n defaults: DefaultLinks,\n now: Date\n): PreviewRow[] {\n const firstRowOf = new Map<string, number>();\n const hasDefaults = !!(defaults.productId || defaults.releaseId || defaults.projectId);\n\n return file.records.map((record, index) => {\n const row = index + 1;\n const cells = cellsOf(file, record);\n const problems: RowProblem[] = [];\n\n const externalId = cells.externalId;\n if (!externalId) problems.push({ code: 'externalIdRequired' });\n else if (externalId.length > EXTERNAL_ID_MAX) {\n problems.push({ code: 'externalIdTooLong', params: { max: EXTERNAL_ID_MAX } });\n } else {\n const first = firstRowOf.get(externalId);\n if (first !== undefined)\n problems.push({ code: 'externalIdDuplicate', params: { row: first } });\n else firstRowOf.set(externalId, row);\n }\n\n const metricText = cells.metricType.toUpperCase();\n const metricType: SatisfactionMetricType | null =\n metricText === 'CSAT' || metricText === 'NPS' ? metricText : null;\n if (!cells.metricType) problems.push({ code: 'metricTypeRequired' });\n else if (!metricType) problems.push({ code: 'metricTypeInvalid' });\n\n let score: number | null = null;\n if (!cells.score) problems.push({ code: 'scoreRequired' });\n else if (!WHOLE_NUMBER.test(cells.score)) problems.push({ code: 'scoreNotWholeNumber' });\n else {\n score = Number(cells.score);\n if (metricType && !isOnScale(metricType, score)) {\n const { min, max } = SCORE_SCALE[metricType];\n problems.push({ code: 'scoreOutOfRange', params: { metric: metricType, min, max } });\n }\n }\n\n const dateProblem = respondedAtProblem(cells.respondedAt, now);\n if (dateProblem === 'required') problems.push({ code: 'respondedAtRequired' });\n else if (dateProblem === 'format') problems.push({ code: 'respondedAtFormat' });\n else if (dateProblem === 'future') problems.push({ code: 'respondedAtInFuture' });\n else if (dateProblem === 'tooOld') {\n problems.push({ code: 'respondedAtTooOld', params: { min: RESPONDED_AT_EARLIEST } });\n }\n\n const isAnonymous = cellIsTrue(cells.isAnonymous);\n if (!isAnonymous && !FALSE_WORDS.has(cells.isAnonymous.toLowerCase())) {\n problems.push({ code: 'anonymousInvalid' });\n }\n\n const name = cells.respondentName;\n const email = cells.respondentEmail;\n if (name.length > RESPONDENT_NAME_MAX) {\n problems.push({ code: 'respondentNameTooLong', params: { max: RESPONDENT_NAME_MAX } });\n }\n if (email && (email.length > RESPONDENT_EMAIL_MAX || !looksLikeEmail(email))) {\n problems.push({ code: 'respondentEmailInvalid', params: { max: RESPONDENT_EMAIL_MAX } });\n }\n if (isAnonymous && (name || email)) problems.push({ code: 'anonymousWithIdentity' });\n\n const comment = cells.comment;\n if (comment.length > COMMENT_MAX) {\n problems.push({ code: 'commentTooLong', params: { max: COMMENT_MAX } });\n }\n\n const ownLinks = !!(cells.productId || cells.releaseId || cells.projectId);\n const usesDefaultLinks = !ownLinks && hasDefaults;\n const links = ownLinks\n ? { productId: cells.productId, releaseId: cells.releaseId, projectId: cells.projectId }\n : defaults;\n if (!links.productId && !links.releaseId && !links.projectId) {\n problems.push({ code: 'linkRequired' });\n }\n\n const input: ImportRowInput | null =\n problems.length === 0 && metricType && score !== null\n ? {\n externalId,\n metricType,\n score,\n respondedAt: cells.respondedAt,\n ...(links.productId ? { productId: links.productId } : {}),\n ...(links.releaseId ? { releaseId: links.releaseId } : {}),\n ...(links.projectId ? { projectId: links.projectId } : {}),\n isAnonymous,\n ...(name ? { respondentName: name } : {}),\n ...(email ? { respondentEmail: email } : {}),\n ...(comment ? { comment } : {}),\n }\n : null;\n\n return { row, cells, usesDefaultLinks, input, problems };\n });\n}\n\n/** The template: the header row only — an example row imported by mistake would count as a real answer. */\nexport function templateCsv(): string {\n return `${UTF8_BOM}${IMPORT_COLUMNS.join(',')}\\r\\n`;\n}\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { SatisfactionMetricType } from './satisfaction';
2
+
3
+ /**
4
+ * ANR-21 add / edit form, framework-free: the form's values, what the service would refuse them for, and exactly the
5
+ * create or update input they become (tests/satisfaction.test.ts). Inputs carry only fields of the SDL input types —
6
+ * an extra field in the variables fails every save, and the document-level drift gates cannot see it.
7
+ */
8
+ export interface ResponseFormValues {
9
+ metricType: SatisfactionMetricType;
10
+ /** Null until a score is picked (and again when the metric changes to a scale the old score is not on). */
11
+ score: number | null;
12
+ /** YYYY-MM-DD — the day the customer answered, sent as a calendar date (that day's UTC midnight). */
13
+ respondedAt: string;
14
+ productId: string;
15
+ releaseId: string;
16
+ projectId: string;
17
+ isAnonymous: boolean;
18
+ respondentName: string;
19
+ respondentEmail: string;
20
+ comment: string;
21
+ /** Create only: source and externalId never change after that. */
22
+ externalId: string;
23
+ }
24
+ /** The stored response a form edits — the fields of `SatisfactionResponse` it needs. */
25
+ export interface StoredResponse {
26
+ metricType: SatisfactionMetricType;
27
+ score: number;
28
+ respondedAt: string;
29
+ productId: string | null;
30
+ releaseId: string | null;
31
+ projectId: string | null;
32
+ isAnonymous: boolean;
33
+ respondentName: string | null;
34
+ respondentEmail: string | null;
35
+ comment: string | null;
36
+ externalId: string | null;
37
+ }
38
+ export interface FormLinks {
39
+ productId: string;
40
+ releaseId: string;
41
+ projectId: string;
42
+ }
43
+ /** A new response's starting values: CSAT, no score yet, answered today, linked to whatever the page is filtered to. */
44
+ export declare function newFormValues(today: string, links: FormLinks): ResponseFormValues;
45
+ /** A stored response as form values. Its answer day is its UTC day — the day the figures count it under. */
46
+ export declare function formValuesOf(response: StoredResponse): ResponseFormValues;
47
+ /** A new metric keeps the score only when it is on the new scale. */
48
+ export declare function withMetricType(values: ResponseFormValues, metricType: SatisfactionMetricType): ResponseFormValues;
49
+ export type FormField = 'score' | 'respondedAt' | 'links' | 'respondentName' | 'respondentEmail' | 'comment' | 'externalId';
50
+ export type FormProblem = 'scoreRequired' | 'scoreOutOfRange' | 'respondedAtRequired' | 'respondedAtFormat' | 'respondedAtInFuture' | 'respondedAtTooOld' | 'linkRequired' | 'respondentNameTooLong' | 'respondentEmailInvalid' | 'commentTooLong' | 'externalIdTooLong';
51
+ /** What the service would refuse these values for, by field. Empty when they can be sent. */
52
+ export declare function formProblems(values: ResponseFormValues, now: Date, isNew: boolean): Partial<Record<FormField, FormProblem>>;
53
+ /** `PlanmagnetCreateCustomerSatisfactionResponseInput`, field for field. */
54
+ export interface CreateResponseInput {
55
+ metricType: SatisfactionMetricType;
56
+ score: number;
57
+ respondedAt: string;
58
+ productId?: string;
59
+ releaseId?: string;
60
+ projectId?: string;
61
+ isAnonymous: boolean;
62
+ respondentName?: string;
63
+ respondentEmail?: string;
64
+ comment?: string;
65
+ externalId?: string;
66
+ }
67
+ /** `PlanmagnetUpdateCustomerSatisfactionResponseInput`: an omitted field keeps its value, null clears it. */
68
+ export interface UpdateResponseInput {
69
+ metricType?: SatisfactionMetricType;
70
+ score?: number;
71
+ respondedAt?: string;
72
+ productId?: string | null;
73
+ releaseId?: string | null;
74
+ projectId?: string | null;
75
+ isAnonymous?: boolean;
76
+ respondentName?: string | null;
77
+ respondentEmail?: string | null;
78
+ comment?: string | null;
79
+ }
80
+ /** The create input for valid values (formProblems is empty). An anonymous response sends no name or email. */
81
+ export declare function createInput(values: ResponseFormValues & {
82
+ score: number;
83
+ }): CreateResponseInput;
84
+ /**
85
+ * Only what changed, so a save never rewrites a field someone else edited meanwhile. A cleared link or text is sent as
86
+ * null (clears it). Making a response anonymous sends `isAnonymous: true` alone — the service then clears the stored
87
+ * name and email itself; a name or email sent with it would be refused.
88
+ */
89
+ export declare function updateInput(initial: ResponseFormValues, next: ResponseFormValues): UpdateResponseInput;
90
+ //# sourceMappingURL=satisfactionForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"satisfactionForm.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/satisfactionForm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { isOnScale as e, looksLikeEmail as t, respondedAtProblem as n, utcDay as r } from "./satisfaction.js";
2
+ //#region src/planmagnet/satisfaction/satisfactionForm.ts
3
+ function i(e, t) {
4
+ return {
5
+ metricType: "CSAT",
6
+ score: null,
7
+ respondedAt: e,
8
+ ...t,
9
+ isAnonymous: !1,
10
+ respondentName: "",
11
+ respondentEmail: "",
12
+ comment: "",
13
+ externalId: ""
14
+ };
15
+ }
16
+ function a(e) {
17
+ return {
18
+ metricType: e.metricType,
19
+ score: e.score,
20
+ respondedAt: r(e.respondedAt) ?? "",
21
+ productId: e.productId ?? "",
22
+ releaseId: e.releaseId ?? "",
23
+ projectId: e.projectId ?? "",
24
+ isAnonymous: e.isAnonymous,
25
+ respondentName: e.respondentName ?? "",
26
+ respondentEmail: e.respondentEmail ?? "",
27
+ comment: e.comment ?? "",
28
+ externalId: e.externalId ?? ""
29
+ };
30
+ }
31
+ function o(t, n) {
32
+ let r = t.score !== null && e(n, t.score) ? t.score : null;
33
+ return {
34
+ ...t,
35
+ metricType: n,
36
+ score: r
37
+ };
38
+ }
39
+ var s = {
40
+ required: "respondedAtRequired",
41
+ format: "respondedAtFormat",
42
+ future: "respondedAtInFuture",
43
+ tooOld: "respondedAtTooOld"
44
+ };
45
+ function c(r, i, a) {
46
+ let o = {};
47
+ r.score === null ? o.score = "scoreRequired" : e(r.metricType, r.score) || (o.score = "scoreOutOfRange");
48
+ let c = n(r.respondedAt, i);
49
+ if (c && (o.respondedAt = s[c]), !r.productId && !r.releaseId && !r.projectId && (o.links = "linkRequired"), !r.isAnonymous) {
50
+ let e = r.respondentName.trim(), n = r.respondentEmail.trim();
51
+ e.length > 200 && (o.respondentName = "respondentNameTooLong"), n && (n.length > 254 || !t(n)) && (o.respondentEmail = "respondentEmailInvalid");
52
+ }
53
+ return r.comment.trim().length > 5e3 && (o.comment = "commentTooLong"), a && r.externalId.trim().length > 200 && (o.externalId = "externalIdTooLong"), o;
54
+ }
55
+ var l = (e) => {
56
+ let t = e.trim();
57
+ return t === "" ? null : t;
58
+ };
59
+ function u(e) {
60
+ let t = e.isAnonymous ? null : l(e.respondentName), n = e.isAnonymous ? null : l(e.respondentEmail), r = l(e.comment), i = l(e.externalId);
61
+ return {
62
+ metricType: e.metricType,
63
+ score: e.score,
64
+ respondedAt: e.respondedAt,
65
+ ...e.productId ? { productId: e.productId } : {},
66
+ ...e.releaseId ? { releaseId: e.releaseId } : {},
67
+ ...e.projectId ? { projectId: e.projectId } : {},
68
+ isAnonymous: e.isAnonymous,
69
+ ...t ? { respondentName: t } : {},
70
+ ...n ? { respondentEmail: n } : {},
71
+ ...r ? { comment: r } : {},
72
+ ...i ? { externalId: i } : {}
73
+ };
74
+ }
75
+ function d(e, t) {
76
+ let n = {};
77
+ if (t.metricType !== e.metricType && (n.metricType = t.metricType), t.score !== null && t.score !== e.score && (n.score = t.score), t.respondedAt !== e.respondedAt && (n.respondedAt = t.respondedAt), t.productId !== e.productId && (n.productId = t.productId || null), t.releaseId !== e.releaseId && (n.releaseId = t.releaseId || null), t.projectId !== e.projectId && (n.projectId = t.projectId || null), t.isAnonymous !== e.isAnonymous && (n.isAnonymous = t.isAnonymous), !t.isAnonymous) {
78
+ let r = e.isAnonymous ? {
79
+ name: null,
80
+ email: null
81
+ } : null, i = l(t.respondentName), a = l(t.respondentEmail);
82
+ i !== (r ? r.name : l(e.respondentName)) && (n.respondentName = i), a !== (r ? r.email : l(e.respondentEmail)) && (n.respondentEmail = a);
83
+ }
84
+ let r = l(t.comment);
85
+ return r !== l(e.comment) && (n.comment = r), n;
86
+ }
87
+ //#endregion
88
+ export { u as createInput, c as formProblems, a as formValuesOf, i as newFormValues, d as updateInput, o as withMetricType };
89
+
90
+ //# sourceMappingURL=satisfactionForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"satisfactionForm.js","names":[],"sources":["../../../src/planmagnet/satisfaction/satisfactionForm.ts"],"sourcesContent":["import {\n COMMENT_MAX,\n EXTERNAL_ID_MAX,\n RESPONDENT_EMAIL_MAX,\n RESPONDENT_NAME_MAX,\n isOnScale,\n looksLikeEmail,\n respondedAtProblem,\n utcDay,\n type SatisfactionMetricType,\n} from './satisfaction';\n\n/**\n * ANR-21 add / edit form, framework-free: the form's values, what the service would refuse them for, and exactly the\n * create or update input they become (tests/satisfaction.test.ts). Inputs carry only fields of the SDL input types —\n * an extra field in the variables fails every save, and the document-level drift gates cannot see it.\n */\n\nexport interface ResponseFormValues {\n metricType: SatisfactionMetricType;\n /** Null until a score is picked (and again when the metric changes to a scale the old score is not on). */\n score: number | null;\n /** YYYY-MM-DD — the day the customer answered, sent as a calendar date (that day's UTC midnight). */\n respondedAt: string;\n productId: string;\n releaseId: string;\n projectId: string;\n isAnonymous: boolean;\n respondentName: string;\n respondentEmail: string;\n comment: string;\n /** Create only: source and externalId never change after that. */\n externalId: string;\n}\n\n/** The stored response a form edits — the fields of `SatisfactionResponse` it needs. */\nexport interface StoredResponse {\n metricType: SatisfactionMetricType;\n score: number;\n respondedAt: string;\n productId: string | null;\n releaseId: string | null;\n projectId: string | null;\n isAnonymous: boolean;\n respondentName: string | null;\n respondentEmail: string | null;\n comment: string | null;\n externalId: string | null;\n}\n\nexport interface FormLinks {\n productId: string;\n releaseId: string;\n projectId: string;\n}\n\n/** A new response's starting values: CSAT, no score yet, answered today, linked to whatever the page is filtered to. */\nexport function newFormValues(today: string, links: FormLinks): ResponseFormValues {\n return {\n metricType: 'CSAT',\n score: null,\n respondedAt: today,\n ...links,\n isAnonymous: false,\n respondentName: '',\n respondentEmail: '',\n comment: '',\n externalId: '',\n };\n}\n\n/** A stored response as form values. Its answer day is its UTC day — the day the figures count it under. */\nexport function formValuesOf(response: StoredResponse): ResponseFormValues {\n return {\n metricType: response.metricType,\n score: response.score,\n respondedAt: utcDay(response.respondedAt) ?? '',\n productId: response.productId ?? '',\n releaseId: response.releaseId ?? '',\n projectId: response.projectId ?? '',\n isAnonymous: response.isAnonymous,\n respondentName: response.respondentName ?? '',\n respondentEmail: response.respondentEmail ?? '',\n comment: response.comment ?? '',\n externalId: response.externalId ?? '',\n };\n}\n\n/** A new metric keeps the score only when it is on the new scale. */\nexport function withMetricType(\n values: ResponseFormValues,\n metricType: SatisfactionMetricType\n): ResponseFormValues {\n const score = values.score !== null && isOnScale(metricType, values.score) ? values.score : null;\n return { ...values, metricType, score };\n}\n\nexport type FormField =\n | 'score'\n | 'respondedAt'\n | 'links'\n | 'respondentName'\n | 'respondentEmail'\n | 'comment'\n | 'externalId';\n\nexport type FormProblem =\n | 'scoreRequired'\n | 'scoreOutOfRange'\n | 'respondedAtRequired'\n | 'respondedAtFormat'\n | 'respondedAtInFuture'\n | 'respondedAtTooOld'\n | 'linkRequired'\n | 'respondentNameTooLong'\n | 'respondentEmailInvalid'\n | 'commentTooLong'\n | 'externalIdTooLong';\n\nconst RESPONDED_AT_PROBLEM = {\n required: 'respondedAtRequired',\n format: 'respondedAtFormat',\n future: 'respondedAtInFuture',\n tooOld: 'respondedAtTooOld',\n} as const;\n\n/** What the service would refuse these values for, by field. Empty when they can be sent. */\nexport function formProblems(\n values: ResponseFormValues,\n now: Date,\n isNew: boolean\n): Partial<Record<FormField, FormProblem>> {\n const out: Partial<Record<FormField, FormProblem>> = {};\n if (values.score === null) out.score = 'scoreRequired';\n else if (!isOnScale(values.metricType, values.score)) out.score = 'scoreOutOfRange';\n const dateProblem = respondedAtProblem(values.respondedAt, now);\n if (dateProblem) out.respondedAt = RESPONDED_AT_PROBLEM[dateProblem];\n if (!values.productId && !values.releaseId && !values.projectId) out.links = 'linkRequired';\n if (!values.isAnonymous) {\n const name = values.respondentName.trim();\n const email = values.respondentEmail.trim();\n if (name.length > RESPONDENT_NAME_MAX) out.respondentName = 'respondentNameTooLong';\n if (email && (email.length > RESPONDENT_EMAIL_MAX || !looksLikeEmail(email))) {\n out.respondentEmail = 'respondentEmailInvalid';\n }\n }\n if (values.comment.trim().length > COMMENT_MAX) out.comment = 'commentTooLong';\n if (isNew && values.externalId.trim().length > EXTERNAL_ID_MAX)\n out.externalId = 'externalIdTooLong';\n return out;\n}\n\n/** `PlanmagnetCreateCustomerSatisfactionResponseInput`, field for field. */\nexport interface CreateResponseInput {\n metricType: SatisfactionMetricType;\n score: number;\n respondedAt: string;\n productId?: string;\n releaseId?: string;\n projectId?: string;\n isAnonymous: boolean;\n respondentName?: string;\n respondentEmail?: string;\n comment?: string;\n externalId?: string;\n}\n\n/** `PlanmagnetUpdateCustomerSatisfactionResponseInput`: an omitted field keeps its value, null clears it. */\nexport interface UpdateResponseInput {\n metricType?: SatisfactionMetricType;\n score?: number;\n respondedAt?: string;\n productId?: string | null;\n releaseId?: string | null;\n projectId?: string | null;\n isAnonymous?: boolean;\n respondentName?: string | null;\n respondentEmail?: string | null;\n comment?: string | null;\n}\n\nconst text = (value: string): string | null => {\n const trimmed = value.trim();\n return trimmed === '' ? null : trimmed;\n};\n\n/** The create input for valid values (formProblems is empty). An anonymous response sends no name or email. */\nexport function createInput(values: ResponseFormValues & { score: number }): CreateResponseInput {\n const name = values.isAnonymous ? null : text(values.respondentName);\n const email = values.isAnonymous ? null : text(values.respondentEmail);\n const comment = text(values.comment);\n const externalId = text(values.externalId);\n return {\n metricType: values.metricType,\n score: values.score,\n respondedAt: values.respondedAt,\n ...(values.productId ? { productId: values.productId } : {}),\n ...(values.releaseId ? { releaseId: values.releaseId } : {}),\n ...(values.projectId ? { projectId: values.projectId } : {}),\n isAnonymous: values.isAnonymous,\n ...(name ? { respondentName: name } : {}),\n ...(email ? { respondentEmail: email } : {}),\n ...(comment ? { comment } : {}),\n ...(externalId ? { externalId } : {}),\n };\n}\n\n/**\n * Only what changed, so a save never rewrites a field someone else edited meanwhile. A cleared link or text is sent as\n * null (clears it). Making a response anonymous sends `isAnonymous: true` alone — the service then clears the stored\n * name and email itself; a name or email sent with it would be refused.\n */\nexport function updateInput(\n initial: ResponseFormValues,\n next: ResponseFormValues\n): UpdateResponseInput {\n const out: UpdateResponseInput = {};\n if (next.metricType !== initial.metricType) out.metricType = next.metricType;\n if (next.score !== null && next.score !== initial.score) out.score = next.score;\n if (next.respondedAt !== initial.respondedAt) out.respondedAt = next.respondedAt;\n if (next.productId !== initial.productId) out.productId = next.productId || null;\n if (next.releaseId !== initial.releaseId) out.releaseId = next.releaseId || null;\n if (next.projectId !== initial.projectId) out.projectId = next.projectId || null;\n if (next.isAnonymous !== initial.isAnonymous) out.isAnonymous = next.isAnonymous;\n if (!next.isAnonymous) {\n // Leaving anonymity, the stored identity is empty: whatever was typed is new.\n const before = initial.isAnonymous ? { name: null, email: null } : null;\n const name = text(next.respondentName);\n const email = text(next.respondentEmail);\n if (name !== (before ? before.name : text(initial.respondentName))) out.respondentName = name;\n if (email !== (before ? before.email : text(initial.respondentEmail))) {\n out.respondentEmail = email;\n }\n }\n const comment = text(next.comment);\n if (comment !== text(initial.comment)) out.comment = comment;\n return out;\n}\n"],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import { SatisfactionMetricType, WindowPreset } from './satisfaction';
2
+
3
+ /** CSAT and NPS for the workspace, or one product / release / project, over a window (default: trailing 90 days). */
4
+ export declare const CUSTOMER_SATISFACTION_SUMMARY: import('graphql').DocumentNode;
5
+ /** One page of responses, newest answer first. */
6
+ export declare const CUSTOMER_SATISFACTION_RESPONSES: import('graphql').DocumentNode;
7
+ /**
8
+ * Releases for the release pickers, with their product: a response's release must belong to the product named with
9
+ * it (or to no product), so the pickers offer only those. Paged — see useSatisfactionReleaseOptions.
10
+ */
11
+ export declare const CUSTOMER_SATISFACTION_RELEASE_OPTIONS: import('graphql').DocumentNode;
12
+ export declare const CREATE_CUSTOMER_SATISFACTION_RESPONSE: import('graphql').DocumentNode;
13
+ export declare const UPDATE_CUSTOMER_SATISFACTION_RESPONSE: import('graphql').DocumentNode;
14
+ export declare const DELETE_CUSTOMER_SATISFACTION_RESPONSE: import('graphql').DocumentNode;
15
+ export declare const IMPORT_CUSTOMER_SATISFACTION_RESPONSES: import('graphql').DocumentNode;
16
+ /** Every query that shows a satisfaction figure or a response — refetched after any satisfaction write. */
17
+ export declare const SATISFACTION_QUERIES: string[];
18
+ export interface CsatFigure {
19
+ /** satisfiedCount * 100 / responseCount, exact. Null below minimumResponses — never 0. */
20
+ score: number | null;
21
+ responseCount: number;
22
+ satisfiedCount: number;
23
+ minimumResponses: number;
24
+ notEnoughResponses: boolean;
25
+ }
26
+ export interface NpsFigure {
27
+ /** (promoters - detractors) * 100 / responseCount, exact, -100..100. Null below minimumResponses. */
28
+ score: number | null;
29
+ responseCount: number;
30
+ promoterCount: number;
31
+ passiveCount: number;
32
+ detractorCount: number;
33
+ minimumResponses: number;
34
+ notEnoughResponses: boolean;
35
+ }
36
+ export interface SatisfactionWindowEcho {
37
+ preset: WindowPreset;
38
+ /** The start of the first UTC day counted. */
39
+ from: string;
40
+ /** The end of the last UTC day counted. */
41
+ to: string;
42
+ }
43
+ export interface CustomerSatisfactionSummary {
44
+ productId: string | null;
45
+ releaseId: string | null;
46
+ projectId: string | null;
47
+ window: SatisfactionWindowEcho;
48
+ csat: CsatFigure;
49
+ nps: NpsFigure;
50
+ }
51
+ export interface CustomerSatisfactionSummaryData {
52
+ planmagnetCustomerSatisfaction: CustomerSatisfactionSummary;
53
+ }
54
+ export interface SatisfactionResponse {
55
+ id: string;
56
+ metricType: SatisfactionMetricType;
57
+ score: number;
58
+ respondedAt: string;
59
+ comment: string | null;
60
+ isAnonymous: boolean;
61
+ respondentName: string | null;
62
+ respondentEmail: string | null;
63
+ /** MANUAL | IMPORT — kept a string so a value added later still renders. */
64
+ source: string;
65
+ externalId: string | null;
66
+ productId: string | null;
67
+ releaseId: string | null;
68
+ projectId: string | null;
69
+ /** Null when there is no product link, or the product was deleted since. */
70
+ product: {
71
+ id: string;
72
+ name: string;
73
+ } | null;
74
+ release: {
75
+ id: string;
76
+ name: string;
77
+ version: string;
78
+ } | null;
79
+ project: {
80
+ id: string;
81
+ name: string;
82
+ } | null;
83
+ createdAt: string;
84
+ updatedAt: string;
85
+ }
86
+ export interface CustomerSatisfactionResponsesData {
87
+ planmagnetCustomerSatisfactionResponses: {
88
+ items: SatisfactionResponse[];
89
+ pageInfo: {
90
+ hasNextPage: boolean;
91
+ endCursor: string | null;
92
+ };
93
+ totalCount: number;
94
+ };
95
+ }
96
+ export interface SatisfactionReleaseOption {
97
+ id: string;
98
+ name: string;
99
+ version: string;
100
+ productId: string | null;
101
+ }
102
+ export interface CustomerSatisfactionReleaseOptionsData {
103
+ planmagnetReleases: {
104
+ items: SatisfactionReleaseOption[];
105
+ pageInfo: {
106
+ hasNextPage: boolean;
107
+ endCursor: string | null;
108
+ };
109
+ };
110
+ }
111
+ export interface ImportCustomerSatisfactionResult {
112
+ createdCount: number;
113
+ skippedCount: number;
114
+ skippedExternalIds: string[];
115
+ }
116
+ //# sourceMappingURL=satisfactionOperations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"satisfactionOperations.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/satisfactionOperations.ts"],"names":[],"mappings":"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"}