@burdenoff/microfe-planmagnet 2026.1002.2 → 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.
- package/dist/generated/wspace-operations.d.ts +574 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +486 -16
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +174 -168
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +63 -59
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/board/BulkActions.d.ts.map +1 -1
- package/dist/planmagnet/board/BulkActions.js +106 -55
- package/dist/planmagnet/board/BulkActions.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +3 -3
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.d.ts +0 -4
- package/dist/planmagnet/dashboards/DashboardUI.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.js +6 -13
- package/dist/planmagnet/dashboards/DashboardUI.js.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +70 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -1
- package/dist/planmagnet/index.d.ts +1 -1
- package/dist/planmagnet/index.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts +3 -3
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +26 -19
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts +2 -2
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +18 -12
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js +251 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +94 -92
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +116 -114
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +174 -159
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -145
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +161 -143
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +157 -155
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +97 -99
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js.map +1 -1
- package/dist/planmagnet/product/FeatureLinkPanels.js +66 -66
- package/dist/planmagnet/product/ProductFeatureControls.js +38 -38
- package/dist/planmagnet/product/ProductFeatureLinkCard.js +8 -8
- package/dist/planmagnet/product/ProductLineControls.js +83 -83
- package/dist/planmagnet/product/ProductTeamsPanel.js +6 -6
- package/dist/planmagnet/product/routes.d.ts +6 -0
- package/dist/planmagnet/product/routes.d.ts.map +1 -1
- package/dist/planmagnet/product/routes.js +2 -2
- package/dist/planmagnet/product/routes.js.map +1 -1
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts +20 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js +55 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts +30 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js +282 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts +9 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js +390 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts +17 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js +77 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts +18 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js +87 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts +13 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js +326 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts +23 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js +258 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts +6 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js +56 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts +16 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js +102 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts +154 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.js +210 -0
- package/dist/planmagnet/satisfaction/satisfaction.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts +108 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js +211 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts +116 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js +86 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts +20 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js +31 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts +35 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js +124 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js.map +1 -0
- package/dist/planmagnet/skills/SkillChips.d.ts +30 -0
- package/dist/planmagnet/skills/SkillChips.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillChips.js +57 -0
- package/dist/planmagnet/skills/SkillChips.js.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts +24 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js +256 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts +10 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js +65 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js.map +1 -0
- package/dist/planmagnet/skills/skillOperations.d.ts +52 -0
- package/dist/planmagnet/skills/skillOperations.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillOperations.js +44 -0
- package/dist/planmagnet/skills/skillOperations.js.map +1 -0
- package/dist/planmagnet/skills/skillTags.d.ts +103 -0
- package/dist/planmagnet/skills/skillTags.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillTags.js +115 -0
- package/dist/planmagnet/skills/skillTags.js.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts +28 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js +30 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { throwOnError as t } from "../utils/mutationResult.js";
|
|
3
|
+
import { ErrorLine as n } from "../controls/ConfirmButton.js";
|
|
4
|
+
import { COMMENT_MAX as r, readImportRowErrors as i } from "./satisfaction.js";
|
|
5
|
+
import { IMPORT_CUSTOMER_SATISFACTION_RESPONSES as a, SATISFACTION_QUERIES as ee } from "./satisfactionOperations.js";
|
|
6
|
+
import { downloadCsv as te } from "../export/workItemsCsv.js";
|
|
7
|
+
import { refusalMessage as ne } from "../product/refusal.js";
|
|
8
|
+
import { SatisfactionLinkFields as re } from "./SatisfactionLinkFields.js";
|
|
9
|
+
import { IMPORT_COLUMNS as o, MAX_FILE_BYTES as s, REQUIRED_COLUMNS as c, cellIsTrue as l, parseImportFile as u, previewRows as d, templateCsv as f } from "./satisfactionCsv.js";
|
|
10
|
+
import { useMemo as p, useState as m } from "react";
|
|
11
|
+
import { AlertTriangle as h, CheckCircle2 as ie, Download as ae, FileUp as oe } from "lucide-react";
|
|
12
|
+
import { useMutation as se } from "@apollo/client/react";
|
|
13
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
14
|
+
import { Badge as v, Button as y, Checkbox as ce, Input as le, ResponsiveDialog as ue, ResponsiveDialogContent as de, ResponsiveDialogDescription as fe, ResponsiveDialogFooter as pe, ResponsiveDialogHeader as me, ResponsiveDialogTitle as he, ResponsiveDialogTrigger as b } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
//#region src/planmagnet/satisfaction/SatisfactionImportDialog.tsx
|
|
16
|
+
var x = {
|
|
17
|
+
productId: "",
|
|
18
|
+
releaseId: "",
|
|
19
|
+
projectId: ""
|
|
20
|
+
}, S = 20;
|
|
21
|
+
function C(e, t) {
|
|
22
|
+
switch (t.code) {
|
|
23
|
+
case "unterminatedQuote": return e("planmagnet.satisfaction.import.file.unterminatedQuote", "The file ends inside a quoted value — a closing \" is missing. Check the file and choose it again.");
|
|
24
|
+
case "empty": return e("planmagnet.satisfaction.import.file.empty", "The file is empty.");
|
|
25
|
+
case "missingColumns": return e("planmagnet.satisfaction.import.file.missingColumns", "The header row is missing required columns: {{columns}}. Start from the template.", { columns: t.columns.join(", ") });
|
|
26
|
+
case "duplicateColumn": return e("planmagnet.satisfaction.import.file.duplicateColumn", "The header names the column “{{column}}” twice.", { column: t.column });
|
|
27
|
+
case "noRows": return e("planmagnet.satisfaction.import.file.noRows", "The file has a header but no rows to import.");
|
|
28
|
+
default: return e("planmagnet.satisfaction.import.file.tooManyRows", "The file has {{count}} rows; one import takes at most {{max}}. Split the file and import each part.", {
|
|
29
|
+
count: t.count,
|
|
30
|
+
max: t.max
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function w(e, t) {
|
|
35
|
+
let n = t.params;
|
|
36
|
+
switch (t.code) {
|
|
37
|
+
case "externalIdRequired": return e("planmagnet.satisfaction.import.row.externalIdRequired", "externalId is required — it is what makes importing the same file again add nothing new.");
|
|
38
|
+
case "externalIdTooLong": return e("planmagnet.satisfaction.import.row.externalIdTooLong", "externalId can be at most {{max}} characters.", n);
|
|
39
|
+
case "externalIdDuplicate": return e("planmagnet.satisfaction.import.row.externalIdDuplicate", "externalId is already used by row {{row}} of this file.", n);
|
|
40
|
+
case "metricTypeRequired": return e("planmagnet.satisfaction.import.row.metricTypeRequired", "metricType is required: CSAT or NPS.");
|
|
41
|
+
case "metricTypeInvalid": return e("planmagnet.satisfaction.import.row.metricTypeInvalid", "metricType must be CSAT or NPS.");
|
|
42
|
+
case "scoreRequired": return e("planmagnet.satisfaction.import.row.scoreRequired", "score is required.");
|
|
43
|
+
case "scoreNotWholeNumber": return e("planmagnet.satisfaction.import.row.scoreNotWholeNumber", "score must be a whole number.");
|
|
44
|
+
case "scoreOutOfRange": return e("planmagnet.satisfaction.import.row.scoreOutOfRange", "A {{metric}} score must be from {{min}} to {{max}}.", n);
|
|
45
|
+
case "respondedAtRequired": return e("planmagnet.satisfaction.import.row.respondedAtRequired", "respondedAt is required.");
|
|
46
|
+
case "respondedAtFormat": return e("planmagnet.satisfaction.import.row.respondedAtFormat", "respondedAt must be a date (2026-09-20) or a date-time with its time zone (2026-09-20T14:30:00Z).");
|
|
47
|
+
case "respondedAtInFuture": return e("planmagnet.satisfaction.import.row.respondedAtInFuture", "respondedAt cannot be in the future.");
|
|
48
|
+
case "respondedAtTooOld": return e("planmagnet.satisfaction.import.row.respondedAtTooOld", "respondedAt cannot be earlier than {{min}}.", n);
|
|
49
|
+
case "anonymousInvalid": return e("planmagnet.satisfaction.import.row.anonymousInvalid", "isAnonymous must be true or false (or yes / no, 1 / 0, or empty).");
|
|
50
|
+
case "anonymousWithIdentity": return e("planmagnet.satisfaction.import.row.anonymousWithIdentity", "An anonymous row cannot have a respondentName or respondentEmail.");
|
|
51
|
+
case "respondentNameTooLong": return e("planmagnet.satisfaction.import.row.respondentNameTooLong", "respondentName can be at most {{max}} characters.", n);
|
|
52
|
+
case "respondentEmailInvalid": return e("planmagnet.satisfaction.import.row.respondentEmailInvalid", "respondentEmail must be a valid email address of at most {{max}} characters.", n);
|
|
53
|
+
case "commentTooLong": return e("planmagnet.satisfaction.import.row.commentTooLong", "comment can be at most {{max}} characters.", n);
|
|
54
|
+
default: return e("planmagnet.satisfaction.import.row.linkRequired", "The row names no productId, releaseId or projectId — fill one in, or choose links for such rows above.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function T(e, t) {
|
|
58
|
+
switch (t) {
|
|
59
|
+
case "externalId": return e("planmagnet.satisfaction.import.columns.externalId", "The answer’s id in your survey tool, unique in the file (at most {{max}} characters). Rows imported before are skipped.", { max: 200 });
|
|
60
|
+
case "metricType": return e("planmagnet.satisfaction.import.columns.metricType", "CSAT or NPS.");
|
|
61
|
+
case "score": return e("planmagnet.satisfaction.import.columns.score", "A whole number: CSAT 1–5, NPS 0–10.");
|
|
62
|
+
case "respondedAt": return e("planmagnet.satisfaction.import.columns.respondedAt", "When the customer answered: 2026-09-20, or 2026-09-20T14:30:00Z with a time zone. Not in the future.");
|
|
63
|
+
case "productId":
|
|
64
|
+
case "releaseId":
|
|
65
|
+
case "projectId": return e("planmagnet.satisfaction.import.columns.links", "Optional, but each row needs at least one of productId, releaseId, projectId (or the links chosen below). An id is the last part of that page’s address.");
|
|
66
|
+
case "isAnonymous": return e("planmagnet.satisfaction.import.columns.isAnonymous", "true or false (empty = false). An anonymous row stores no name or email.");
|
|
67
|
+
case "respondentName": return e("planmagnet.satisfaction.import.columns.respondentName", "Optional, at most {{max}} characters.", { max: 200 });
|
|
68
|
+
case "respondentEmail": return e("planmagnet.satisfaction.import.columns.respondentEmail", "Optional, at most {{max}} characters.", { max: 254 });
|
|
69
|
+
default: return e("planmagnet.satisfaction.import.columns.comment", "Optional, at most {{max}} characters.", { max: r });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function E({ row: t }) {
|
|
73
|
+
let n = e();
|
|
74
|
+
return t.usesDefaultLinks ? /* @__PURE__ */ g(v, {
|
|
75
|
+
variant: "outline",
|
|
76
|
+
className: "text-xs",
|
|
77
|
+
children: n("planmagnet.satisfaction.import.preview.defaultLinks", "Links chosen above")
|
|
78
|
+
}) : /* @__PURE__ */ g("div", {
|
|
79
|
+
className: "space-y-0.5 text-xs",
|
|
80
|
+
children: [
|
|
81
|
+
[n("planmagnet.satisfaction.import.preview.product", "Product"), t.cells.productId],
|
|
82
|
+
[n("planmagnet.satisfaction.import.preview.release", "Release"), t.cells.releaseId],
|
|
83
|
+
[n("planmagnet.satisfaction.import.preview.project", "Project"), t.cells.projectId]
|
|
84
|
+
].filter(([, e]) => e).map(([e, t]) => /* @__PURE__ */ _("p", {
|
|
85
|
+
className: "max-w-48 truncate",
|
|
86
|
+
title: t,
|
|
87
|
+
children: [
|
|
88
|
+
e,
|
|
89
|
+
": ",
|
|
90
|
+
/* @__PURE__ */ g("span", {
|
|
91
|
+
className: "font-mono",
|
|
92
|
+
children: t
|
|
93
|
+
})
|
|
94
|
+
]
|
|
95
|
+
}, e))
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function D({ links: r, onImported: v }) {
|
|
99
|
+
let D = e(), [O, k] = m(!1), [A, j] = m(0), [M, N] = m(null), [P, F] = m(null), [I, L] = m(null), [R, z] = m(x), [B, V] = m(null), [H, U] = m(null), [W, G] = m(null), [K, q] = m(!0), [ge, J] = se(a, {
|
|
100
|
+
refetchQueries: ee,
|
|
101
|
+
awaitRefetchQueries: !0
|
|
102
|
+
}), Y = p(() => P && !P.problem ? d(P, R, /* @__PURE__ */ new Date()) : [], [P, R]), X = Y.filter((e) => e.input !== null), _e = Y.length - X.length, Z = p(() => {
|
|
103
|
+
let e = /* @__PURE__ */ new Map();
|
|
104
|
+
for (let t of B ?? []) e.set(t.row, [...e.get(t.row) ?? [], t]);
|
|
105
|
+
return e;
|
|
106
|
+
}, [B]), ve = Y.length > 0 && _e === 0 && !J.loading && W === null, Q = (e) => e.problems.length === 0 && !Z.has(e.row), $ = Y.filter((e) => !Q(e)).length, ye = $ > 0 && K ? Y.filter((e) => !Q(e)) : Y, be = () => {
|
|
107
|
+
q(!0), N(null), F(null), L(null), V(null), U(null), G(null), j((e) => e + 1);
|
|
108
|
+
}, xe = async (e) => {
|
|
109
|
+
let t = e.target.files?.[0];
|
|
110
|
+
if (V(null), U(null), G(null), F(null), L(null), q(!0), N(t?.name ?? null), t) {
|
|
111
|
+
if (t.size > 5242880) {
|
|
112
|
+
L(D("planmagnet.satisfaction.import.file.tooLarge", "The file is larger than {{max}} MB. Split it and import each part.", { max: s / (1024 * 1024) }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
F(u(await t.text()));
|
|
117
|
+
} catch {
|
|
118
|
+
L(D("planmagnet.satisfaction.import.file.unreadable", "The file could not be read. Save it as CSV (UTF-8) and try again."));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, Se = async () => {
|
|
122
|
+
let e = X.flatMap((e) => e.input ? [e.input] : []);
|
|
123
|
+
if (e.length === Y.length) {
|
|
124
|
+
U(null), V(null);
|
|
125
|
+
try {
|
|
126
|
+
let { data: n } = await t(ge({ variables: { input: { rows: e } } })), r = n?.planmagnetImportCustomerSatisfactionResponses;
|
|
127
|
+
r && (G(r), v(r));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
V(i(e)), U(ne(e, D("planmagnet.satisfaction.import.errors.failed", "The file could not be imported. Nothing was saved."), D("planmagnet.satisfaction.errors.forbidden", "Your role cannot change customer satisfaction responses in this workspace.")));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return /* @__PURE__ */ _(ue, {
|
|
134
|
+
open: O,
|
|
135
|
+
onOpenChange: (e) => {
|
|
136
|
+
k(e), e && (be(), z(r));
|
|
137
|
+
},
|
|
138
|
+
children: [/* @__PURE__ */ g(b, {
|
|
139
|
+
asChild: !0,
|
|
140
|
+
children: /* @__PURE__ */ _(y, {
|
|
141
|
+
size: "sm",
|
|
142
|
+
variant: "outline",
|
|
143
|
+
"data-testid": "satisfaction-import-open",
|
|
144
|
+
children: [/* @__PURE__ */ g(oe, { className: "mr-1 h-4 w-4" }), D("planmagnet.satisfaction.actions.import", "Import CSV")]
|
|
145
|
+
})
|
|
146
|
+
}), /* @__PURE__ */ _(de, {
|
|
147
|
+
className: "max-h-[90vh] overflow-y-auto sm:max-w-5xl",
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ _(me, { children: [/* @__PURE__ */ g(he, { children: D("planmagnet.satisfaction.import.title", "Import customer responses") }), /* @__PURE__ */ g(fe, { children: D("planmagnet.satisfaction.import.description", "One row per answer, up to {{max}} rows. All or nothing: if any row is refused, nothing is saved. A row whose externalId was imported before is skipped, so importing the same file again adds nothing.", { max: 500 }) })] }),
|
|
150
|
+
/* @__PURE__ */ _("div", {
|
|
151
|
+
className: "min-w-0 space-y-4",
|
|
152
|
+
children: [
|
|
153
|
+
/* @__PURE__ */ _("div", {
|
|
154
|
+
className: "flex flex-wrap items-center gap-3",
|
|
155
|
+
children: [/* @__PURE__ */ _(y, {
|
|
156
|
+
size: "sm",
|
|
157
|
+
variant: "outline",
|
|
158
|
+
onClick: () => te("customer-satisfaction-template.csv", f()),
|
|
159
|
+
"data-testid": "satisfaction-import-template",
|
|
160
|
+
children: [/* @__PURE__ */ g(ae, { className: "mr-1 h-4 w-4" }), D("planmagnet.satisfaction.import.template", "Download template")]
|
|
161
|
+
}), /* @__PURE__ */ _("div", {
|
|
162
|
+
className: "min-w-64 flex-1",
|
|
163
|
+
children: [/* @__PURE__ */ g("label", {
|
|
164
|
+
htmlFor: "satisfaction-import-file",
|
|
165
|
+
className: "sr-only",
|
|
166
|
+
children: D("planmagnet.satisfaction.import.chooseFile", "CSV file")
|
|
167
|
+
}), /* @__PURE__ */ g(le, {
|
|
168
|
+
id: "satisfaction-import-file",
|
|
169
|
+
type: "file",
|
|
170
|
+
accept: ".csv,text/csv",
|
|
171
|
+
onChange: (e) => void xe(e),
|
|
172
|
+
"data-testid": "satisfaction-import-file"
|
|
173
|
+
}, A)]
|
|
174
|
+
})]
|
|
175
|
+
}),
|
|
176
|
+
/* @__PURE__ */ _("details", {
|
|
177
|
+
className: "rounded-md border p-3 text-sm",
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ g("summary", {
|
|
180
|
+
className: "cursor-pointer font-medium",
|
|
181
|
+
children: D("planmagnet.satisfaction.import.columns.title", "Columns")
|
|
182
|
+
}),
|
|
183
|
+
/* @__PURE__ */ g("p", {
|
|
184
|
+
className: "mt-2 text-xs text-muted-foreground",
|
|
185
|
+
children: D("planmagnet.satisfaction.import.columns.intro", "The first row names the columns (any order; case, spaces and underscores do not matter). Comma, semicolon or tab separated, UTF-8. Required: {{required}}.", { required: c.join(", ") })
|
|
186
|
+
}),
|
|
187
|
+
/* @__PURE__ */ g("dl", {
|
|
188
|
+
className: "mt-2 grid gap-x-4 gap-y-1 text-xs sm:grid-cols-[max-content_1fr]",
|
|
189
|
+
children: o.map((e) => /* @__PURE__ */ _("div", {
|
|
190
|
+
className: "contents",
|
|
191
|
+
children: [/* @__PURE__ */ g("dt", {
|
|
192
|
+
className: "font-mono",
|
|
193
|
+
children: e
|
|
194
|
+
}), /* @__PURE__ */ g("dd", {
|
|
195
|
+
className: "text-muted-foreground",
|
|
196
|
+
children: T(D, e)
|
|
197
|
+
})]
|
|
198
|
+
}, e))
|
|
199
|
+
})
|
|
200
|
+
]
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ _("fieldset", {
|
|
203
|
+
className: "space-y-2 rounded-md border p-3",
|
|
204
|
+
children: [
|
|
205
|
+
/* @__PURE__ */ g("legend", {
|
|
206
|
+
className: "px-1 text-sm font-medium",
|
|
207
|
+
children: D("planmagnet.satisfaction.import.defaults.title", "Links for rows that name none")
|
|
208
|
+
}),
|
|
209
|
+
/* @__PURE__ */ g("p", {
|
|
210
|
+
className: "text-xs text-muted-foreground",
|
|
211
|
+
children: D("planmagnet.satisfaction.import.defaults.hint", "A row with no productId, releaseId or projectId of its own is linked to what you choose here. A row with any link of its own keeps exactly its own.")
|
|
212
|
+
}),
|
|
213
|
+
/* @__PURE__ */ g(re, {
|
|
214
|
+
idPrefix: "satisfaction-import",
|
|
215
|
+
columns: !0,
|
|
216
|
+
value: R,
|
|
217
|
+
onChange: (e) => {
|
|
218
|
+
z(e), V(null), U(null);
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
]
|
|
222
|
+
}),
|
|
223
|
+
I && /* @__PURE__ */ g(n, { message: I }),
|
|
224
|
+
P?.problem && /* @__PURE__ */ g(n, { message: C(D, P.problem) }),
|
|
225
|
+
P && P.ignoredColumns.length > 0 && /* @__PURE__ */ _("p", {
|
|
226
|
+
className: "flex items-center gap-1.5 rounded-md bg-status-warning-bg-subtle px-2 py-1 text-xs text-foreground",
|
|
227
|
+
"data-testid": "satisfaction-import-ignored",
|
|
228
|
+
children: [/* @__PURE__ */ g(h, {
|
|
229
|
+
className: "h-3.5 w-3.5 shrink-0",
|
|
230
|
+
"aria-hidden": !0
|
|
231
|
+
}), D("planmagnet.satisfaction.import.file.ignoredColumns", "These columns are not imported: {{columns}}.", { columns: P.ignoredColumns.join(", ") })]
|
|
232
|
+
}),
|
|
233
|
+
H && /* @__PURE__ */ g("div", {
|
|
234
|
+
role: "alert",
|
|
235
|
+
className: "rounded-md bg-status-error-bg-subtle p-3 text-sm text-status-error-text",
|
|
236
|
+
"data-testid": "satisfaction-import-error",
|
|
237
|
+
children: H
|
|
238
|
+
}),
|
|
239
|
+
W && /* @__PURE__ */ _("div", {
|
|
240
|
+
role: "status",
|
|
241
|
+
className: "space-y-1 rounded-md bg-status-success-bg-subtle p-3 text-sm text-status-success-text",
|
|
242
|
+
"data-testid": "satisfaction-import-result",
|
|
243
|
+
children: [/* @__PURE__ */ _("p", {
|
|
244
|
+
className: "flex items-center gap-1.5 font-medium",
|
|
245
|
+
children: [/* @__PURE__ */ g(ie, {
|
|
246
|
+
className: "h-4 w-4",
|
|
247
|
+
"aria-hidden": !0
|
|
248
|
+
}), W.createdCount === 1 ? D("planmagnet.satisfaction.import.result.createdOne", "Imported 1 new response.") : D("planmagnet.satisfaction.import.result.created", "Imported {{count}} new responses.", { count: W.createdCount })]
|
|
249
|
+
}), W.skippedCount > 0 && /* @__PURE__ */ g("p", { children: D("planmagnet.satisfaction.import.result.skipped", "Skipped {{count}} already imported (same externalId — never updated or duplicated): {{ids}}", {
|
|
250
|
+
count: W.skippedCount,
|
|
251
|
+
ids: W.skippedExternalIds.slice(0, S).join(", ") + (W.skippedExternalIds.length > S ? D("planmagnet.satisfaction.import.result.more", " and {{count}} more", { count: W.skippedExternalIds.length - S }) : "")
|
|
252
|
+
}) })]
|
|
253
|
+
}),
|
|
254
|
+
Y.length > 0 && /* @__PURE__ */ _("div", {
|
|
255
|
+
className: "space-y-2",
|
|
256
|
+
children: [
|
|
257
|
+
$ > 0 && /* @__PURE__ */ _("label", {
|
|
258
|
+
className: "flex items-center gap-2 text-xs",
|
|
259
|
+
children: [/* @__PURE__ */ g(ce, {
|
|
260
|
+
checked: K,
|
|
261
|
+
onCheckedChange: (e) => q(e === !0),
|
|
262
|
+
"data-testid": "satisfaction-import-only-problems"
|
|
263
|
+
}), D("planmagnet.satisfaction.import.preview.onlyProblems", "Show only rows with problems ({{count}})", { count: $ })]
|
|
264
|
+
}),
|
|
265
|
+
/* @__PURE__ */ _("p", {
|
|
266
|
+
className: "text-sm",
|
|
267
|
+
"data-testid": "satisfaction-import-summary",
|
|
268
|
+
children: [
|
|
269
|
+
D("planmagnet.satisfaction.import.preview.summary", "{{file}} — rows: {{count}}; ready: {{ready}}; with problems: {{problems}}.", {
|
|
270
|
+
file: M ?? "",
|
|
271
|
+
count: Y.length,
|
|
272
|
+
ready: Y.length - $,
|
|
273
|
+
problems: $
|
|
274
|
+
}),
|
|
275
|
+
" ",
|
|
276
|
+
/* @__PURE__ */ g("span", {
|
|
277
|
+
className: "text-muted-foreground",
|
|
278
|
+
children: D("planmagnet.satisfaction.import.preview.numbering", "Row 1 is the first row under the header.")
|
|
279
|
+
})
|
|
280
|
+
]
|
|
281
|
+
}),
|
|
282
|
+
/* @__PURE__ */ g("div", {
|
|
283
|
+
className: "max-h-96 overflow-auto rounded-md border",
|
|
284
|
+
children: /* @__PURE__ */ _("table", {
|
|
285
|
+
className: "w-full text-left text-xs",
|
|
286
|
+
children: [/* @__PURE__ */ g("thead", {
|
|
287
|
+
className: "sticky top-0 bg-muted",
|
|
288
|
+
children: /* @__PURE__ */ _("tr", { children: [
|
|
289
|
+
/* @__PURE__ */ g("th", {
|
|
290
|
+
className: "px-2 py-1.5 font-medium",
|
|
291
|
+
children: D("planmagnet.satisfaction.import.preview.row", "Row")
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ g("th", {
|
|
294
|
+
className: "px-2 py-1.5 font-medium",
|
|
295
|
+
children: "externalId"
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ g("th", {
|
|
298
|
+
className: "px-2 py-1.5 font-medium",
|
|
299
|
+
children: D("planmagnet.satisfaction.list.score", "Score")
|
|
300
|
+
}),
|
|
301
|
+
/* @__PURE__ */ g("th", {
|
|
302
|
+
className: "px-2 py-1.5 font-medium",
|
|
303
|
+
children: D("planmagnet.satisfaction.list.answered", "Answered")
|
|
304
|
+
}),
|
|
305
|
+
/* @__PURE__ */ g("th", {
|
|
306
|
+
className: "px-2 py-1.5 font-medium",
|
|
307
|
+
children: D("planmagnet.satisfaction.list.about", "About")
|
|
308
|
+
}),
|
|
309
|
+
/* @__PURE__ */ g("th", {
|
|
310
|
+
className: "px-2 py-1.5 font-medium",
|
|
311
|
+
children: D("planmagnet.satisfaction.list.respondent", "Respondent")
|
|
312
|
+
}),
|
|
313
|
+
/* @__PURE__ */ g("th", {
|
|
314
|
+
className: "px-2 py-1.5 font-medium",
|
|
315
|
+
children: D("planmagnet.satisfaction.import.preview.check", "Check")
|
|
316
|
+
})
|
|
317
|
+
] })
|
|
318
|
+
}), /* @__PURE__ */ g("tbody", { children: ye.map((e) => {
|
|
319
|
+
let t = Z.get(e.row) ?? [], n = Q(e);
|
|
320
|
+
return /* @__PURE__ */ _("tr", {
|
|
321
|
+
className: n ? "border-t" : "border-t bg-status-error-bg-subtle/40",
|
|
322
|
+
"data-testid": "satisfaction-import-row",
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ g("td", {
|
|
325
|
+
className: "px-2 py-1.5 align-top tabular-nums",
|
|
326
|
+
children: e.row
|
|
327
|
+
}),
|
|
328
|
+
/* @__PURE__ */ g("td", {
|
|
329
|
+
className: "max-w-40 truncate px-2 py-1.5 align-top font-mono",
|
|
330
|
+
title: e.cells.externalId,
|
|
331
|
+
children: e.cells.externalId
|
|
332
|
+
}),
|
|
333
|
+
/* @__PURE__ */ _("td", {
|
|
334
|
+
className: "whitespace-nowrap px-2 py-1.5 align-top",
|
|
335
|
+
children: [
|
|
336
|
+
e.cells.metricType,
|
|
337
|
+
" ",
|
|
338
|
+
e.cells.score
|
|
339
|
+
]
|
|
340
|
+
}),
|
|
341
|
+
/* @__PURE__ */ g("td", {
|
|
342
|
+
className: "whitespace-nowrap px-2 py-1.5 align-top",
|
|
343
|
+
children: e.cells.respondedAt
|
|
344
|
+
}),
|
|
345
|
+
/* @__PURE__ */ g("td", {
|
|
346
|
+
className: "px-2 py-1.5 align-top",
|
|
347
|
+
children: /* @__PURE__ */ g(E, { row: e })
|
|
348
|
+
}),
|
|
349
|
+
/* @__PURE__ */ g("td", {
|
|
350
|
+
className: "max-w-40 truncate px-2 py-1.5 align-top",
|
|
351
|
+
children: l(e.cells.isAnonymous) ? D("planmagnet.satisfaction.list.anonymous", "Anonymous") : e.cells.respondentName || e.cells.respondentEmail
|
|
352
|
+
}),
|
|
353
|
+
/* @__PURE__ */ g("td", {
|
|
354
|
+
className: "px-2 py-1.5 align-top",
|
|
355
|
+
children: n ? /* @__PURE__ */ g("span", {
|
|
356
|
+
className: "text-status-success-text",
|
|
357
|
+
children: D("planmagnet.satisfaction.import.preview.ready", "Ready")
|
|
358
|
+
}) : /* @__PURE__ */ _("ul", {
|
|
359
|
+
className: "space-y-0.5 text-status-error-text",
|
|
360
|
+
"data-testid": "satisfaction-import-row-problems",
|
|
361
|
+
children: [e.problems.map((e, t) => /* @__PURE__ */ g("li", { children: w(D, e) }, `c${t}`)), t.map((e, t) => /* @__PURE__ */ _("li", { children: [e.field ? `${e.field}: ` : "", e.message] }, `s${t}`))]
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
]
|
|
365
|
+
}, e.row);
|
|
366
|
+
}) })]
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
]
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
}),
|
|
373
|
+
/* @__PURE__ */ g(pe, { children: W ? /* @__PURE__ */ g(y, {
|
|
374
|
+
onClick: () => k(!1),
|
|
375
|
+
"data-testid": "satisfaction-import-done",
|
|
376
|
+
children: D("planmagnet.satisfaction.import.done", "Done")
|
|
377
|
+
}) : /* @__PURE__ */ g(y, {
|
|
378
|
+
onClick: () => void Se(),
|
|
379
|
+
disabled: !ve,
|
|
380
|
+
"data-testid": "satisfaction-import-submit",
|
|
381
|
+
children: J.loading ? D("planmagnet.satisfaction.import.importing", "Importing…") : Y.length === 1 ? D("planmagnet.satisfaction.import.submitOne", "Import 1 row") : D("planmagnet.satisfaction.import.submit", "Import {{count}} rows", { count: Y.length })
|
|
382
|
+
}) })
|
|
383
|
+
]
|
|
384
|
+
})]
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
export { D as SatisfactionImportDialog };
|
|
389
|
+
|
|
390
|
+
//# sourceMappingURL=SatisfactionImportDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SatisfactionImportDialog.js","names":[],"sources":["../../../src/planmagnet/satisfaction/SatisfactionImportDialog.tsx"],"sourcesContent":["import { useMemo, useState, type ChangeEvent } from 'react';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n Checkbox,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { AlertTriangle, CheckCircle2, Download, FileUp } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { downloadCsv } from '../export/workItemsCsv';\nimport { refusalMessage } from '../product/refusal';\nimport { throwOnError } from '../utils/mutationResult';\nimport { SatisfactionLinkFields } from './SatisfactionLinkFields';\nimport {\n COMMENT_MAX,\n EXTERNAL_ID_MAX,\n MAX_IMPORT_ROWS,\n RESPONDENT_EMAIL_MAX,\n RESPONDENT_NAME_MAX,\n readImportRowErrors,\n type ImportRowError,\n} from './satisfaction';\nimport {\n IMPORT_COLUMNS,\n MAX_FILE_BYTES,\n cellIsTrue,\n REQUIRED_COLUMNS,\n parseImportFile,\n previewRows,\n templateCsv,\n type DefaultLinks,\n type FileProblem,\n type ImportColumn,\n type ImportRowInput,\n type ParsedImportFile,\n type PreviewRow,\n type RowProblem,\n} from './satisfactionCsv';\nimport {\n IMPORT_CUSTOMER_SATISFACTION_RESPONSES,\n SATISFACTION_QUERIES,\n type ImportCustomerSatisfactionResult,\n} from './satisfactionOperations';\n\n/**\n * Import customer answers from a CSV file (source IMPORT). The file is read in the browser: its rows are previewed with\n * every problem the service would refuse them for, and nothing is sent until every row is ready. The service then checks\n * every row again (and that each linked product, release and project exists) and imports all of them or none — a\n * refusal names each refused row by its number, shown on that row here. Re-importing a file is safe: a row whose\n * external id was imported before is skipped, never updated or duplicated, and the result says which.\n */\n\ntype Tr = ReturnType<typeof useTr>;\n\ninterface ImportData {\n planmagnetImportCustomerSatisfactionResponses: ImportCustomerSatisfactionResult;\n}\n\nconst EMPTY_LINKS: DefaultLinks = { productId: '', releaseId: '', projectId: '' };\n/** How many skipped external ids the result lists before \"and N more\". */\nconst SKIPPED_LISTED = 20;\n\nfunction fileProblemText(tr: Tr, problem: FileProblem): string {\n switch (problem.code) {\n case 'unterminatedQuote':\n return tr(\n 'planmagnet.satisfaction.import.file.unterminatedQuote',\n 'The file ends inside a quoted value — a closing \" is missing. Check the file and choose it again.'\n );\n case 'empty':\n return tr('planmagnet.satisfaction.import.file.empty', 'The file is empty.');\n case 'missingColumns':\n return tr(\n 'planmagnet.satisfaction.import.file.missingColumns',\n 'The header row is missing required columns: {{columns}}. Start from the template.',\n { columns: problem.columns.join(', ') }\n );\n case 'duplicateColumn':\n return tr(\n 'planmagnet.satisfaction.import.file.duplicateColumn',\n 'The header names the column “{{column}}” twice.',\n { column: problem.column }\n );\n case 'noRows':\n return tr(\n 'planmagnet.satisfaction.import.file.noRows',\n 'The file has a header but no rows to import.'\n );\n default:\n return tr(\n 'planmagnet.satisfaction.import.file.tooManyRows',\n 'The file has {{count}} rows; one import takes at most {{max}}. Split the file and import each part.',\n { count: problem.count, max: problem.max }\n );\n }\n}\n\nfunction rowProblemText(tr: Tr, problem: RowProblem): string {\n const p = problem.params;\n switch (problem.code) {\n case 'externalIdRequired':\n return tr(\n 'planmagnet.satisfaction.import.row.externalIdRequired',\n 'externalId is required — it is what makes importing the same file again add nothing new.'\n );\n case 'externalIdTooLong':\n return tr(\n 'planmagnet.satisfaction.import.row.externalIdTooLong',\n 'externalId can be at most {{max}} characters.',\n p\n );\n case 'externalIdDuplicate':\n return tr(\n 'planmagnet.satisfaction.import.row.externalIdDuplicate',\n 'externalId is already used by row {{row}} of this file.',\n p\n );\n case 'metricTypeRequired':\n return tr(\n 'planmagnet.satisfaction.import.row.metricTypeRequired',\n 'metricType is required: CSAT or NPS.'\n );\n case 'metricTypeInvalid':\n return tr(\n 'planmagnet.satisfaction.import.row.metricTypeInvalid',\n 'metricType must be CSAT or NPS.'\n );\n case 'scoreRequired':\n return tr('planmagnet.satisfaction.import.row.scoreRequired', 'score is required.');\n case 'scoreNotWholeNumber':\n return tr(\n 'planmagnet.satisfaction.import.row.scoreNotWholeNumber',\n 'score must be a whole number.'\n );\n case 'scoreOutOfRange':\n return tr(\n 'planmagnet.satisfaction.import.row.scoreOutOfRange',\n 'A {{metric}} score must be from {{min}} to {{max}}.',\n p\n );\n case 'respondedAtRequired':\n return tr(\n 'planmagnet.satisfaction.import.row.respondedAtRequired',\n 'respondedAt is required.'\n );\n case 'respondedAtFormat':\n return tr(\n 'planmagnet.satisfaction.import.row.respondedAtFormat',\n 'respondedAt must be a date (2026-09-20) or a date-time with its time zone (2026-09-20T14:30:00Z).'\n );\n case 'respondedAtInFuture':\n return tr(\n 'planmagnet.satisfaction.import.row.respondedAtInFuture',\n 'respondedAt cannot be in the future.'\n );\n case 'respondedAtTooOld':\n return tr(\n 'planmagnet.satisfaction.import.row.respondedAtTooOld',\n 'respondedAt cannot be earlier than {{min}}.',\n p\n );\n case 'anonymousInvalid':\n return tr(\n 'planmagnet.satisfaction.import.row.anonymousInvalid',\n 'isAnonymous must be true or false (or yes / no, 1 / 0, or empty).'\n );\n case 'anonymousWithIdentity':\n return tr(\n 'planmagnet.satisfaction.import.row.anonymousWithIdentity',\n 'An anonymous row cannot have a respondentName or respondentEmail.'\n );\n case 'respondentNameTooLong':\n return tr(\n 'planmagnet.satisfaction.import.row.respondentNameTooLong',\n 'respondentName can be at most {{max}} characters.',\n p\n );\n case 'respondentEmailInvalid':\n return tr(\n 'planmagnet.satisfaction.import.row.respondentEmailInvalid',\n 'respondentEmail must be a valid email address of at most {{max}} characters.',\n p\n );\n case 'commentTooLong':\n return tr(\n 'planmagnet.satisfaction.import.row.commentTooLong',\n 'comment can be at most {{max}} characters.',\n p\n );\n default:\n return tr(\n 'planmagnet.satisfaction.import.row.linkRequired',\n 'The row names no productId, releaseId or projectId — fill one in, or choose links for such rows above.'\n );\n }\n}\n\n/** One line of the column guide. */\nfunction columnHelp(tr: Tr, column: ImportColumn): string {\n switch (column) {\n case 'externalId':\n return tr(\n 'planmagnet.satisfaction.import.columns.externalId',\n 'The answer’s id in your survey tool, unique in the file (at most {{max}} characters). Rows imported before are skipped.',\n { max: EXTERNAL_ID_MAX }\n );\n case 'metricType':\n return tr('planmagnet.satisfaction.import.columns.metricType', 'CSAT or NPS.');\n case 'score':\n return tr(\n 'planmagnet.satisfaction.import.columns.score',\n 'A whole number: CSAT 1–5, NPS 0–10.'\n );\n case 'respondedAt':\n return tr(\n 'planmagnet.satisfaction.import.columns.respondedAt',\n 'When the customer answered: 2026-09-20, or 2026-09-20T14:30:00Z with a time zone. Not in the future.'\n );\n case 'productId':\n case 'releaseId':\n case 'projectId':\n return tr(\n 'planmagnet.satisfaction.import.columns.links',\n 'Optional, but each row needs at least one of productId, releaseId, projectId (or the links chosen below). An id is the last part of that page’s address.'\n );\n case 'isAnonymous':\n return tr(\n 'planmagnet.satisfaction.import.columns.isAnonymous',\n 'true or false (empty = false). An anonymous row stores no name or email.'\n );\n case 'respondentName':\n return tr(\n 'planmagnet.satisfaction.import.columns.respondentName',\n 'Optional, at most {{max}} characters.',\n {\n max: RESPONDENT_NAME_MAX,\n }\n );\n case 'respondentEmail':\n return tr(\n 'planmagnet.satisfaction.import.columns.respondentEmail',\n 'Optional, at most {{max}} characters.',\n {\n max: RESPONDENT_EMAIL_MAX,\n }\n );\n default:\n return tr(\n 'planmagnet.satisfaction.import.columns.comment',\n 'Optional, at most {{max}} characters.',\n {\n max: COMMENT_MAX,\n }\n );\n }\n}\n\nfunction RowLinks({ row }: { row: PreviewRow }) {\n const tr = useTr();\n if (row.usesDefaultLinks) {\n return (\n <Badge variant=\"outline\" className=\"text-xs\">\n {tr('planmagnet.satisfaction.import.preview.defaultLinks', 'Links chosen above')}\n </Badge>\n );\n }\n const parts: [string, string][] = [\n [tr('planmagnet.satisfaction.import.preview.product', 'Product'), row.cells.productId],\n [tr('planmagnet.satisfaction.import.preview.release', 'Release'), row.cells.releaseId],\n [tr('planmagnet.satisfaction.import.preview.project', 'Project'), row.cells.projectId],\n ];\n return (\n <div className=\"space-y-0.5 text-xs\">\n {parts\n .filter(([, id]) => id)\n .map(([label, id]) => (\n <p key={label} className=\"max-w-48 truncate\" title={id}>\n {label}: <span className=\"font-mono\">{id}</span>\n </p>\n ))}\n </div>\n );\n}\n\nexport function SatisfactionImportDialog({\n links,\n onImported,\n}: {\n /** Starting links for rows that name none — the page's product / release / project filters. */\n links: DefaultLinks;\n onImported: (result: ImportCustomerSatisfactionResult) => void;\n}) {\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const [inputKey, setInputKey] = useState(0);\n const [fileName, setFileName] = useState<string | null>(null);\n const [parsed, setParsed] = useState<ParsedImportFile | null>(null);\n const [readError, setReadError] = useState<string | null>(null);\n const [defaults, setDefaults] = useState<DefaultLinks>(EMPTY_LINKS);\n const [serverRows, setServerRows] = useState<ImportRowError[] | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [result, setResult] = useState<ImportCustomerSatisfactionResult | null>(null);\n const [onlyProblems, setOnlyProblems] = useState(true);\n const [importRows, importState] = useMutation<ImportData>(\n IMPORT_CUSTOMER_SATISFACTION_RESPONSES,\n {\n refetchQueries: SATISFACTION_QUERIES,\n awaitRefetchQueries: true,\n }\n );\n\n const rows = useMemo(\n () => (parsed && !parsed.problem ? previewRows(parsed, defaults, new Date()) : []),\n [parsed, defaults]\n );\n const ready = rows.filter((r) => r.input !== null);\n const withProblems = rows.length - ready.length;\n const serverByRow = useMemo(() => {\n const map = new Map<number, ImportRowError[]>();\n for (const e of serverRows ?? []) map.set(e.row, [...(map.get(e.row) ?? []), e]);\n return map;\n }, [serverRows]);\n const canImport =\n rows.length > 0 && withProblems === 0 && !importState.loading && result === null;\n const isOk = (row: PreviewRow) => row.problems.length === 0 && !serverByRow.has(row.row);\n const failing = rows.filter((row) => !isOk(row)).length;\n // With problems on screen, the table starts with just the rows that have them — 3 refusals among 500 rows.\n const shownRows = failing > 0 && onlyProblems ? rows.filter((row) => !isOk(row)) : rows;\n\n const reset = () => {\n setOnlyProblems(true);\n setFileName(null);\n setParsed(null);\n setReadError(null);\n setServerRows(null);\n setError(null);\n setResult(null);\n setInputKey((k) => k + 1);\n };\n\n const onFile = async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n setServerRows(null);\n setError(null);\n setResult(null);\n setParsed(null);\n setReadError(null);\n setOnlyProblems(true);\n setFileName(file?.name ?? null);\n if (!file) return;\n if (file.size > MAX_FILE_BYTES) {\n setReadError(\n tr(\n 'planmagnet.satisfaction.import.file.tooLarge',\n 'The file is larger than {{max}} MB. Split it and import each part.',\n { max: MAX_FILE_BYTES / (1024 * 1024) }\n )\n );\n return;\n }\n try {\n setParsed(parseImportFile(await file.text()));\n } catch {\n setReadError(\n tr(\n 'planmagnet.satisfaction.import.file.unreadable',\n 'The file could not be read. Save it as CSV (UTF-8) and try again.'\n )\n );\n }\n };\n\n const onImport = async () => {\n const input: ImportRowInput[] = ready.flatMap((r) => (r.input ? [r.input] : []));\n if (input.length !== rows.length) return;\n setError(null);\n setServerRows(null);\n try {\n const { data } = await throwOnError(importRows({ variables: { input: { rows: input } } }));\n const outcome = data?.planmagnetImportCustomerSatisfactionResponses;\n if (outcome) {\n setResult(outcome);\n onImported(outcome);\n }\n } catch (e) {\n setServerRows(readImportRowErrors(e));\n setError(\n refusalMessage(\n e,\n tr(\n 'planmagnet.satisfaction.import.errors.failed',\n 'The file could not be imported. Nothing was saved.'\n ),\n tr(\n 'planmagnet.satisfaction.errors.forbidden',\n 'Your role cannot change customer satisfaction responses in this workspace.'\n )\n )\n );\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n reset();\n setDefaults(links);\n }\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"satisfaction-import-open\">\n <FileUp className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.satisfaction.actions.import', 'Import CSV')}\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-h-[90vh] overflow-y-auto sm:max-w-5xl\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('planmagnet.satisfaction.import.title', 'Import customer responses')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'planmagnet.satisfaction.import.description',\n 'One row per answer, up to {{max}} rows. All or nothing: if any row is refused, nothing is saved. A row whose externalId was imported before is skipped, so importing the same file again adds nothing.',\n { max: MAX_IMPORT_ROWS }\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"min-w-0 space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => downloadCsv('customer-satisfaction-template.csv', templateCsv())}\n data-testid=\"satisfaction-import-template\"\n >\n <Download className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.satisfaction.import.template', 'Download template')}\n </Button>\n <div className=\"min-w-64 flex-1\">\n <label htmlFor=\"satisfaction-import-file\" className=\"sr-only\">\n {tr('planmagnet.satisfaction.import.chooseFile', 'CSV file')}\n </label>\n <Input\n key={inputKey}\n id=\"satisfaction-import-file\"\n type=\"file\"\n accept=\".csv,text/csv\"\n onChange={(e) => void onFile(e)}\n data-testid=\"satisfaction-import-file\"\n />\n </div>\n </div>\n\n <details className=\"rounded-md border p-3 text-sm\">\n <summary className=\"cursor-pointer font-medium\">\n {tr('planmagnet.satisfaction.import.columns.title', 'Columns')}\n </summary>\n <p className=\"mt-2 text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.satisfaction.import.columns.intro',\n 'The first row names the columns (any order; case, spaces and underscores do not matter). Comma, semicolon or tab separated, UTF-8. Required: {{required}}.',\n { required: REQUIRED_COLUMNS.join(', ') }\n )}\n </p>\n <dl className=\"mt-2 grid gap-x-4 gap-y-1 text-xs sm:grid-cols-[max-content_1fr]\">\n {IMPORT_COLUMNS.map((column) => (\n <div key={column} className=\"contents\">\n <dt className=\"font-mono\">{column}</dt>\n <dd className=\"text-muted-foreground\">{columnHelp(tr, column)}</dd>\n </div>\n ))}\n </dl>\n </details>\n\n <fieldset className=\"space-y-2 rounded-md border p-3\">\n <legend className=\"px-1 text-sm font-medium\">\n {tr('planmagnet.satisfaction.import.defaults.title', 'Links for rows that name none')}\n </legend>\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.satisfaction.import.defaults.hint',\n 'A row with no productId, releaseId or projectId of its own is linked to what you choose here. A row with any link of its own keeps exactly its own.'\n )}\n </p>\n <SatisfactionLinkFields\n idPrefix=\"satisfaction-import\"\n columns\n value={defaults}\n onChange={(next) => {\n setDefaults(next);\n setServerRows(null);\n setError(null);\n }}\n />\n </fieldset>\n\n {readError && <ErrorLine message={readError} />}\n {parsed?.problem && <ErrorLine message={fileProblemText(tr, parsed.problem)} />}\n {parsed && parsed.ignoredColumns.length > 0 && (\n <p\n className=\"flex items-center gap-1.5 rounded-md bg-status-warning-bg-subtle px-2 py-1 text-xs text-foreground\"\n data-testid=\"satisfaction-import-ignored\"\n >\n <AlertTriangle className=\"h-3.5 w-3.5 shrink-0\" aria-hidden={true} />\n {tr(\n 'planmagnet.satisfaction.import.file.ignoredColumns',\n 'These columns are not imported: {{columns}}.',\n { columns: parsed.ignoredColumns.join(', ') }\n )}\n </p>\n )}\n\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md bg-status-error-bg-subtle p-3 text-sm text-status-error-text\"\n data-testid=\"satisfaction-import-error\"\n >\n {error}\n </div>\n )}\n\n {result && (\n <div\n role=\"status\"\n className=\"space-y-1 rounded-md bg-status-success-bg-subtle p-3 text-sm text-status-success-text\"\n data-testid=\"satisfaction-import-result\"\n >\n <p className=\"flex items-center gap-1.5 font-medium\">\n <CheckCircle2 className=\"h-4 w-4\" aria-hidden={true} />\n {result.createdCount === 1\n ? tr(\n 'planmagnet.satisfaction.import.result.createdOne',\n 'Imported 1 new response.'\n )\n : tr(\n 'planmagnet.satisfaction.import.result.created',\n 'Imported {{count}} new responses.',\n {\n count: result.createdCount,\n }\n )}\n </p>\n {result.skippedCount > 0 && (\n <p>\n {tr(\n 'planmagnet.satisfaction.import.result.skipped',\n 'Skipped {{count}} already imported (same externalId — never updated or duplicated): {{ids}}',\n {\n count: result.skippedCount,\n ids:\n result.skippedExternalIds.slice(0, SKIPPED_LISTED).join(', ') +\n (result.skippedExternalIds.length > SKIPPED_LISTED\n ? tr(\n 'planmagnet.satisfaction.import.result.more',\n ' and {{count}} more',\n {\n count: result.skippedExternalIds.length - SKIPPED_LISTED,\n }\n )\n : ''),\n }\n )}\n </p>\n )}\n </div>\n )}\n\n {rows.length > 0 && (\n <div className=\"space-y-2\">\n {failing > 0 && (\n <label className=\"flex items-center gap-2 text-xs\">\n <Checkbox\n checked={onlyProblems}\n onCheckedChange={(checked) => setOnlyProblems(checked === true)}\n data-testid=\"satisfaction-import-only-problems\"\n />\n {tr(\n 'planmagnet.satisfaction.import.preview.onlyProblems',\n 'Show only rows with problems ({{count}})',\n { count: failing }\n )}\n </label>\n )}\n <p className=\"text-sm\" data-testid=\"satisfaction-import-summary\">\n {tr(\n 'planmagnet.satisfaction.import.preview.summary',\n '{{file}} — rows: {{count}}; ready: {{ready}}; with problems: {{problems}}.',\n {\n file: fileName ?? '',\n count: rows.length,\n // A row the service refused is not ready, even though it passed the checks here.\n ready: rows.length - failing,\n problems: failing,\n }\n )}{' '}\n <span className=\"text-muted-foreground\">\n {tr(\n 'planmagnet.satisfaction.import.preview.numbering',\n 'Row 1 is the first row under the header.'\n )}\n </span>\n </p>\n <div className=\"max-h-96 overflow-auto rounded-md border\">\n <table className=\"w-full text-left text-xs\">\n <thead className=\"sticky top-0 bg-muted\">\n <tr>\n <th className=\"px-2 py-1.5 font-medium\">\n {tr('planmagnet.satisfaction.import.preview.row', 'Row')}\n </th>\n <th className=\"px-2 py-1.5 font-medium\">externalId</th>\n <th className=\"px-2 py-1.5 font-medium\">\n {tr('planmagnet.satisfaction.list.score', 'Score')}\n </th>\n <th className=\"px-2 py-1.5 font-medium\">\n {tr('planmagnet.satisfaction.list.answered', 'Answered')}\n </th>\n <th className=\"px-2 py-1.5 font-medium\">\n {tr('planmagnet.satisfaction.list.about', 'About')}\n </th>\n <th className=\"px-2 py-1.5 font-medium\">\n {tr('planmagnet.satisfaction.list.respondent', 'Respondent')}\n </th>\n <th className=\"px-2 py-1.5 font-medium\">\n {tr('planmagnet.satisfaction.import.preview.check', 'Check')}\n </th>\n </tr>\n </thead>\n <tbody>\n {shownRows.map((row) => {\n const server = serverByRow.get(row.row) ?? [];\n const ok = isOk(row);\n return (\n <tr\n key={row.row}\n className={ok ? 'border-t' : 'border-t bg-status-error-bg-subtle/40'}\n data-testid=\"satisfaction-import-row\"\n >\n <td className=\"px-2 py-1.5 align-top tabular-nums\">{row.row}</td>\n <td\n className=\"max-w-40 truncate px-2 py-1.5 align-top font-mono\"\n title={row.cells.externalId}\n >\n {row.cells.externalId}\n </td>\n <td className=\"whitespace-nowrap px-2 py-1.5 align-top\">\n {row.cells.metricType} {row.cells.score}\n </td>\n <td className=\"whitespace-nowrap px-2 py-1.5 align-top\">\n {row.cells.respondedAt}\n </td>\n <td className=\"px-2 py-1.5 align-top\">\n <RowLinks row={row} />\n </td>\n <td className=\"max-w-40 truncate px-2 py-1.5 align-top\">\n {cellIsTrue(row.cells.isAnonymous)\n ? tr('planmagnet.satisfaction.list.anonymous', 'Anonymous')\n : row.cells.respondentName || row.cells.respondentEmail}\n </td>\n <td className=\"px-2 py-1.5 align-top\">\n {ok ? (\n <span className=\"text-status-success-text\">\n {tr('planmagnet.satisfaction.import.preview.ready', 'Ready')}\n </span>\n ) : (\n <ul\n className=\"space-y-0.5 text-status-error-text\"\n data-testid=\"satisfaction-import-row-problems\"\n >\n {row.problems.map((problem, i) => (\n <li key={`c${i}`}>{rowProblemText(tr, problem)}</li>\n ))}\n {server.map((e, i) => (\n <li key={`s${i}`}>\n {e.field ? `${e.field}: ` : ''}\n {e.message}\n </li>\n ))}\n </ul>\n )}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </div>\n )}\n </div>\n\n <ResponsiveDialogFooter>\n {result ? (\n <Button onClick={() => setOpen(false)} data-testid=\"satisfaction-import-done\">\n {tr('planmagnet.satisfaction.import.done', 'Done')}\n </Button>\n ) : (\n <Button\n onClick={() => void onImport()}\n disabled={!canImport}\n data-testid=\"satisfaction-import-submit\"\n >\n {importState.loading\n ? tr('planmagnet.satisfaction.import.importing', 'Importing…')\n : rows.length === 1\n ? tr('planmagnet.satisfaction.import.submitOne', 'Import 1 row')\n : tr('planmagnet.satisfaction.import.submit', 'Import {{count}} rows', {\n count: rows.length,\n })}\n </Button>\n )}\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FormLinks } from './satisfactionForm';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a response is about: a product, a release and/or a project. A release must belong to the product named with it
|
|
5
|
+
* (or to no product) — the release list offers only those, and choosing another product drops a release of the old
|
|
6
|
+
* one instead of leaving a pair the service refuses.
|
|
7
|
+
*/
|
|
8
|
+
export declare function SatisfactionLinkFields({ idPrefix, value, onChange, columns, notes, }: {
|
|
9
|
+
idPrefix: string;
|
|
10
|
+
value: FormLinks;
|
|
11
|
+
onChange: (next: FormLinks) => void;
|
|
12
|
+
/** Side by side (the import dialog) rather than stacked (the response form). */
|
|
13
|
+
columns?: boolean;
|
|
14
|
+
/** A line under a field, e.g. that its linked record was deleted. */
|
|
15
|
+
notes?: Partial<Record<keyof FormLinks, string | null>>;
|
|
16
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=SatisfactionLinkFields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SatisfactionLinkFields.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/SatisfactionLinkFields.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,OAAe,EACf,KAAU,GACX,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACpC,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;CACzD,2CAuDA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import { ProductSelect as n, ProjectSelect as r } from "../controls/Pickers.js";
|
|
4
|
+
import { SatisfactionReleaseSelect as i, useSatisfactionReleaseOptions as a } from "./SatisfactionReleaseSelect.js";
|
|
5
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
6
|
+
//#region src/planmagnet/satisfaction/SatisfactionLinkFields.tsx
|
|
7
|
+
function c({ idPrefix: c, value: l, onChange: u, columns: d = !1, notes: f = {} }) {
|
|
8
|
+
let p = e(), { releases: m } = a(), h = (e) => {
|
|
9
|
+
let t = m.find((e) => e.id === l.releaseId), n = !t || !t.productId || !e || t.productId === e;
|
|
10
|
+
u({
|
|
11
|
+
...l,
|
|
12
|
+
productId: e,
|
|
13
|
+
releaseId: n ? l.releaseId : ""
|
|
14
|
+
});
|
|
15
|
+
}, g = (e) => f[e] ? /* @__PURE__ */ o("p", {
|
|
16
|
+
className: "text-xs text-muted-foreground",
|
|
17
|
+
children: f[e]
|
|
18
|
+
}) : null;
|
|
19
|
+
return /* @__PURE__ */ s("div", {
|
|
20
|
+
className: t("gap-2", d ? "grid sm:grid-cols-3" : "space-y-2"),
|
|
21
|
+
children: [
|
|
22
|
+
/* @__PURE__ */ s("div", { children: [
|
|
23
|
+
/* @__PURE__ */ o("label", {
|
|
24
|
+
htmlFor: `${c}-product`,
|
|
25
|
+
className: "text-xs text-muted-foreground",
|
|
26
|
+
children: p("planmagnet.satisfaction.form.product", "Product")
|
|
27
|
+
}),
|
|
28
|
+
/* @__PURE__ */ o(n, {
|
|
29
|
+
id: `${c}-product`,
|
|
30
|
+
value: l.productId,
|
|
31
|
+
noneLabel: p("planmagnet.satisfaction.form.noProduct", "No product"),
|
|
32
|
+
onChange: h
|
|
33
|
+
}),
|
|
34
|
+
g("productId")
|
|
35
|
+
] }),
|
|
36
|
+
/* @__PURE__ */ s("div", { children: [
|
|
37
|
+
/* @__PURE__ */ o("label", {
|
|
38
|
+
htmlFor: `${c}-release`,
|
|
39
|
+
className: "text-xs text-muted-foreground",
|
|
40
|
+
children: p("planmagnet.satisfaction.form.release", "Release")
|
|
41
|
+
}),
|
|
42
|
+
/* @__PURE__ */ o(i, {
|
|
43
|
+
id: `${c}-release`,
|
|
44
|
+
value: l.releaseId,
|
|
45
|
+
productId: l.productId,
|
|
46
|
+
noneLabel: p("planmagnet.satisfaction.form.noRelease", "No release"),
|
|
47
|
+
onChange: (e) => u({
|
|
48
|
+
...l,
|
|
49
|
+
releaseId: e
|
|
50
|
+
})
|
|
51
|
+
}),
|
|
52
|
+
g("releaseId")
|
|
53
|
+
] }),
|
|
54
|
+
/* @__PURE__ */ s("div", { children: [
|
|
55
|
+
/* @__PURE__ */ o("label", {
|
|
56
|
+
htmlFor: `${c}-project`,
|
|
57
|
+
className: "text-xs text-muted-foreground",
|
|
58
|
+
children: p("planmagnet.satisfaction.form.project", "Project")
|
|
59
|
+
}),
|
|
60
|
+
/* @__PURE__ */ o(r, {
|
|
61
|
+
id: `${c}-project`,
|
|
62
|
+
value: l.projectId,
|
|
63
|
+
noneLabel: p("planmagnet.satisfaction.form.noProject", "No project"),
|
|
64
|
+
onChange: (e) => u({
|
|
65
|
+
...l,
|
|
66
|
+
projectId: e
|
|
67
|
+
})
|
|
68
|
+
}),
|
|
69
|
+
g("projectId")
|
|
70
|
+
] })
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { c as SatisfactionLinkFields };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=SatisfactionLinkFields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SatisfactionLinkFields.js","names":[],"sources":["../../../src/planmagnet/satisfaction/SatisfactionLinkFields.tsx"],"sourcesContent":["import { useTr } from '../../shared/hooks/useTr';\nimport { ProductSelect, ProjectSelect } from '../controls/Pickers';\nimport { cn } from '../utils/cn';\nimport {\n SatisfactionReleaseSelect,\n useSatisfactionReleaseOptions,\n} from './SatisfactionReleaseSelect';\nimport type { FormLinks } from './satisfactionForm';\n\n/**\n * What a response is about: a product, a release and/or a project. A release must belong to the product named with it\n * (or to no product) — the release list offers only those, and choosing another product drops a release of the old\n * one instead of leaving a pair the service refuses.\n */\nexport function SatisfactionLinkFields({\n idPrefix,\n value,\n onChange,\n columns = false,\n notes = {},\n}: {\n idPrefix: string;\n value: FormLinks;\n onChange: (next: FormLinks) => void;\n /** Side by side (the import dialog) rather than stacked (the response form). */\n columns?: boolean;\n /** A line under a field, e.g. that its linked record was deleted. */\n notes?: Partial<Record<keyof FormLinks, string | null>>;\n}) {\n const tr = useTr();\n const { releases } = useSatisfactionReleaseOptions();\n\n const onProduct = (productId: string) => {\n const release = releases.find((r) => r.id === value.releaseId);\n const keepRelease =\n !release || !release.productId || !productId || release.productId === productId;\n onChange({ ...value, productId, releaseId: keepRelease ? value.releaseId : '' });\n };\n\n const note = (key: keyof FormLinks) =>\n notes[key] ? <p className=\"text-xs text-muted-foreground\">{notes[key]}</p> : null;\n\n return (\n <div className={cn('gap-2', columns ? 'grid sm:grid-cols-3' : 'space-y-2')}>\n <div>\n <label htmlFor={`${idPrefix}-product`} className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.satisfaction.form.product', 'Product')}\n </label>\n <ProductSelect\n id={`${idPrefix}-product`}\n value={value.productId}\n noneLabel={tr('planmagnet.satisfaction.form.noProduct', 'No product')}\n onChange={onProduct}\n />\n {note('productId')}\n </div>\n <div>\n <label htmlFor={`${idPrefix}-release`} className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.satisfaction.form.release', 'Release')}\n </label>\n <SatisfactionReleaseSelect\n id={`${idPrefix}-release`}\n value={value.releaseId}\n productId={value.productId}\n noneLabel={tr('planmagnet.satisfaction.form.noRelease', 'No release')}\n onChange={(releaseId) => onChange({ ...value, releaseId })}\n />\n {note('releaseId')}\n </div>\n <div>\n <label htmlFor={`${idPrefix}-project`} className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.satisfaction.form.project', 'Project')}\n </label>\n <ProjectSelect\n id={`${idPrefix}-project`}\n value={value.projectId}\n noneLabel={tr('planmagnet.satisfaction.form.noProject', 'No project')}\n onChange={(projectId) => onChange({ ...value, projectId })}\n />\n {note('projectId')}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;AAcA,SAAgB,EAAuB,EACrC,aACA,UACA,aACA,aAAU,IACV,WAAQ,EAAE,IAST;CACD,IAAM,IAAK,GAAO,EACZ,EAAE,gBAAa,GAA+B,EAE9C,KAAa,MAAsB;EACvC,IAAM,IAAU,EAAS,MAAM,MAAM,EAAE,OAAO,EAAM,UAAU,EACxD,IACJ,CAAC,KAAW,CAAC,EAAQ,aAAa,CAAC,KAAa,EAAQ,cAAc;AACxE,IAAS;GAAE,GAAG;GAAO;GAAW,WAAW,IAAc,EAAM,YAAY;GAAI,CAAC;IAG5E,KAAQ,MACZ,EAAM,KAAO,kBAAC,KAAD;EAAG,WAAU;YAAiC,EAAM;EAAS,CAAA,GAAG;AAE/E,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,SAAS,IAAU,wBAAwB,YAAY;YAA1E;GACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KAAO,SAAS,GAAG,EAAS;KAAW,WAAU;eAC9C,EAAG,wCAAwC,UAAU;KAChD,CAAA;IACR,kBAAC,GAAD;KACE,IAAI,GAAG,EAAS;KAChB,OAAO,EAAM;KACb,WAAW,EAAG,0CAA0C,aAAa;KACrE,UAAU;KACV,CAAA;IACD,EAAK,YAAY;IACd,EAAA,CAAA;GACN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KAAO,SAAS,GAAG,EAAS;KAAW,WAAU;eAC9C,EAAG,wCAAwC,UAAU;KAChD,CAAA;IACR,kBAAC,GAAD;KACE,IAAI,GAAG,EAAS;KAChB,OAAO,EAAM;KACb,WAAW,EAAM;KACjB,WAAW,EAAG,0CAA0C,aAAa;KACrE,WAAW,MAAc,EAAS;MAAE,GAAG;MAAO;MAAW,CAAC;KAC1D,CAAA;IACD,EAAK,YAAY;IACd,EAAA,CAAA;GACN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KAAO,SAAS,GAAG,EAAS;KAAW,WAAU;eAC9C,EAAG,wCAAwC,UAAU;KAChD,CAAA;IACR,kBAAC,GAAD;KACE,IAAI,GAAG,EAAS;KAChB,OAAO,EAAM;KACb,WAAW,EAAG,0CAA0C,aAAa;KACrE,WAAW,MAAc,EAAS;MAAE,GAAG;MAAO;MAAW,CAAC;KAC1D,CAAA;IACD,EAAK,YAAY;IACd,EAAA,CAAA;GACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Every release in the workspace (up to the cap), with its product. */
|
|
2
|
+
export declare function useSatisfactionReleaseOptions(): {
|
|
3
|
+
releases: import('./satisfactionOperations').SatisfactionReleaseOption[];
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: import('@apollo/client').ErrorLike | undefined;
|
|
6
|
+
truncated: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** A release select, narrowed to what `productId` allows. '' means "no release" (`noneLabel`). */
|
|
9
|
+
export declare function SatisfactionReleaseSelect({ id, value, onChange, productId, noneLabel, disabled, invalid, }: {
|
|
10
|
+
id?: string;
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (releaseId: string) => void;
|
|
13
|
+
productId: string;
|
|
14
|
+
noneLabel: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
invalid?: boolean;
|
|
17
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=SatisfactionReleaseSelect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SatisfactionReleaseSelect.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/satisfaction/SatisfactionReleaseSelect.tsx"],"names":[],"mappings":"AAgBA,wEAAwE;AACxE,wBAAgB,6BAA6B;;;;;EAwC5C;AAED,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,EACxC,EAAE,EACF,KAAK,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EACT,QAAgB,EAChB,OAAe,GAChB,EAAE;IACD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,2CA8CA"}
|