@ciridae/ui 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/blocks/comment-composer.d.ts +3 -1
- package/dist/components/blocks/comment-composer.js +13 -7
- package/dist/components/blocks/comment-thread.d.ts +2 -1
- package/dist/components/blocks/comment-thread.js +2 -2
- package/dist/components/blocks/feedback-center-comments.d.ts +3 -1
- package/dist/components/blocks/feedback-center-comments.js +10 -3
- package/dist/components/blocks/feedback-center-footer.d.ts +12 -0
- package/dist/components/blocks/feedback-center-footer.js +10 -0
- package/dist/components/blocks/feedback-center-sheet-parts.d.ts +14 -11
- package/dist/components/blocks/feedback-center-sheet-parts.js +17 -22
- package/dist/components/blocks/feedback-center-sheet.d.ts +8 -2
- package/dist/components/blocks/feedback-center-sheet.js +72 -15
- package/dist/components/blocks/feedback-composer-parts.d.ts +4 -4
- package/dist/components/blocks/feedback-composer-parts.js +4 -4
- package/dist/components/blocks/feedback-composer-types.d.ts +5 -4
- package/dist/components/blocks/feedback-composer.d.ts +7 -3
- package/dist/components/blocks/feedback-composer.js +49 -79
- package/dist/components/blocks/feedback-detail-sheet.d.ts +9 -3
- package/dist/components/blocks/feedback-detail-sheet.js +27 -9
- package/dist/components/blocks/feedback-emergency-fields.d.ts +17 -0
- package/dist/components/blocks/feedback-emergency-fields.js +32 -0
- package/dist/components/blocks/feedback-metrics.js +1 -1
- package/dist/components/blocks/feedback-sheet.d.ts +1 -1
- package/dist/components/blocks/feedback-sheet.js +17 -4
- package/dist/components/blocks/feedback-status-badge.js +3 -3
- package/dist/components/blocks/feedback-table-row.js +8 -18
- package/dist/components/blocks/feedback-table-sort.js +4 -4
- package/dist/components/blocks/feedback-table.js +2 -2
- package/dist/components/blocks/feedback-tracking-types.d.ts +20 -11
- package/dist/components/blocks/feedback-tracking-types.js +15 -7
- package/dist/components/blocks/feedback-type-badge.js +1 -1
- package/dist/components/blocks/feedback-user-detail.d.ts +2 -1
- package/dist/components/blocks/feedback-user-detail.js +13 -6
- package/dist/components/blocks/feedback-user-list.d.ts +3 -1
- package/dist/components/blocks/feedback-user-list.js +11 -3
- package/dist/components/blocks/submit-feedback-composer.d.ts +5 -0
- package/dist/components/blocks/submit-feedback-composer.js +40 -0
- package/dist/components/blocks/use-feedback-composer-state.d.ts +27 -0
- package/dist/components/blocks/use-feedback-composer-state.js +60 -0
- package/dist/components/ui/form-sheet.d.ts +15 -0
- package/dist/components/ui/form-sheet.js +108 -0
- package/dist/entries/feedback-center-footer.d.ts +1 -0
- package/dist/entries/feedback-center-footer.js +1 -0
- package/dist/entries/feedback-emergency-fields.d.ts +1 -0
- package/dist/entries/feedback-emergency-fields.js +1 -0
- package/dist/entries/form-sheet.d.ts +1 -0
- package/dist/entries/form-sheet.js +1 -0
- package/dist/entries/submit-feedback-composer.d.ts +1 -0
- package/dist/entries/submit-feedback-composer.js +1 -0
- package/dist/entries/use-feedback-composer-state.d.ts +1 -0
- package/dist/entries/use-feedback-composer-state.js +1 -0
- package/package.json +1 -1
|
@@ -1,112 +1,82 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { FeedbackComposerActions, FeedbackSubmittedState, FeedbackTypeField, } from "./feedback-composer-parts";
|
|
4
|
+
import { FeedbackEmergencyFields } from "./feedback-emergency-fields";
|
|
5
|
+
import { submitFeedbackComposer } from "./submit-feedback-composer";
|
|
6
|
+
import { useFeedbackComposerState } from "./use-feedback-composer-state";
|
|
4
7
|
import { FileUploadField } from "../ui/file-upload-field";
|
|
5
8
|
import { Field, FieldContent, FieldError, FieldGroup, FieldLabel } from "../ui/field";
|
|
6
9
|
import { Input } from "../ui/input";
|
|
7
10
|
import { Textarea } from "../ui/textarea";
|
|
8
11
|
function FeedbackComposer(props) {
|
|
9
|
-
|
|
12
|
+
// Without its controls, emergency could never be confirmed — a hidden
|
|
13
|
+
// emergency default would soft-lock submit — so it needs urgencyEnabled.
|
|
14
|
+
const defaultUrgency = props.urgencyEnabled ? (props.defaultUrgency ?? "normal") : "normal";
|
|
15
|
+
const state = useFeedbackComposerState(props.defaultType ?? "bug", defaultUrgency);
|
|
10
16
|
const ids = useFeedbackComposerIds();
|
|
11
|
-
|
|
17
|
+
const { onDirtyChange, onUrgencyChange } = props;
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
onDirtyChange?.(state.isDirty);
|
|
20
|
+
}, [onDirtyChange, state.isDirty]);
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
onUrgencyChange?.(state.urgency);
|
|
23
|
+
}, [onUrgencyChange, state.urgency]);
|
|
24
|
+
if (state.isSubmitted) {
|
|
12
25
|
return (_jsx(FeedbackSubmittedState, { onCancel: props.onCancel, onSendAnother: () => {
|
|
13
26
|
state.resetForm();
|
|
14
27
|
props.onSubmittedChange?.(false);
|
|
15
|
-
}
|
|
28
|
+
} }));
|
|
16
29
|
}
|
|
17
30
|
return _jsx(FeedbackComposerForm, { ids: ids, props: props, state: state });
|
|
18
31
|
}
|
|
19
32
|
function FeedbackComposerForm({ ids, props, state, }) {
|
|
20
33
|
const isSubmitting = props.isSubmitting ?? false;
|
|
21
|
-
return (_jsxs("form", { className: "grid gap-(--panel-gap)", id: props.formId, onSubmit: (event) =>
|
|
34
|
+
return (_jsxs("form", { className: "grid gap-(--panel-gap)", id: props.formId, onSubmit: (event) => submitFeedbackComposer(event, props, state), children: [_jsxs(FieldGroup, { children: [state.urgency === "normal" ? (_jsx(FeedbackTypeField, { disabled: isSubmitting, onTypeChange: state.setType, type: state.type })) : null, _jsx(FeedbackSubjectField, { disabled: isSubmitting, error: state.error, errorId: ids.subjectErrorId, fieldId: ids.subjectId, onSubjectChange: state.setSubject, subject: state.subject }), _jsx(FeedbackDescriptionField, { description: state.description, disabled: isSubmitting, error: state.error, errorId: ids.descriptionErrorId, fieldId: ids.descriptionId, onDescriptionChange: state.setDescription }), _jsx(FeedbackAttachmentsField, { props: props, state: state }), _jsx(FeedbackComposerEmergencySection, { ids: ids, props: props, state: state }), state.submitError ? _jsx(FieldError, { children: state.submitError }) : null] }), props.actions === "external" ? null : (_jsx(FeedbackComposerActions, { isSubmitting: isSubmitting, onCancel: props.onCancel, urgency: state.urgency }))] }));
|
|
22
35
|
}
|
|
23
|
-
function FeedbackSubjectField({ disabled, error, fieldId, onSubjectChange, subject, }) {
|
|
24
|
-
|
|
36
|
+
function FeedbackSubjectField({ disabled, error, errorId, fieldId, onSubjectChange, subject, }) {
|
|
37
|
+
const invalid = error === "subject";
|
|
38
|
+
return (_jsxs(Field, { "data-invalid": invalid, children: [_jsx(FieldContent, { children: _jsx(FieldLabel, { htmlFor: fieldId, required: true, children: "Subject" }) }), _jsx(Input, { "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid, "aria-required": "true", disabled: disabled, id: fieldId, maxLength: 255, onChange: (event) => onSubjectChange(event.target.value), placeholder: "Short subject for the team", value: subject }), invalid ? _jsx(FieldError, { id: errorId, children: "Subject is required." }) : null] }));
|
|
25
39
|
}
|
|
26
|
-
function FeedbackDescriptionField({ description, disabled, error, fieldId, onDescriptionChange, }) {
|
|
27
|
-
|
|
40
|
+
function FeedbackDescriptionField({ description, disabled, error, errorId, fieldId, onDescriptionChange, }) {
|
|
41
|
+
const invalid = error === "description";
|
|
42
|
+
return (_jsxs(Field, { "data-invalid": invalid, children: [_jsx(FieldContent, { children: _jsx(FieldLabel, { htmlFor: fieldId, required: true, children: "Feedback" }) }), _jsx(Textarea, { "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid, "aria-required": "true", className: "min-h-40", disabled: disabled, id: fieldId, maxLength: 10000, onChange: (event) => onDescriptionChange(event.target.value), placeholder: "What happened, what did you expect, or what would you like to see?", rows: 6, value: description }), invalid ? _jsx(FieldError, { id: errorId, children: "Feedback is required." }) : null] }));
|
|
28
43
|
}
|
|
29
44
|
function FeedbackAttachmentsField({ props, state, }) {
|
|
30
45
|
if (props.attachmentsEnabled === false)
|
|
31
46
|
return null;
|
|
32
47
|
return (_jsx(FileUploadField, { acceptedFileTypes: props.acceptedFileTypes ?? "image/*,.pdf,.csv,.txt,.doc,.docx,.xls,.xlsx", disabled: props.isSubmitting ?? false, files: state.attachments, maxAttachmentSizeMb: props.maxAttachmentSizeMb, maxAttachments: props.maxAttachments, onFilesChange: state.setAttachments, showConstraints: false, variant: "dropzone" }));
|
|
33
48
|
}
|
|
49
|
+
function FeedbackComposerEmergencySection({ ids, props, state, }) {
|
|
50
|
+
if (!props.urgencyEnabled)
|
|
51
|
+
return null;
|
|
52
|
+
return (_jsx(FeedbackEmergencyFields, { affectedScope: state.affectedScope, affectedScopeId: ids.emergencyAffectedScopeId, confirmed: state.emergencyConfirmed, confirmationDescriptionId: ids.emergencyConfirmationDescriptionId, confirmationErrorId: ids.emergencyConfirmationErrorId, confirmationId: ids.emergencyConfirmationId, confirmationInvalid: state.error === "emergency_confirmation", disabled: props.isSubmitting ?? false, isEmergency: state.urgency === "emergency", onAffectedScopeChange: state.setAffectedScope, onConfirmedChange: (confirmed) => {
|
|
53
|
+
state.setEmergencyConfirmed(confirmed);
|
|
54
|
+
if (confirmed && state.error === "emergency_confirmation") {
|
|
55
|
+
state.setError(undefined);
|
|
56
|
+
}
|
|
57
|
+
}, onEmergencyChange: (emergency) => {
|
|
58
|
+
state.setUrgency(emergency ? "emergency" : "normal");
|
|
59
|
+
if (emergency) {
|
|
60
|
+
state.setType("bug");
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// leaving emergency mode abandons its scope and confirmation
|
|
64
|
+
state.setAffectedScope("unknown");
|
|
65
|
+
state.setEmergencyConfirmed(false);
|
|
66
|
+
}
|
|
67
|
+
} }));
|
|
68
|
+
}
|
|
34
69
|
function useFeedbackComposerIds() {
|
|
35
70
|
const formId = React.useId();
|
|
36
71
|
return {
|
|
72
|
+
descriptionErrorId: `${formId}-description-error`,
|
|
37
73
|
descriptionId: `${formId}-description`,
|
|
74
|
+
emergencyAffectedScopeId: `${formId}-emergency-affected-scope`,
|
|
75
|
+
emergencyConfirmationDescriptionId: `${formId}-emergency-confirmation-description`,
|
|
76
|
+
emergencyConfirmationErrorId: `${formId}-emergency-confirmation-error`,
|
|
77
|
+
emergencyConfirmationId: `${formId}-emergency-confirmation`,
|
|
78
|
+
subjectErrorId: `${formId}-subject-error`,
|
|
38
79
|
subjectId: `${formId}-subject`,
|
|
39
80
|
};
|
|
40
81
|
}
|
|
41
|
-
function useFeedbackComposerState(defaultType) {
|
|
42
|
-
const [attachments, setAttachments] = React.useState([]);
|
|
43
|
-
const [description, setDescription] = React.useState("");
|
|
44
|
-
const [error, setError] = React.useState();
|
|
45
|
-
const [submitError, setSubmitError] = React.useState();
|
|
46
|
-
const [submitResult, setSubmitResult] = React.useState();
|
|
47
|
-
const [subject, setSubject] = React.useState("");
|
|
48
|
-
const [type, setType] = React.useState(defaultType);
|
|
49
|
-
React.useEffect(() => {
|
|
50
|
-
setType(defaultType);
|
|
51
|
-
}, [defaultType]);
|
|
52
|
-
const resetForm = () => {
|
|
53
|
-
setAttachments([]);
|
|
54
|
-
setDescription("");
|
|
55
|
-
setError(undefined);
|
|
56
|
-
setSubmitError(undefined);
|
|
57
|
-
setSubmitResult(undefined);
|
|
58
|
-
setSubject("");
|
|
59
|
-
setType(defaultType);
|
|
60
|
-
};
|
|
61
|
-
return {
|
|
62
|
-
attachments,
|
|
63
|
-
description,
|
|
64
|
-
error,
|
|
65
|
-
resetForm,
|
|
66
|
-
setAttachments,
|
|
67
|
-
setDescription,
|
|
68
|
-
setError,
|
|
69
|
-
setSubmitError,
|
|
70
|
-
setSubmitResult,
|
|
71
|
-
setSubject,
|
|
72
|
-
setType,
|
|
73
|
-
submitError,
|
|
74
|
-
submitResult,
|
|
75
|
-
subject,
|
|
76
|
-
type,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
async function submitFeedback(event, props, state) {
|
|
80
|
-
event.preventDefault();
|
|
81
|
-
const trimmedDescription = state.description.trim();
|
|
82
|
-
const trimmedSubject = state.subject.trim();
|
|
83
|
-
const validationError = getValidationError({
|
|
84
|
-
description: trimmedDescription,
|
|
85
|
-
subject: trimmedSubject,
|
|
86
|
-
});
|
|
87
|
-
state.setError(validationError);
|
|
88
|
-
state.setSubmitError(undefined);
|
|
89
|
-
if (validationError)
|
|
90
|
-
return;
|
|
91
|
-
try {
|
|
92
|
-
const result = await props.onSubmit({
|
|
93
|
-
attachments: state.attachments,
|
|
94
|
-
description: trimmedDescription,
|
|
95
|
-
subject: trimmedSubject,
|
|
96
|
-
type: state.type,
|
|
97
|
-
});
|
|
98
|
-
state.setSubmitResult(result ?? {});
|
|
99
|
-
props.onSubmittedChange?.(true);
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
state.setSubmitError("Unable to submit feedback. Please try again.");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function getValidationError({ description, subject, }) {
|
|
106
|
-
if (!subject)
|
|
107
|
-
return "subject";
|
|
108
|
-
if (!description)
|
|
109
|
-
return "description";
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
82
|
export { FeedbackComposer, };
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { type StakeholderFeedbackItem } from "./feedback-tracking-types";
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type FeedbackStatus, type StakeholderFeedbackItem } from "./feedback-tracking-types";
|
|
3
3
|
type FeedbackDetailSheetProps = {
|
|
4
|
+
/** Rendered after the submission — the slot for a comment thread. */
|
|
5
|
+
conversation?: React.ReactNode;
|
|
4
6
|
feedback?: StakeholderFeedbackItem;
|
|
5
7
|
onOpenChange: (open: boolean) => void;
|
|
8
|
+
/** Renders a status select when present; the app owns the save. */
|
|
9
|
+
onStatusChange?: (status: FeedbackStatus) => void;
|
|
6
10
|
open: boolean;
|
|
11
|
+
/** The selection being saved: shown and locked in the select while set. */
|
|
12
|
+
pendingStatus?: FeedbackStatus;
|
|
7
13
|
};
|
|
8
|
-
declare function FeedbackDetailSheet({ feedback, onOpenChange, open }: FeedbackDetailSheetProps): React.JSX.Element;
|
|
14
|
+
declare function FeedbackDetailSheet({ conversation, feedback, onOpenChange, onStatusChange, open, pendingStatus, }: FeedbackDetailSheetProps): React.JSX.Element;
|
|
9
15
|
export { FeedbackDetailSheet, type FeedbackDetailSheetProps };
|
|
@@ -1,27 +1,45 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
2
3
|
import { PaperclipIcon } from "lucide-react";
|
|
3
4
|
import { formatFeedbackTimestampFull, getFeedbackTimestampDateTime } from "./feedback-date-format";
|
|
4
5
|
import { FeedbackStatusBadge } from "./feedback-status-badge";
|
|
5
6
|
import { FeedbackTypeBadge } from "./feedback-type-badge";
|
|
6
|
-
import { getFeedbackInitials } from "./feedback-tracking-types";
|
|
7
|
+
import { feedbackStatusLabels, feedbackStatusValues, getFeedbackInitials, } from "./feedback-tracking-types";
|
|
7
8
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
|
8
9
|
import { Button } from "../ui/button";
|
|
9
10
|
import { Card } from "../ui/card";
|
|
10
|
-
import { Item, ItemContent, ItemGroup, ItemGroupItem, ItemMedia, ItemTitle } from "../ui/item";
|
|
11
|
+
import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemGroupItem, ItemMedia, ItemTitle, } from "../ui/item";
|
|
12
|
+
import { NativeSelect, NativeSelectOption } from "../ui/native-select";
|
|
11
13
|
import { PropertyList, PropertyListItem, PropertyListLabel, PropertyListValue, } from "../ui/property-list";
|
|
12
14
|
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "../ui/sheet";
|
|
13
|
-
function FeedbackDetailSheet({ feedback, onOpenChange, open }) {
|
|
14
|
-
return (_jsx(Sheet, { onOpenChange: onOpenChange, open: open, children: _jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: feedback?.subject ?? "Feedback" }), _jsx(SheetDescription, { children:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
function FeedbackDetailSheet({ conversation, feedback, onOpenChange, onStatusChange, open, pendingStatus, }) {
|
|
16
|
+
return (_jsx(Sheet, { onOpenChange: onOpenChange, open: open, children: _jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: feedback?.subject ?? "Feedback" }), _jsx(SheetDescription, { children: onStatusChange
|
|
17
|
+
? "Review the submission and set where it stands."
|
|
18
|
+
: "Review the original submission and current progress." })] }), _jsxs(SheetBody, { className: "grid content-start gap-(--panel-gap) pb-(--panel-padding)", children: [feedback ? (_jsx(FeedbackDetailContent, { feedback: feedback, onStatusChange: onStatusChange, pendingStatus: pendingStatus })) : null, feedback ? conversation : null] }), _jsx(SheetFooter, { children: _jsx(Button, { onClick: () => onOpenChange(false), type: "button", children: "Done" }) })] }) }));
|
|
19
|
+
}
|
|
20
|
+
function FeedbackDetailContent({ feedback, onStatusChange, pendingStatus, }) {
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx(FeedbackDetailSummary, { feedback: feedback }), onStatusChange ? (_jsx(FeedbackDetailStatusSection, { onStatusChange: onStatusChange, pendingStatus: pendingStatus, status: feedback.status })) : null, _jsx(FeedbackDetailBody, { feedback: feedback }), _jsx(FeedbackDetailAttachments, { feedback: feedback })] }));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Presentation only — the app owns the save. Selecting fires onStatusChange;
|
|
25
|
+
* while pendingStatus is set the select shows it and locks, so the sheet
|
|
26
|
+
* reflects the in-flight decision until the app's write settles.
|
|
27
|
+
*/
|
|
28
|
+
function FeedbackDetailStatusSection({ onStatusChange, pendingStatus, status, }) {
|
|
29
|
+
const selectId = React.useId();
|
|
30
|
+
return (_jsxs("div", { className: "grid gap-2", children: [_jsx("label", { className: "text-meta text-muted-foreground", htmlFor: selectId, children: "Status" }), _jsx(NativeSelect, { disabled: pendingStatus !== undefined, id: selectId, onChange: (event) => {
|
|
31
|
+
const next = feedbackStatusValues.find((value) => value === event.target.value);
|
|
32
|
+
if (next && next !== status)
|
|
33
|
+
onStatusChange(next);
|
|
34
|
+
}, value: pendingStatus ?? status, children: feedbackStatusValues.map((value) => (_jsx(NativeSelectOption, { value: value, children: feedbackStatusLabels[value] }, value))) })] }));
|
|
18
35
|
}
|
|
19
36
|
function FeedbackDetailSummary({ feedback }) {
|
|
20
37
|
const submitter = feedback.submittedBy;
|
|
21
|
-
|
|
38
|
+
const affiliation = submitter.organization ?? submitter.email ?? "Stakeholder";
|
|
39
|
+
return (_jsxs(Item, { variant: "outline", children: [_jsx(ItemMedia, { children: _jsxs(Avatar, { children: [_jsx(AvatarImage, { alt: "", src: submitter.imageUrl }), _jsx(AvatarFallback, { children: getFeedbackInitials(submitter.name, submitter.email) })] }) }), _jsxs(ItemContent, { children: [_jsx(ItemTitle, { className: "max-w-full truncate", title: submitter.name, children: submitter.name }), _jsx(ItemDescription, { title: affiliation, children: affiliation })] }), _jsx(ItemActions, { children: _jsx(FeedbackStatusBadge, { status: feedback.status }) })] }));
|
|
22
40
|
}
|
|
23
41
|
function FeedbackDetailBody({ feedback }) {
|
|
24
|
-
return (_jsxs("div", { className: "grid gap-(--panel-gap)", children: [_jsxs(PropertyList, { children: [_jsx(FeedbackProperty, { label: "Type", children: _jsx(FeedbackTypeBadge, { type: feedback.type }) }), _jsx(FeedbackProperty, { label: "Submitted", children: _jsx(FeedbackDetailDateValue, { value: feedback.submittedAt }) }), _jsx(FeedbackProperty, { label: "Updated", children: _jsx(FeedbackDetailDateValue, { value: feedback.updatedAt }) }), feedback.referenceId ? (_jsx(FeedbackProperty, { label: "Reference", children: _jsx("span", { className: "font-mono text-xs tabular-nums", children: feedback.referenceId }) })) : null] }), _jsxs(Card, { className: "grid gap-2 p-(--panel-padding)", children: [_jsx("div", { className: "text-meta text-muted-foreground", children: "Feedback" }), _jsx("p", { className: "text-sm whitespace-pre-wrap text-foreground", children: feedback.description })] })] }));
|
|
42
|
+
return (_jsxs("div", { className: "grid gap-(--panel-gap)", children: [_jsxs(PropertyList, { children: [_jsx(FeedbackProperty, { label: "Type", children: _jsx(FeedbackTypeBadge, { type: feedback.type }) }), _jsx(FeedbackProperty, { label: "Submitted", children: _jsx(FeedbackDetailDateValue, { value: feedback.submittedAt }) }), _jsx(FeedbackProperty, { label: "Updated", children: _jsx(FeedbackDetailDateValue, { value: feedback.updatedAt }) }), feedback.referenceId ? (_jsx(FeedbackProperty, { label: "Reference", children: _jsx("span", { className: "font-mono text-xs tabular-nums", children: feedback.referenceId }) })) : null] }), _jsxs(Card, { className: "grid gap-2 p-(--panel-padding)", children: [_jsx("div", { className: "text-meta text-muted-foreground", children: "Feedback" }), feedback.description ? (_jsx("p", { className: "text-sm whitespace-pre-wrap text-foreground", children: feedback.description })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No description provided." }))] })] }));
|
|
25
43
|
}
|
|
26
44
|
function FeedbackProperty({ children, label }) {
|
|
27
45
|
return (_jsxs(PropertyListItem, { children: [_jsx(PropertyListLabel, { children: label }), _jsx(PropertyListValue, { children: children })] }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FeedbackAffectedScope } from "./feedback-composer-types";
|
|
2
|
+
type FeedbackEmergencyFieldsProps = {
|
|
3
|
+
affectedScope: FeedbackAffectedScope;
|
|
4
|
+
affectedScopeId: string;
|
|
5
|
+
confirmed: boolean;
|
|
6
|
+
confirmationDescriptionId: string;
|
|
7
|
+
confirmationErrorId: string;
|
|
8
|
+
confirmationId: string;
|
|
9
|
+
confirmationInvalid: boolean;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
onAffectedScopeChange: (scope: FeedbackAffectedScope) => void;
|
|
12
|
+
onConfirmedChange: (confirmed: boolean) => void;
|
|
13
|
+
onEmergencyChange: (emergency: boolean) => void;
|
|
14
|
+
isEmergency: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare function FeedbackEmergencyFields({ affectedScope, affectedScopeId, confirmed, confirmationDescriptionId, confirmationErrorId, confirmationId, confirmationInvalid, disabled, onAffectedScopeChange, onConfirmedChange, onEmergencyChange, isEmergency, }: FeedbackEmergencyFieldsProps): import("@types/react").JSX.Element;
|
|
17
|
+
export { FeedbackEmergencyFields, type FeedbackEmergencyFieldsProps };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertTriangleIcon } from "lucide-react";
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
import { Checkbox } from "../ui/checkbox";
|
|
6
|
+
import { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from "../ui/field";
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
|
8
|
+
function FeedbackEmergencyFields({ affectedScope, affectedScopeId, confirmed, confirmationDescriptionId, confirmationErrorId, confirmationId, confirmationInvalid, disabled, onAffectedScopeChange, onConfirmedChange, onEmergencyChange, isEmergency, }) {
|
|
9
|
+
if (!isEmergency)
|
|
10
|
+
return _jsx(FeedbackEmergencyEntry, { disabled: disabled, onEmergencyChange: onEmergencyChange });
|
|
11
|
+
return (_jsxs("div", { className: "grid gap-(--panel-gap)", children: [_jsx(FeedbackEmergencyNotice, {}), _jsx(FeedbackAffectedScopeField, { affectedScope: affectedScope, affectedScopeId: affectedScopeId, disabled: disabled, onAffectedScopeChange: onAffectedScopeChange }), _jsx(FeedbackEmergencyConfirmation, { confirmed: confirmed, descriptionId: confirmationDescriptionId, disabled: disabled, errorId: confirmationErrorId, fieldId: confirmationId, invalid: confirmationInvalid, onConfirmedChange: onConfirmedChange }), _jsx(Button, { className: "justify-self-start", disabled: disabled, onClick: () => onEmergencyChange(false), type: "button", variant: "ghost", children: "Use standard feedback instead" })] }));
|
|
12
|
+
}
|
|
13
|
+
function FeedbackEmergencyEntry({ disabled, onEmergencyChange, }) {
|
|
14
|
+
return (_jsxs("div", { className: "border-t pt-(--panel-gap)", children: [_jsxs(Button, { className: "w-full justify-start text-destructive hover:text-destructive", disabled: disabled, onClick: () => onEmergencyChange(true), type: "button", variant: "outline", children: [_jsx(AlertTriangleIcon, {}), "Report an emergency"] }), _jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Use this when service or time-sensitive work is blocked and the team needs to respond now." })] }));
|
|
15
|
+
}
|
|
16
|
+
function FeedbackEmergencyNotice() {
|
|
17
|
+
return (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTriangleIcon, {}), _jsx(AlertTitle, { children: "Emergency feedback" }), _jsx(AlertDescription, { children: "This follows the same support workflow, with an urgent priority and an immediate notification to the configured response team." })] }));
|
|
18
|
+
}
|
|
19
|
+
function FeedbackAffectedScopeField({ affectedScope, affectedScopeId, disabled, onAffectedScopeChange, }) {
|
|
20
|
+
return (_jsxs(Field, { children: [_jsx(FieldContent, { children: _jsx(FieldLabel, { htmlFor: affectedScopeId, required: true, children: "Who is affected?" }) }), _jsxs(Select, { disabled: disabled, onValueChange: (value) => {
|
|
21
|
+
if (isAffectedScope(value))
|
|
22
|
+
onAffectedScopeChange(value);
|
|
23
|
+
}, value: affectedScope, children: [_jsx(SelectTrigger, { "aria-required": "true", className: "w-full", id: affectedScopeId, children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "just_me", children: "Just me" }), _jsx(SelectItem, { value: "my_team", children: "My team" }), _jsx(SelectItem, { value: "everyone", children: "Everyone" }), _jsx(SelectItem, { value: "unknown", children: "I'm not sure" })] })] })] }));
|
|
24
|
+
}
|
|
25
|
+
function FeedbackEmergencyConfirmation({ confirmed, descriptionId, disabled, errorId, fieldId, invalid, onConfirmedChange, }) {
|
|
26
|
+
const describedBy = invalid ? `${descriptionId} ${errorId}` : descriptionId;
|
|
27
|
+
return (_jsxs(Field, { "data-invalid": invalid, orientation: "horizontal", children: [_jsx(Checkbox, { "aria-describedby": describedBy, "aria-invalid": invalid, "aria-required": "true", checked: confirmed, disabled: disabled, id: fieldId, onCheckedChange: (checked) => onConfirmedChange(checked === true) }), _jsxs("div", { className: "grid gap-1", children: [_jsx(FieldLabel, { htmlFor: fieldId, required: true, children: "This needs an immediate response" }), _jsx(FieldDescription, { id: descriptionId, children: "I understand this will notify the configured emergency response team." }), invalid ? (_jsx(FieldError, { id: errorId, children: "Confirm that this needs an immediate response before sending." })) : null] })] }));
|
|
28
|
+
}
|
|
29
|
+
function isAffectedScope(value) {
|
|
30
|
+
return ["everyone", "just_me", "my_team", "unknown"].includes(value);
|
|
31
|
+
}
|
|
32
|
+
export { FeedbackEmergencyFields };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { MetricsStrip, MetricsStripDetail, MetricsStripItem, MetricsStripLabel, MetricsStripValue, } from "../ui/metrics-strip";
|
|
3
3
|
function FeedbackMetricsStrip({ metrics }) {
|
|
4
|
-
return (_jsxs(MetricsStrip, { className: "md:grid-cols-5", children: [_jsx(FeedbackMetricItem, { detail: "
|
|
4
|
+
return (_jsxs(MetricsStrip, { className: "md:grid-cols-3 xl:grid-cols-5", children: [_jsx(FeedbackMetricItem, { detail: "Visible tickets", label: "Total", tone: "neutral", value: metrics.total }), _jsx(FeedbackMetricItem, { detail: "Awaiting triage", label: "Received", tone: "warning", value: metrics.received }), _jsx(FeedbackMetricItem, { detail: "Active work", label: "In progress", tone: "info", value: metrics.inProgress }), _jsx(FeedbackMetricItem, { detail: "Needs a decision", label: "Discussion required", tone: "warning", value: metrics.discussionRequired }), _jsx(FeedbackMetricItem, { detail: "Finished", label: "Completed", tone: "success", value: metrics.completed })] }));
|
|
5
5
|
}
|
|
6
6
|
function FeedbackMetricItem({ detail, label, tone, value, }) {
|
|
7
7
|
return (_jsxs(MetricsStripItem, { children: [_jsx(MetricsStripLabel, { children: label }), _jsx(MetricsStripValue, { children: value }), _jsx(MetricsStripDetail, { className: feedbackMetricToneClasses[tone], children: detail })] }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type FeedbackComposerProps } from "./feedback-composer";
|
|
3
|
-
type FeedbackSheetProps = Omit<FeedbackComposerProps, "actions" | "formId" | "onCancel" | "onSubmittedChange"> & {
|
|
3
|
+
type FeedbackSheetProps = Omit<FeedbackComposerProps, "actions" | "formId" | "onCancel" | "onDirtyChange" | "onSubmittedChange" | "onUrgencyChange"> & {
|
|
4
4
|
onOpenChange: (open: boolean) => void;
|
|
5
5
|
open: boolean;
|
|
6
6
|
};
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { SendIcon } from "lucide-react";
|
|
3
|
+
import { AlertTriangleIcon, SendIcon } from "lucide-react";
|
|
4
4
|
import { FeedbackComposer } from "./feedback-composer";
|
|
5
5
|
import { Button } from "../ui/button";
|
|
6
|
-
import {
|
|
6
|
+
import { FormSheet, useFormSheetActions } from "../ui/form-sheet";
|
|
7
|
+
import { SheetBody, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "../ui/sheet";
|
|
7
8
|
function FeedbackSheet({ onOpenChange, open, ...composerProps }) {
|
|
9
|
+
const [isDirty, setIsDirty] = React.useState(false);
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
if (!open)
|
|
12
|
+
setIsDirty(false);
|
|
13
|
+
}, [open]);
|
|
14
|
+
return (_jsx(FormSheet, { closeDisabled: composerProps.isSubmitting ?? false, isDirty: isDirty, onOpenChange: onOpenChange, open: open, children: _jsx(FeedbackSheetContent, { composerProps: composerProps, onDirtyChange: setIsDirty, open: open }) }));
|
|
15
|
+
}
|
|
16
|
+
function FeedbackSheetContent({ composerProps, onDirtyChange, open, }) {
|
|
17
|
+
const { requestClose } = useFormSheetActions();
|
|
8
18
|
const formId = React.useId();
|
|
9
19
|
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
|
20
|
+
const [urgency, setUrgency] = React.useState("normal");
|
|
10
21
|
React.useEffect(() => {
|
|
11
|
-
if (!open)
|
|
22
|
+
if (!open) {
|
|
12
23
|
setIsSubmitted(false);
|
|
24
|
+
setUrgency("normal");
|
|
25
|
+
}
|
|
13
26
|
}, [open]);
|
|
14
|
-
return (
|
|
27
|
+
return (_jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "Send feedback" }), _jsx(SheetDescription, { children: "Share a bug, feature request, or general note." })] }), _jsx(SheetBody, { children: _jsx(FeedbackComposer, { ...composerProps, actions: "external", formId: formId, onCancel: requestClose, onDirtyChange: onDirtyChange, onSubmittedChange: setIsSubmitted, onUrgencyChange: setUrgency }) }), isSubmitted ? null : (_jsxs(SheetFooter, { children: [_jsx(Button, { disabled: composerProps.isSubmitting, onClick: requestClose, type: "button", variant: "outline", children: "Cancel" }), _jsxs(Button, { form: formId, loading: composerProps.isSubmitting, type: "submit", variant: urgency === "emergency" ? "destructive" : "default", children: [urgency === "emergency" ? _jsx(AlertTriangleIcon, {}) : _jsx(SendIcon, {}), urgency === "emergency" ? "Send emergency" : "Submit feedback"] })] }))] }));
|
|
15
28
|
}
|
|
16
29
|
export { FeedbackSheet };
|
|
@@ -5,10 +5,10 @@ function FeedbackStatusBadge({ status }) {
|
|
|
5
5
|
return _jsx(Badge, { variant: feedbackStatusVariants[status], children: feedbackStatusLabels[status] });
|
|
6
6
|
}
|
|
7
7
|
const feedbackStatusVariants = {
|
|
8
|
+
completed: "success",
|
|
9
|
+
discussion_required: "warning",
|
|
8
10
|
in_progress: "info",
|
|
9
|
-
|
|
10
|
-
planned: "info",
|
|
11
|
+
no_action: "secondary",
|
|
11
12
|
received: "outline",
|
|
12
|
-
resolved: "success",
|
|
13
13
|
};
|
|
14
14
|
export { FeedbackStatusBadge };
|
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { AlertTriangleIcon, MessageCircleMoreIcon } from "lucide-react";
|
|
3
3
|
import { formatFeedbackTimestamp, getFeedbackTimestampDateTime } from "./feedback-date-format";
|
|
4
4
|
import { FeedbackStatusBadge } from "./feedback-status-badge";
|
|
5
5
|
import { FeedbackTypeBadge } from "./feedback-type-badge";
|
|
6
6
|
import { getFeedbackInitials } from "./feedback-tracking-types";
|
|
7
7
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
|
8
|
+
import { Badge } from "../ui/badge";
|
|
9
|
+
import { Button } from "../ui/button";
|
|
8
10
|
import { TableCell, TableRow } from "../ui/table";
|
|
9
11
|
function FeedbackTableRow({ item, onSelectFeedback, }) {
|
|
10
|
-
return (_jsxs(TableRow, { className: onSelectFeedback ? "cursor-pointer" : undefined, onClick: () => onSelectFeedback?.(item),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return;
|
|
15
|
-
if (event.key !== "Enter" && event.key !== " ")
|
|
16
|
-
return;
|
|
17
|
-
event.preventDefault();
|
|
18
|
-
onSelectFeedback(item);
|
|
19
|
-
}, role: onSelectFeedback ? "button" : undefined, tabIndex: onSelectFeedback ? 0 : undefined, children: [_jsx(TableCell, { className: "max-w-[22rem]", children: _jsx(FeedbackSummaryCell, { item: item }) }), _jsx(TableCell, { children: _jsx(FeedbackTypeBadge, { type: item.type }) }), _jsx(TableCell, { children: _jsx(FeedbackSubmitterCell, { item: item }) }), _jsx(TableCell, { children: _jsx(FeedbackStatusBadge, { status: item.status }) }), _jsx(TableCell, { children: _jsx(FeedbackDateValue, { value: item.submittedAt }) }), _jsx(TableCell, { children: _jsx(FeedbackDateValue, { value: item.updatedAt }) }), _jsx(TableCell, { className: "text-right", children: _jsx(FeedbackAttachmentCount, { item: item }) })] }));
|
|
12
|
+
return (_jsxs(TableRow, { className: onSelectFeedback ? "cursor-pointer focus-within:bg-muted/60" : undefined, onClick: () => onSelectFeedback?.(item), children: [_jsx(TableCell, { className: "max-w-[22rem]", children: onSelectFeedback ? (_jsx(Button, { "aria-label": `Open feedback: ${item.subject}`, className: "h-auto w-full justify-start p-0 text-left whitespace-normal hover:bg-transparent", onClick: (event) => {
|
|
13
|
+
event.stopPropagation();
|
|
14
|
+
onSelectFeedback(item);
|
|
15
|
+
}, type: "button", variant: "ghost", children: _jsx(FeedbackSummaryCell, { item: item }) })) : (_jsx(FeedbackSummaryCell, { item: item })) }), _jsx(TableCell, { children: _jsx(FeedbackTypeBadge, { type: item.type }) }), _jsx(TableCell, { children: _jsx(FeedbackSubmitterCell, { item: item }) }), _jsx(TableCell, { children: _jsx(FeedbackStatusBadge, { status: item.status }) }), _jsx(TableCell, { children: _jsx(FeedbackDateValue, { value: item.submittedAt }) }), _jsx(TableCell, { children: _jsx(FeedbackDateValue, { value: item.updatedAt }) })] }));
|
|
20
16
|
}
|
|
21
17
|
function FeedbackSummaryCell({ item }) {
|
|
22
|
-
return (_jsxs("
|
|
18
|
+
return (_jsxs("span", { className: "grid min-w-0 gap-1", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate text-sm font-medium text-foreground", children: item.subject }), item.urgency === "emergency" ? (_jsxs(Badge, { variant: "destructive", children: [_jsx(AlertTriangleIcon, {}), "Emergency"] })) : null, item.hasUnreadCustomerReply ? (_jsxs(Badge, { variant: "secondary", children: [_jsx(MessageCircleMoreIcon, {}), "Customer replied"] })) : null] }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: item.description }), _jsx("span", { className: "font-mono text-xs text-muted-foreground tabular-nums", children: item.referenceId ?? item.id })] }));
|
|
23
19
|
}
|
|
24
20
|
function FeedbackSubmitterCell({ item }) {
|
|
25
21
|
const submitter = item.submittedBy;
|
|
@@ -28,10 +24,4 @@ function FeedbackSubmitterCell({ item }) {
|
|
|
28
24
|
function FeedbackDateValue({ value }) {
|
|
29
25
|
return (_jsx("time", { className: "font-mono text-xs text-muted-foreground tabular-nums", dateTime: getFeedbackTimestampDateTime(value), children: formatFeedbackTimestamp(value) }));
|
|
30
26
|
}
|
|
31
|
-
function FeedbackAttachmentCount({ item }) {
|
|
32
|
-
const count = item.attachments?.length ?? 0;
|
|
33
|
-
if (count === 0)
|
|
34
|
-
return _jsx("span", { className: "text-xs text-muted-foreground", children: "None" });
|
|
35
|
-
return (_jsxs("span", { className: "inline-flex items-center justify-end gap-1 font-mono text-xs text-muted-foreground tabular-nums", children: [_jsx(PaperclipIcon, { className: "size-3.5" }), count] }));
|
|
36
|
-
}
|
|
37
27
|
export { FeedbackTableRow };
|
|
@@ -67,11 +67,11 @@ const defaultFeedbackTableSort = {
|
|
|
67
67
|
key: "submittedAt",
|
|
68
68
|
};
|
|
69
69
|
const statusSortOrder = {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
completed: 3,
|
|
71
|
+
discussion_required: 2,
|
|
72
|
+
in_progress: 1,
|
|
73
|
+
no_action: 4,
|
|
73
74
|
received: 0,
|
|
74
|
-
resolved: 4,
|
|
75
75
|
};
|
|
76
76
|
const typeSortOrder = {
|
|
77
77
|
bug: 0,
|
|
@@ -24,7 +24,7 @@ function FeedbackTable({ defaultSort = defaultFeedbackTableSort, emptyAction, em
|
|
|
24
24
|
return (_jsx(DataTable, { children: _jsx(DataTableContent, { children: _jsx(FeedbackTableContent, { items: sortedItems, onSelectFeedback: onSelectFeedback, onSortChange: handleSortChange, sort: activeSort }) }) }));
|
|
25
25
|
}
|
|
26
26
|
function FeedbackTableLoading() {
|
|
27
|
-
return (_jsx(DataTable, { children: _jsx(DataTableSkeleton, { columns:
|
|
27
|
+
return (_jsx(DataTable, { children: _jsx(DataTableSkeleton, { columns: 6, rows: 6 }) }));
|
|
28
28
|
}
|
|
29
29
|
function FeedbackTableEmpty({ action, description, title, }) {
|
|
30
30
|
return (_jsx(DataTable, { children: _jsx(DataTableEmpty, { className: "min-h-80 border-b-0", children: _jsx(FeedbackTableEmptyContent, { action: action, description: description, title: title }) }) }));
|
|
@@ -36,7 +36,7 @@ function FeedbackTableContent({ items, onSelectFeedback, onSortChange, sort, })
|
|
|
36
36
|
return (_jsxs(Table, { children: [_jsx(FeedbackTableHeader, { onSortChange: onSortChange, sort: sort }), _jsx(TableBody, { children: items.map((item) => (_jsx(FeedbackTableRow, { item: item, onSelectFeedback: onSelectFeedback }, item.id))) })] }));
|
|
37
37
|
}
|
|
38
38
|
function FeedbackTableHeader({ onSortChange, sort, }) {
|
|
39
|
-
return (_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Feedback" }), _jsx(FeedbackSortableHead, { label: "Type", onSortChange: onSortChange, sort: sort, sortKey: "type" }), _jsx(FeedbackSortableHead, { label: "Submitted by", onSortChange: onSortChange, sort: sort, sortKey: "submittedBy" }), _jsx(FeedbackSortableHead, { label: "Status", onSortChange: onSortChange, sort: sort, sortKey: "status" }), _jsx(FeedbackSortableHead, { label: "Submitted", onSortChange: onSortChange, sort: sort, sortKey: "submittedAt" }), _jsx(FeedbackSortableHead, { label: "Updated", onSortChange: onSortChange, sort: sort, sortKey: "updatedAt" })
|
|
39
|
+
return (_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Feedback" }), _jsx(FeedbackSortableHead, { label: "Type", onSortChange: onSortChange, sort: sort, sortKey: "type" }), _jsx(FeedbackSortableHead, { label: "Submitted by", onSortChange: onSortChange, sort: sort, sortKey: "submittedBy" }), _jsx(FeedbackSortableHead, { label: "Status", onSortChange: onSortChange, sort: sort, sortKey: "status" }), _jsx(FeedbackSortableHead, { label: "Submitted", onSortChange: onSortChange, sort: sort, sortKey: "submittedAt" }), _jsx(FeedbackSortableHead, { label: "Updated", onSortChange: onSortChange, sort: sort, sortKey: "updatedAt" })] }) }));
|
|
40
40
|
}
|
|
41
41
|
function FeedbackSortableHead({ label, onSortChange, sort, sortKey, }) {
|
|
42
42
|
const active = sort.key === sortKey;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
declare const feedbackStatusValues: readonly ["received", "in_progress", "discussion_required", "completed", "no_action"];
|
|
2
|
+
type FeedbackStatus = (typeof feedbackStatusValues)[number];
|
|
2
3
|
type StakeholderFeedbackType = "bug" | "feature" | "general";
|
|
4
|
+
type FeedbackUrgency = "emergency" | "normal";
|
|
5
|
+
type FeedbackAffectedScope = "everyone" | "just_me" | "my_team" | "unknown";
|
|
3
6
|
type FeedbackSubmittedBy = {
|
|
4
7
|
email?: string;
|
|
5
8
|
imageUrl?: string;
|
|
@@ -11,8 +14,10 @@ type FeedbackAttachment = {
|
|
|
11
14
|
url?: string;
|
|
12
15
|
};
|
|
13
16
|
type StakeholderFeedbackItem = {
|
|
17
|
+
affectedScope?: FeedbackAffectedScope;
|
|
14
18
|
attachments?: FeedbackAttachment[];
|
|
15
19
|
description: string;
|
|
20
|
+
hasUnreadCustomerReply?: boolean;
|
|
16
21
|
id: string;
|
|
17
22
|
referenceId?: string;
|
|
18
23
|
status: FeedbackStatus;
|
|
@@ -21,24 +26,28 @@ type StakeholderFeedbackItem = {
|
|
|
21
26
|
submittedBy: FeedbackSubmittedBy;
|
|
22
27
|
type: StakeholderFeedbackType;
|
|
23
28
|
updatedAt?: string;
|
|
29
|
+
urgency?: FeedbackUrgency;
|
|
24
30
|
};
|
|
25
31
|
type UserFeedbackItem = {
|
|
32
|
+
affectedScope?: FeedbackAffectedScope;
|
|
26
33
|
attachments?: FeedbackAttachment[];
|
|
27
34
|
description: string;
|
|
35
|
+
hasUnreadReply?: boolean;
|
|
28
36
|
id: string;
|
|
29
|
-
referenceId?: string;
|
|
30
37
|
status: FeedbackStatus;
|
|
31
38
|
subject: string;
|
|
32
39
|
submittedAt: string;
|
|
33
40
|
type: StakeholderFeedbackType;
|
|
41
|
+
unreadReplyCount?: number;
|
|
34
42
|
updatedAt?: string;
|
|
43
|
+
urgency?: FeedbackUrgency;
|
|
35
44
|
};
|
|
36
45
|
type FeedbackMetrics = {
|
|
37
|
-
|
|
46
|
+
completed: number;
|
|
47
|
+
discussionRequired: number;
|
|
38
48
|
inProgress: number;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
triagedThisWeek: number;
|
|
49
|
+
received: number;
|
|
50
|
+
total: number;
|
|
42
51
|
};
|
|
43
52
|
declare const feedbackStatusLabels: Record<FeedbackStatus, string>;
|
|
44
53
|
declare const feedbackTypeLabels: Record<StakeholderFeedbackType, string>;
|
|
@@ -50,16 +59,16 @@ declare const feedbackStatusOptions: readonly [{
|
|
|
50
59
|
readonly value: "received";
|
|
51
60
|
}, {
|
|
52
61
|
readonly label: string;
|
|
53
|
-
readonly value: "
|
|
62
|
+
readonly value: "in_progress";
|
|
54
63
|
}, {
|
|
55
64
|
readonly label: string;
|
|
56
|
-
readonly value: "
|
|
65
|
+
readonly value: "discussion_required";
|
|
57
66
|
}, {
|
|
58
67
|
readonly label: string;
|
|
59
|
-
readonly value: "
|
|
68
|
+
readonly value: "completed";
|
|
60
69
|
}, {
|
|
61
70
|
readonly label: string;
|
|
62
|
-
readonly value: "
|
|
71
|
+
readonly value: "no_action";
|
|
63
72
|
}];
|
|
64
73
|
declare const feedbackTypeOptions: readonly [{
|
|
65
74
|
readonly label: "All types";
|
|
@@ -75,4 +84,4 @@ declare const feedbackTypeOptions: readonly [{
|
|
|
75
84
|
readonly value: "general";
|
|
76
85
|
}];
|
|
77
86
|
declare function getFeedbackInitials(name: string, email: string | undefined): string;
|
|
78
|
-
export { feedbackStatusLabels, feedbackStatusOptions, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, type FeedbackAttachment, type FeedbackMetrics, type FeedbackStatus, type FeedbackSubmittedBy, type StakeholderFeedbackItem, type StakeholderFeedbackType, type UserFeedbackItem, };
|
|
87
|
+
export { feedbackStatusLabels, feedbackStatusOptions, feedbackStatusValues, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, type FeedbackAffectedScope, type FeedbackAttachment, type FeedbackMetrics, type FeedbackStatus, type FeedbackSubmittedBy, type FeedbackUrgency, type StakeholderFeedbackItem, type StakeholderFeedbackType, type UserFeedbackItem, };
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
// The canonical status list, in triage order; the union derives from it.
|
|
2
|
+
const feedbackStatusValues = [
|
|
3
|
+
"received",
|
|
4
|
+
"in_progress",
|
|
5
|
+
"discussion_required",
|
|
6
|
+
"completed",
|
|
7
|
+
"no_action",
|
|
8
|
+
];
|
|
1
9
|
const feedbackStatusLabels = {
|
|
10
|
+
completed: "Completed",
|
|
11
|
+
discussion_required: "Discussion required",
|
|
2
12
|
in_progress: "In progress",
|
|
3
|
-
|
|
4
|
-
planned: "Planned",
|
|
13
|
+
no_action: "No action",
|
|
5
14
|
received: "Received",
|
|
6
|
-
resolved: "Resolved",
|
|
7
15
|
};
|
|
8
16
|
const feedbackTypeLabels = {
|
|
9
17
|
bug: "Bug",
|
|
@@ -13,10 +21,10 @@ const feedbackTypeLabels = {
|
|
|
13
21
|
const feedbackStatusOptions = [
|
|
14
22
|
{ label: "All statuses", value: "all" },
|
|
15
23
|
{ label: feedbackStatusLabels.received, value: "received" },
|
|
16
|
-
{ label: feedbackStatusLabels.in_review, value: "in_review" },
|
|
17
|
-
{ label: feedbackStatusLabels.planned, value: "planned" },
|
|
18
24
|
{ label: feedbackStatusLabels.in_progress, value: "in_progress" },
|
|
19
|
-
{ label: feedbackStatusLabels.
|
|
25
|
+
{ label: feedbackStatusLabels.discussion_required, value: "discussion_required" },
|
|
26
|
+
{ label: feedbackStatusLabels.completed, value: "completed" },
|
|
27
|
+
{ label: feedbackStatusLabels.no_action, value: "no_action" },
|
|
20
28
|
];
|
|
21
29
|
const feedbackTypeOptions = [
|
|
22
30
|
{ label: "All types", value: "all" },
|
|
@@ -33,4 +41,4 @@ function getFeedbackInitials(name, email) {
|
|
|
33
41
|
.map((part) => part.charAt(0).toUpperCase())
|
|
34
42
|
.join("");
|
|
35
43
|
}
|
|
36
|
-
export { feedbackStatusLabels, feedbackStatusOptions, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, };
|
|
44
|
+
export { feedbackStatusLabels, feedbackStatusOptions, feedbackStatusValues, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, };
|
|
@@ -3,8 +3,9 @@ import type { UserFeedbackItem } from "./feedback-tracking-types";
|
|
|
3
3
|
type FeedbackUserDetailProps = {
|
|
4
4
|
activityLabel?: string;
|
|
5
5
|
activitySlot?: React.ReactNode;
|
|
6
|
+
evidenceSlot?: React.ReactNode;
|
|
6
7
|
feedback?: UserFeedbackItem;
|
|
7
8
|
onBack?: () => void;
|
|
8
9
|
};
|
|
9
|
-
declare function FeedbackUserDetail({ activityLabel, activitySlot, feedback, onBack, }: FeedbackUserDetailProps): React.JSX.Element;
|
|
10
|
+
declare function FeedbackUserDetail({ activityLabel, activitySlot, evidenceSlot, feedback, onBack, }: FeedbackUserDetailProps): React.JSX.Element;
|
|
10
11
|
export { FeedbackUserDetail, type FeedbackUserDetailProps };
|