@flamingo-stack/openframe-frontend-core 0.0.562 → 0.0.563-snapshot.20260824140722
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/{chunk-4PCVRVRP.cjs → chunk-6OPEULTD.cjs} +9 -9
- package/dist/{chunk-4PCVRVRP.cjs.map → chunk-6OPEULTD.cjs.map} +1 -1
- package/dist/{chunk-T2WSZ2W4.js → chunk-7D62XVHH.js} +6 -6
- package/dist/{chunk-ACIVGQA4.js → chunk-ASULU3T2.js} +2 -2
- package/dist/{chunk-3QP6VIXF.cjs → chunk-AZPUAPXW.cjs} +469 -471
- package/dist/chunk-AZPUAPXW.cjs.map +1 -0
- package/dist/{chunk-JVXF5DIA.js → chunk-BK7V6CH5.js} +831 -833
- package/dist/chunk-BK7V6CH5.js.map +1 -0
- package/dist/{chunk-P4G3I3OE.js → chunk-CP7TMQBH.js} +2 -2
- package/dist/{chunk-FFQ7IHZZ.js → chunk-CQNK6RS6.js} +2 -2
- package/dist/{chunk-ICAWPAGF.js → chunk-DPMOEYEM.js} +2 -2
- package/dist/{chunk-PRXMZB6K.cjs → chunk-E6PW7GRQ.cjs} +62 -62
- package/dist/{chunk-PRXMZB6K.cjs.map → chunk-E6PW7GRQ.cjs.map} +1 -1
- package/dist/{chunk-TGREMQ5Z.cjs → chunk-FG4BSJ4A.cjs} +26 -26
- package/dist/{chunk-TGREMQ5Z.cjs.map → chunk-FG4BSJ4A.cjs.map} +1 -1
- package/dist/{chunk-DHB2ODZC.js → chunk-G2VDS3M5.js} +2 -2
- package/dist/{chunk-WEMZGECB.cjs → chunk-GEC2NHQH.cjs} +20 -20
- package/dist/{chunk-WEMZGECB.cjs.map → chunk-GEC2NHQH.cjs.map} +1 -1
- package/dist/{chunk-BHHJTOKI.cjs → chunk-J7R3VZL5.cjs} +14 -14
- package/dist/{chunk-BHHJTOKI.cjs.map → chunk-J7R3VZL5.cjs.map} +1 -1
- package/dist/{chunk-NIQGA3YT.js → chunk-NQ6UF4LP.js} +4 -4
- package/dist/{chunk-PL6FTS27.js → chunk-NUKQWLOG.js} +5 -5
- package/dist/{chunk-UNR2ROYX.js → chunk-O3SEC45L.js} +4 -4
- package/dist/{chunk-E5L6FGRF.cjs → chunk-OYFXHET4.cjs} +7 -7
- package/dist/{chunk-E5L6FGRF.cjs.map → chunk-OYFXHET4.cjs.map} +1 -1
- package/dist/{chunk-W6RG3S5S.cjs → chunk-PKCZL56Y.cjs} +4 -4
- package/dist/{chunk-W6RG3S5S.cjs.map → chunk-PKCZL56Y.cjs.map} +1 -1
- package/dist/{chunk-D6I24QAR.cjs → chunk-PT4GS4CE.cjs} +3 -3
- package/dist/{chunk-D6I24QAR.cjs.map → chunk-PT4GS4CE.cjs.map} +1 -1
- package/dist/{chunk-35U4SIQA.js → chunk-T3P56JKN.js} +2 -2
- package/dist/{chunk-5HD3SSC2.js → chunk-TDYF6Q2D.js} +2 -2
- package/dist/{chunk-AYJAWSDY.cjs → chunk-U5EVZLL5.cjs} +5 -5
- package/dist/{chunk-AYJAWSDY.cjs.map → chunk-U5EVZLL5.cjs.map} +1 -1
- package/dist/{chunk-JAJKPAV3.cjs → chunk-VVDLVCPW.cjs} +39 -39
- package/dist/{chunk-JAJKPAV3.cjs.map → chunk-VVDLVCPW.cjs.map} +1 -1
- package/dist/{chunk-WCHOWGKI.cjs → chunk-WGDZW237.cjs} +86 -86
- package/dist/{chunk-WCHOWGKI.cjs.map → chunk-WGDZW237.cjs.map} +1 -1
- package/dist/{chunk-6U2VJFKW.js → chunk-X4JKXYY5.js} +2 -2
- package/dist/{chunk-VYHMCNZT.cjs → chunk-XFI5PCER.cjs} +31 -31
- package/dist/{chunk-VYHMCNZT.cjs.map → chunk-XFI5PCER.cjs.map} +1 -1
- package/dist/{chunk-RWGX2HIB.cjs → chunk-YFLSOYX4.cjs} +11 -11
- package/dist/{chunk-RWGX2HIB.cjs.map → chunk-YFLSOYX4.cjs.map} +1 -1
- package/dist/{chunk-3QYCR7I6.js → chunk-Z4VU6MJ4.js} +2 -2
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/form.d.ts +1 -1
- package/dist/components/form.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +131 -131
- package/dist/components/index.js +11 -11
- package/dist/components/meeting-scheduler/index.cjs +30 -30
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts +3 -3
- package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts.map +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/dashboard-info-card.d.ts +1 -1
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/components/ui/ticket-info-section.d.ts +10 -6
- package/dist/components/ui/ticket-info-section.d.ts.map +1 -1
- package/dist/components/ui/ticket-status-tag.d.ts +8 -1
- package/dist/components/ui/ticket-status-tag.d.ts.map +1 -1
- package/dist/hooks/use-meeting-booking.d.ts +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/features/board/types.ts +1 -1
- package/src/components/ui/dashboard-info-card.tsx +1 -1
- package/src/components/ui/ticket-info-section.tsx +90 -87
- package/src/components/ui/ticket-status-tag.tsx +74 -52
- package/src/stories/.Board.stories.md +1 -1
- package/src/stories/Board.stories.tsx +4 -4
- package/src/stories/DashboardInfoCard.stories.tsx +1 -1
- package/src/stories/InfoSection.stories.tsx +1 -1
- package/src/stories/TicketEventMessage.stories.tsx +1 -1
- package/dist/chunk-3QP6VIXF.cjs.map +0 -1
- package/dist/chunk-JVXF5DIA.js.map +0 -1
- /package/dist/{chunk-T2WSZ2W4.js.map → chunk-7D62XVHH.js.map} +0 -0
- /package/dist/{chunk-ACIVGQA4.js.map → chunk-ASULU3T2.js.map} +0 -0
- /package/dist/{chunk-P4G3I3OE.js.map → chunk-CP7TMQBH.js.map} +0 -0
- /package/dist/{chunk-FFQ7IHZZ.js.map → chunk-CQNK6RS6.js.map} +0 -0
- /package/dist/{chunk-ICAWPAGF.js.map → chunk-DPMOEYEM.js.map} +0 -0
- /package/dist/{chunk-DHB2ODZC.js.map → chunk-G2VDS3M5.js.map} +0 -0
- /package/dist/{chunk-NIQGA3YT.js.map → chunk-NQ6UF4LP.js.map} +0 -0
- /package/dist/{chunk-PL6FTS27.js.map → chunk-NUKQWLOG.js.map} +0 -0
- /package/dist/{chunk-UNR2ROYX.js.map → chunk-O3SEC45L.js.map} +0 -0
- /package/dist/{chunk-35U4SIQA.js.map → chunk-T3P56JKN.js.map} +0 -0
- /package/dist/{chunk-5HD3SSC2.js.map → chunk-TDYF6Q2D.js.map} +0 -0
- /package/dist/{chunk-6U2VJFKW.js.map → chunk-X4JKXYY5.js.map} +0 -0
- /package/dist/{chunk-3QYCR7I6.js.map → chunk-Z4VU6MJ4.js.map} +0 -0
|
@@ -1,117 +1,128 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
import * as React from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { AssigneeDropdown, type TicketAssigneeOption } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import type { TicketNote } from
|
|
14
|
-
import { TicketNotesSection } from
|
|
15
|
-
import {
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
|
|
6
|
+
import { UserIcon } from '../icons-v2-generated/users/user-icon';
|
|
7
|
+
import { AssigneeDropdown, type TicketAssigneeOption } from './assignee-dropdown';
|
|
8
|
+
import { SimpleMarkdownRenderer } from './markdown/simple-markdown-renderer';
|
|
9
|
+
import { SquareAvatar } from './square-avatar';
|
|
10
|
+
import { Tag } from './tag';
|
|
11
|
+
import { type TicketAttachment, TicketAttachmentsList } from './ticket-attachments-list';
|
|
12
|
+
import { TicketDetailSection } from './ticket-detail-section';
|
|
13
|
+
import type { TicketNote } from './ticket-note-card';
|
|
14
|
+
import { TicketNotesSection } from './ticket-notes-section';
|
|
15
|
+
import { TicketStatusTag, type TicketStatusTagOption } from './ticket-status-tag';
|
|
16
16
|
|
|
17
|
-
export type { TicketAssigneeOption }
|
|
17
|
+
export type { TicketAssigneeOption };
|
|
18
18
|
|
|
19
19
|
export interface TicketInfoSectionProps {
|
|
20
20
|
/** Organization name and image */
|
|
21
21
|
organization?: {
|
|
22
|
-
name: string
|
|
23
|
-
imageSrc?: string
|
|
24
|
-
}
|
|
22
|
+
name: string;
|
|
23
|
+
imageSrc?: string;
|
|
24
|
+
};
|
|
25
25
|
/** User name */
|
|
26
|
-
user?: string
|
|
26
|
+
user?: string;
|
|
27
27
|
/** Device info */
|
|
28
28
|
device?: {
|
|
29
|
-
name: string
|
|
30
|
-
icon?: React.ReactNode
|
|
31
|
-
onClick?: () => void
|
|
32
|
-
}
|
|
29
|
+
name: string;
|
|
30
|
+
icon?: React.ReactNode;
|
|
31
|
+
onClick?: () => void;
|
|
32
|
+
};
|
|
33
33
|
/** Status tag */
|
|
34
|
-
status?: string
|
|
34
|
+
status?: string;
|
|
35
35
|
/** Display label for the status tag (e.g. a custom status name). */
|
|
36
|
-
statusLabel?: string
|
|
36
|
+
statusLabel?: string;
|
|
37
37
|
/** Hex color for the status tag (e.g. a custom status color). */
|
|
38
|
-
statusColor?: string
|
|
38
|
+
statusColor?: string;
|
|
39
39
|
/** When provided, the status tag becomes an inline changer with these options. */
|
|
40
|
-
statusOptions?: TicketStatusTagOption[]
|
|
40
|
+
statusOptions?: TicketStatusTagOption[];
|
|
41
41
|
/** Called with the chosen status id from the inline status dropdown. */
|
|
42
|
-
onStatusSelect?: (id: string) => void
|
|
42
|
+
onStatusSelect?: (id: string) => void;
|
|
43
43
|
/** Disables the inline status dropdown while a change is in flight. */
|
|
44
|
-
isStatusPending?: boolean
|
|
44
|
+
isStatusPending?: boolean;
|
|
45
|
+
/** Locks the status: renders the plain tag instead of the dropdown (e.g. a pending approval blocks the transition). */
|
|
46
|
+
isStatusDisabled?: boolean;
|
|
47
|
+
/** Reason shown in a tooltip over the status tag when `isStatusDisabled`. */
|
|
48
|
+
statusDisabledReason?: string;
|
|
45
49
|
/** Expand button click handler */
|
|
46
|
-
onExpand?: () => void
|
|
50
|
+
onExpand?: () => void;
|
|
47
51
|
/** Whether the section is expanded */
|
|
48
|
-
expanded?: boolean
|
|
52
|
+
expanded?: boolean;
|
|
49
53
|
/** Additional className */
|
|
50
|
-
className?: string
|
|
54
|
+
className?: string;
|
|
51
55
|
|
|
52
56
|
// --- Expanded view props ---
|
|
53
57
|
|
|
54
58
|
/** Assigned person info with inline dropdown */
|
|
55
59
|
assigned?: {
|
|
56
60
|
currentAssignee?: {
|
|
57
|
-
id: string
|
|
58
|
-
name: string
|
|
59
|
-
avatarSrc?: string
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
avatarSrc?: string;
|
|
60
64
|
/** Deleted account (DELETED / SELF_DELETED) — renders the red user-x placeholder + red name. */
|
|
61
|
-
deleted?: boolean
|
|
62
|
-
}
|
|
63
|
-
options: TicketAssigneeOption[]
|
|
64
|
-
isLoading?: boolean
|
|
65
|
-
isPending?: boolean
|
|
66
|
-
onAssign: (userId: string | null) => void
|
|
67
|
-
}
|
|
65
|
+
deleted?: boolean;
|
|
66
|
+
};
|
|
67
|
+
options: TicketAssigneeOption[];
|
|
68
|
+
isLoading?: boolean;
|
|
69
|
+
isPending?: boolean;
|
|
70
|
+
onAssign: (userId: string | null) => void;
|
|
71
|
+
};
|
|
68
72
|
/** Created date string */
|
|
69
|
-
createdAt?: string
|
|
73
|
+
createdAt?: string;
|
|
70
74
|
/** Ticket description — markdown/HTML content */
|
|
71
|
-
description?: string
|
|
75
|
+
description?: string;
|
|
72
76
|
/** File attachments (view-only with download) */
|
|
73
|
-
attachments?: TicketAttachment[]
|
|
77
|
+
attachments?: TicketAttachment[];
|
|
74
78
|
/** Tag labels */
|
|
75
|
-
tags?: string[]
|
|
79
|
+
tags?: string[];
|
|
76
80
|
/** Notes */
|
|
77
|
-
notes?: TicketNote[]
|
|
78
|
-
onAddNote?: (text: string) => void
|
|
79
|
-
onEditNote?: (id: string, text: string) => void
|
|
80
|
-
onDeleteNote?: (id: string) => void
|
|
81
|
+
notes?: TicketNote[];
|
|
82
|
+
onAddNote?: (text: string) => void;
|
|
83
|
+
onEditNote?: (id: string, text: string) => void;
|
|
84
|
+
onDeleteNote?: (id: string) => void;
|
|
81
85
|
/** Disables the note input while a note is being added */
|
|
82
|
-
isAddingNote?: boolean
|
|
86
|
+
isAddingNote?: boolean;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
function InfoCell({
|
|
86
|
-
value
|
|
87
|
-
label
|
|
88
|
-
icon
|
|
89
|
-
onClick
|
|
89
|
+
function InfoCell({
|
|
90
|
+
value,
|
|
91
|
+
label,
|
|
92
|
+
icon,
|
|
93
|
+
onClick,
|
|
94
|
+
}: {
|
|
95
|
+
value: string;
|
|
96
|
+
label: string;
|
|
97
|
+
icon?: React.ReactNode;
|
|
98
|
+
onClick?: () => void;
|
|
90
99
|
}) {
|
|
91
100
|
return (
|
|
92
101
|
<div className="flex-1 min-w-0 overflow-hidden">
|
|
93
102
|
<div className="flex flex-col justify-center">
|
|
94
103
|
<div className="flex items-center gap-1 w-full min-w-0">
|
|
95
104
|
{icon && (
|
|
96
|
-
<span className="shrink-0 size-4 flex items-center justify-center text-ods-text-secondary">
|
|
97
|
-
{icon}
|
|
98
|
-
</span>
|
|
105
|
+
<span className="shrink-0 size-4 flex items-center justify-center text-ods-text-secondary">{icon}</span>
|
|
99
106
|
)}
|
|
100
107
|
{onClick ? (
|
|
101
108
|
<button
|
|
102
109
|
type="button"
|
|
103
110
|
onClick={onClick}
|
|
104
|
-
className="text-h4 text-ods-text-primary truncate hover:text-ods-accent transition-colors cursor-pointer text-left"
|
|
111
|
+
className="text-h4 text-ods-text-primary truncate hover:text-ods-accent transition-colors cursor-pointer text-left"
|
|
112
|
+
title={value}
|
|
113
|
+
>
|
|
105
114
|
{value}
|
|
106
115
|
</button>
|
|
107
116
|
) : (
|
|
108
|
-
<span className="text-h4 text-ods-text-primary truncate" title={value}>
|
|
117
|
+
<span className="text-h4 text-ods-text-primary truncate" title={value}>
|
|
118
|
+
{value}
|
|
119
|
+
</span>
|
|
109
120
|
)}
|
|
110
121
|
</div>
|
|
111
122
|
<span className="text-h6 text-ods-text-secondary truncate">{label}</span>
|
|
112
123
|
</div>
|
|
113
124
|
</div>
|
|
114
|
-
)
|
|
125
|
+
);
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
export function TicketInfoSection({
|
|
@@ -123,6 +134,8 @@ export function TicketInfoSection({
|
|
|
123
134
|
statusOptions,
|
|
124
135
|
onStatusSelect,
|
|
125
136
|
isStatusPending,
|
|
137
|
+
isStatusDisabled,
|
|
138
|
+
statusDisabledReason,
|
|
126
139
|
onExpand,
|
|
127
140
|
expanded = false,
|
|
128
141
|
className,
|
|
@@ -138,12 +151,7 @@ export function TicketInfoSection({
|
|
|
138
151
|
isAddingNote,
|
|
139
152
|
}: TicketInfoSectionProps) {
|
|
140
153
|
return (
|
|
141
|
-
<div
|
|
142
|
-
className={cn(
|
|
143
|
-
"rounded-[6px] border border-ods-border overflow-hidden",
|
|
144
|
-
className,
|
|
145
|
-
)}
|
|
146
|
-
>
|
|
154
|
+
<div className={cn('rounded-[6px] border border-ods-border overflow-hidden', className)}>
|
|
147
155
|
{/* Header row */}
|
|
148
156
|
<div className="grid grid-cols-2 lg:grid-cols-[1fr_1fr_1fr_auto] gap-4 px-4 py-3 bg-ods-card border-b border-ods-border items-center">
|
|
149
157
|
{/* Organization with image */}
|
|
@@ -151,11 +159,11 @@ export function TicketInfoSection({
|
|
|
151
159
|
<SquareAvatar
|
|
152
160
|
src={organization?.imageSrc}
|
|
153
161
|
alt={organization?.name}
|
|
154
|
-
fallback={organization?.name ||
|
|
162
|
+
fallback={organization?.name || 'Org'}
|
|
155
163
|
size="md"
|
|
156
164
|
className="shrink-0"
|
|
157
165
|
/>
|
|
158
|
-
<InfoCell value={organization?.name ||
|
|
166
|
+
<InfoCell value={organization?.name || 'Unassigned'} label="Organization" />
|
|
159
167
|
</div>
|
|
160
168
|
|
|
161
169
|
{/* Assigned */}
|
|
@@ -180,24 +188,21 @@ export function TicketInfoSection({
|
|
|
180
188
|
</div>
|
|
181
189
|
|
|
182
190
|
{/* Device */}
|
|
183
|
-
<InfoCell
|
|
184
|
-
value={device?.name || "Unassigned"}
|
|
185
|
-
label="Device"
|
|
186
|
-
icon={device?.icon}
|
|
187
|
-
onClick={device?.onClick}
|
|
188
|
-
/>
|
|
191
|
+
<InfoCell value={device?.name || 'Unassigned'} label="Device" icon={device?.icon} onClick={device?.onClick} />
|
|
189
192
|
|
|
190
193
|
{/* Status tag + expand button */}
|
|
191
194
|
<div className="flex items-center gap-4 min-w-0">
|
|
192
195
|
{(status || statusLabel) && (
|
|
193
196
|
<div className="min-w-0">
|
|
194
197
|
<TicketStatusTag
|
|
195
|
-
status={status ??
|
|
198
|
+
status={status ?? ''}
|
|
196
199
|
label={statusLabel}
|
|
197
200
|
color={statusColor}
|
|
198
201
|
options={statusOptions}
|
|
199
202
|
onSelect={onStatusSelect}
|
|
200
203
|
isPending={isStatusPending}
|
|
204
|
+
disabled={isStatusDisabled}
|
|
205
|
+
disabledReason={statusDisabledReason}
|
|
201
206
|
/>
|
|
202
207
|
</div>
|
|
203
208
|
)}
|
|
@@ -206,16 +211,16 @@ export function TicketInfoSection({
|
|
|
206
211
|
type="button"
|
|
207
212
|
onClick={onExpand}
|
|
208
213
|
className={cn(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
'shrink-0 flex items-center justify-center p-3 rounded-[6px]',
|
|
215
|
+
'bg-ods-card border border-ods-border',
|
|
216
|
+
'hover:bg-ods-bg-hover transition-colors duration-150',
|
|
212
217
|
)}
|
|
213
|
-
aria-label={expanded ?
|
|
218
|
+
aria-label={expanded ? 'Collapse' : 'Expand'}
|
|
214
219
|
>
|
|
215
220
|
<Chevron02DownIcon
|
|
216
221
|
className={cn(
|
|
217
|
-
|
|
218
|
-
expanded &&
|
|
222
|
+
'size-6 text-ods-text-primary transition-transform duration-200',
|
|
223
|
+
expanded && 'rotate-180',
|
|
219
224
|
)}
|
|
220
225
|
/>
|
|
221
226
|
</button>
|
|
@@ -236,9 +241,7 @@ export function TicketInfoSection({
|
|
|
236
241
|
{/* Content area */}
|
|
237
242
|
<div className="flex flex-col gap-4 p-4 bg-ods-bg border-b border-ods-border">
|
|
238
243
|
{/* Description */}
|
|
239
|
-
{description &&
|
|
240
|
-
<SimpleMarkdownRenderer content={description} />
|
|
241
|
-
)}
|
|
244
|
+
{description && <SimpleMarkdownRenderer content={description} />}
|
|
242
245
|
|
|
243
246
|
{/* Attachments */}
|
|
244
247
|
{attachments && attachments.length > 0 && (
|
|
@@ -274,5 +277,5 @@ export function TicketInfoSection({
|
|
|
274
277
|
</>
|
|
275
278
|
)}
|
|
276
279
|
</div>
|
|
277
|
-
)
|
|
280
|
+
);
|
|
278
281
|
}
|
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
'use client'
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
import { deriveActiveColor, deriveHoverColor, getReadableTextColor } from '../../utils/ods-color-utils';
|
|
6
|
+
import { CheckCircleIcon, Chevron02DownIcon } from '../icons-v2-generated';
|
|
7
|
+
import { ActionsMenuDropdown } from './actions-menu';
|
|
8
|
+
import { ColorSwatch } from './color-swatch';
|
|
9
|
+
import { Tag, type TagProps, tagVariants } from './tag';
|
|
10
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Canonical ticket status values.
|
|
13
14
|
* This is the single source of truth for ticket statuses across all frontends.
|
|
14
15
|
*/
|
|
15
|
-
export type TicketStatus = 'ACTIVE' | 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'ON_HOLD' | 'RESOLVED' | 'ARCHIVED'
|
|
16
|
+
export type TicketStatus = 'ACTIVE' | 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'ON_HOLD' | 'RESOLVED' | 'ARCHIVED';
|
|
16
17
|
|
|
17
|
-
type TagVariant = NonNullable<TagProps['variant']
|
|
18
|
+
type TagVariant = NonNullable<TagProps['variant']>;
|
|
18
19
|
|
|
19
20
|
interface TicketStatusConfig {
|
|
20
|
-
label: string
|
|
21
|
-
variant: TagVariant
|
|
22
|
-
icon?: React.ReactNode
|
|
21
|
+
label: string;
|
|
22
|
+
variant: TagVariant;
|
|
23
|
+
icon?: React.ReactNode;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const STATUS_CONFIG: Record<TicketStatus, TicketStatusConfig> = {
|
|
@@ -28,7 +29,7 @@ const STATUS_CONFIG: Record<TicketStatus, TicketStatusConfig> = {
|
|
|
28
29
|
variant: 'success',
|
|
29
30
|
},
|
|
30
31
|
AI_ASSISTANCE: {
|
|
31
|
-
label: 'AI
|
|
32
|
+
label: 'AI Handling',
|
|
32
33
|
variant: 'outline',
|
|
33
34
|
},
|
|
34
35
|
TECH_REQUIRED: {
|
|
@@ -48,7 +49,7 @@ const STATUS_CONFIG: Record<TicketStatus, TicketStatusConfig> = {
|
|
|
48
49
|
label: 'Archived',
|
|
49
50
|
variant: 'grey',
|
|
50
51
|
},
|
|
51
|
-
}
|
|
52
|
+
};
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Maps known aliases to canonical TicketStatus values.
|
|
@@ -81,14 +82,14 @@ const STATUS_ALIASES: Record<string, TicketStatus> = {
|
|
|
81
82
|
resolved: 'RESOLVED',
|
|
82
83
|
archived: 'ARCHIVED',
|
|
83
84
|
action_required: 'TECH_REQUIRED',
|
|
84
|
-
}
|
|
85
|
+
};
|
|
85
86
|
|
|
86
87
|
/**
|
|
87
88
|
* Resolves any status string to a canonical TicketStatus.
|
|
88
89
|
* Returns null for unknown values.
|
|
89
90
|
*/
|
|
90
91
|
export function resolveTicketStatus(status: string): TicketStatus | null {
|
|
91
|
-
return STATUS_ALIASES[status] ?? null
|
|
92
|
+
return STATUS_ALIASES[status] ?? null;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/**
|
|
@@ -96,11 +97,11 @@ export function resolveTicketStatus(status: string): TicketStatus | null {
|
|
|
96
97
|
* Accepts any known status format (backend, chat, dialog).
|
|
97
98
|
*/
|
|
98
99
|
export function getTicketStatusConfig(status: string): TicketStatusConfig {
|
|
99
|
-
const canonical = resolveTicketStatus(status)
|
|
100
|
+
const canonical = resolveTicketStatus(status);
|
|
100
101
|
if (!canonical) {
|
|
101
|
-
return { label: status.replace(/_/g, ' '), variant: 'outline' }
|
|
102
|
+
return { label: status.replace(/_/g, ' '), variant: 'outline' };
|
|
102
103
|
}
|
|
103
|
-
return STATUS_CONFIG[canonical]
|
|
104
|
+
return STATUS_CONFIG[canonical];
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/**
|
|
@@ -108,8 +109,8 @@ export function getTicketStatusConfig(status: string): TicketStatusConfig {
|
|
|
108
109
|
* Drop-in replacement for the getTicketStatusTag utility in openframe-frontend.
|
|
109
110
|
*/
|
|
110
111
|
export function getTicketStatusTag(status: string): { label: string; variant: TagVariant; icon?: React.ReactNode } {
|
|
111
|
-
const config = getTicketStatusConfig(status)
|
|
112
|
-
return { label: config.label, variant: config.variant, icon: config.icon }
|
|
112
|
+
const config = getTicketStatusConfig(status);
|
|
113
|
+
return { label: config.label, variant: config.variant, icon: config.icon };
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
// ---------------------------------------------------------------------------
|
|
@@ -117,19 +118,19 @@ export function getTicketStatusTag(status: string): { label: string; variant: Ta
|
|
|
117
118
|
// ---------------------------------------------------------------------------
|
|
118
119
|
|
|
119
120
|
/** Backend status-definition kinds. */
|
|
120
|
-
export type TicketStatusKind = 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'RESOLVED' | 'ARCHIVED' | 'CUSTOM'
|
|
121
|
+
export type TicketStatusKind = 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'RESOLVED' | 'ARCHIVED' | 'CUSTOM';
|
|
121
122
|
|
|
122
123
|
const KIND_TO_CANONICAL: Record<Exclude<TicketStatusKind, 'CUSTOM'>, TicketStatus> = {
|
|
123
124
|
AI_ASSISTANCE: 'AI_ASSISTANCE',
|
|
124
125
|
TECH_REQUIRED: 'TECH_REQUIRED',
|
|
125
126
|
RESOLVED: 'RESOLVED',
|
|
126
127
|
ARCHIVED: 'ARCHIVED',
|
|
127
|
-
}
|
|
128
|
+
};
|
|
128
129
|
|
|
129
130
|
/** Canonical TicketStatus for a backend kind, or undefined for CUSTOM/unknown. */
|
|
130
131
|
export function kindToCanonicalStatus(kind: string | null | undefined): TicketStatus | undefined {
|
|
131
|
-
if (kind == null || kind === 'CUSTOM') return undefined
|
|
132
|
-
return (KIND_TO_CANONICAL as Record<string, TicketStatus | undefined>)[kind]
|
|
132
|
+
if (kind == null || kind === 'CUSTOM') return undefined;
|
|
133
|
+
return (KIND_TO_CANONICAL as Record<string, TicketStatus | undefined>)[kind];
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/**
|
|
@@ -138,18 +139,18 @@ export function kindToCanonicalStatus(kind: string | null | undefined): TicketSt
|
|
|
138
139
|
* TECH_REQUIRED and custom statuses use their color.
|
|
139
140
|
*/
|
|
140
141
|
export function usesCanonicalStatusStyle(kind: string | null | undefined): boolean {
|
|
141
|
-
return kind != null && kind !== 'CUSTOM' && kind !== 'TECH_REQUIRED'
|
|
142
|
+
return kind != null && kind !== 'CUSTOM' && kind !== 'TECH_REQUIRED';
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
export interface TicketStatusInput {
|
|
145
146
|
/** Legacy enum status (may be empty/null under the lifecycle feature). */
|
|
146
|
-
status?: string | null
|
|
147
|
+
status?: string | null;
|
|
147
148
|
/** Backend status kind (AI_ASSISTANCE, TECH_REQUIRED, RESOLVED, ARCHIVED, CUSTOM). */
|
|
148
|
-
statusKind?: string | null
|
|
149
|
+
statusKind?: string | null;
|
|
149
150
|
/** Custom status display name. */
|
|
150
|
-
statusName?: string | null
|
|
151
|
+
statusName?: string | null;
|
|
151
152
|
/** Custom status hex color. */
|
|
152
|
-
statusColor?: string | null
|
|
153
|
+
statusColor?: string | null;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
/**
|
|
@@ -159,12 +160,12 @@ export interface TicketStatusInput {
|
|
|
159
160
|
* backend color, labelled by the custom status name.
|
|
160
161
|
*/
|
|
161
162
|
export function resolveStatusTagProps(input: TicketStatusInput): { status: string; label?: string; color?: string } {
|
|
162
|
-
const canonical = usesCanonicalStatusStyle(input.statusKind) ? kindToCanonicalStatus(input.statusKind) : undefined
|
|
163
|
+
const canonical = usesCanonicalStatusStyle(input.statusKind) ? kindToCanonicalStatus(input.statusKind) : undefined;
|
|
163
164
|
return {
|
|
164
165
|
status: canonical ?? input.status ?? '',
|
|
165
166
|
label: input.statusName ?? undefined,
|
|
166
167
|
color: canonical ? undefined : (input.statusColor ?? undefined),
|
|
167
|
-
}
|
|
168
|
+
};
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
// ---------------------------------------------------------------------------
|
|
@@ -173,36 +174,43 @@ export function resolveStatusTagProps(input: TicketStatusInput): { status: strin
|
|
|
173
174
|
|
|
174
175
|
/** A selectable status option for the inline status-change dropdown. */
|
|
175
176
|
export interface TicketStatusTagOption {
|
|
176
|
-
id: string
|
|
177
|
-
name: string
|
|
178
|
-
color: string
|
|
177
|
+
id: string;
|
|
178
|
+
name: string;
|
|
179
|
+
color: string;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
export interface TicketStatusTagProps {
|
|
182
183
|
/** Ticket status in any known format (ACTIVE, active, ACTION_REQUIRED, etc.) */
|
|
183
|
-
status: string
|
|
184
|
+
status: string;
|
|
184
185
|
/** Override the default label */
|
|
185
|
-
label?: string
|
|
186
|
+
label?: string;
|
|
186
187
|
/**
|
|
187
188
|
* Hex color override. Replaces the variant's fill; text
|
|
188
189
|
* color is computed for contrast. The status's preset icon is preserved.
|
|
189
190
|
*/
|
|
190
|
-
color?: string
|
|
191
|
+
color?: string;
|
|
191
192
|
/** Override the status's default Tag variant (e.g. force 'outline' or 'primary'). */
|
|
192
|
-
variant?: TagVariant
|
|
193
|
+
variant?: TagVariant;
|
|
193
194
|
/** Additional className for the Tag */
|
|
194
|
-
className?: string
|
|
195
|
+
className?: string;
|
|
195
196
|
/** Show the status-specific icon (e.g. checkmark for resolved) */
|
|
196
|
-
showIcon?: boolean
|
|
197
|
+
showIcon?: boolean;
|
|
197
198
|
/**
|
|
198
199
|
* Make the tag an inline status changer. When provided (with `onSelect`), the
|
|
199
200
|
* tag renders with a chevron and opens a dropdown of these options on click.
|
|
200
201
|
*/
|
|
201
|
-
options?: TicketStatusTagOption[]
|
|
202
|
+
options?: TicketStatusTagOption[];
|
|
202
203
|
/** Called with the chosen option id. Required for the inline dropdown. */
|
|
203
|
-
onSelect?: (id: string) => void
|
|
204
|
+
onSelect?: (id: string) => void;
|
|
204
205
|
/** Disables the dropdown while a change is in flight. */
|
|
205
|
-
isPending?: boolean
|
|
206
|
+
isPending?: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Lock the status: renders the plain non-interactive tag even when `options`
|
|
209
|
+
* are provided (e.g. a pending approval request blocks the transition).
|
|
210
|
+
*/
|
|
211
|
+
disabled?: boolean;
|
|
212
|
+
/** Reason shown in a tooltip over the tag when `disabled`. */
|
|
213
|
+
disabledReason?: string;
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
/**
|
|
@@ -220,8 +228,10 @@ export function TicketStatusTag({
|
|
|
220
228
|
options,
|
|
221
229
|
onSelect,
|
|
222
230
|
isPending = false,
|
|
231
|
+
disabled = false,
|
|
232
|
+
disabledReason,
|
|
223
233
|
}: TicketStatusTagProps) {
|
|
224
|
-
const config = getTicketStatusConfig(status)
|
|
234
|
+
const config = getTicketStatusConfig(status);
|
|
225
235
|
// Custom color: drive bg via CSS vars so the derived hover/active fills can win
|
|
226
236
|
// on :hover/:active (an inline backgroundColor would block class-based hover).
|
|
227
237
|
const customStyle = color
|
|
@@ -231,10 +241,10 @@ export function TicketStatusTag({
|
|
|
231
241
|
'--tag-bg-active': deriveActiveColor(color),
|
|
232
242
|
color: getReadableTextColor(color),
|
|
233
243
|
} as React.CSSProperties)
|
|
234
|
-
: undefined
|
|
244
|
+
: undefined;
|
|
235
245
|
const customColorClasses = color
|
|
236
246
|
? 'bg-[var(--tag-bg)] hover:bg-[var(--tag-bg-hover)] active:bg-[var(--tag-bg-active)]'
|
|
237
|
-
: undefined
|
|
247
|
+
: undefined;
|
|
238
248
|
|
|
239
249
|
const tag = (
|
|
240
250
|
<Tag
|
|
@@ -244,10 +254,22 @@ export function TicketStatusTag({
|
|
|
244
254
|
className={cn('shrink-0 w-fit', customColorClasses, className)}
|
|
245
255
|
style={customStyle}
|
|
246
256
|
/>
|
|
247
|
-
)
|
|
257
|
+
);
|
|
248
258
|
|
|
249
|
-
if (!options || options.length === 0 || !onSelect) {
|
|
250
|
-
|
|
259
|
+
if (disabled || !options || options.length === 0 || !onSelect) {
|
|
260
|
+
if (disabled && disabledReason) {
|
|
261
|
+
return (
|
|
262
|
+
<TooltipProvider delayDuration={0}>
|
|
263
|
+
<Tooltip>
|
|
264
|
+
<TooltipTrigger asChild>
|
|
265
|
+
<span className="w-fit shrink-0 cursor-not-allowed">{tag}</span>
|
|
266
|
+
</TooltipTrigger>
|
|
267
|
+
<TooltipContent>{disabledReason}</TooltipContent>
|
|
268
|
+
</Tooltip>
|
|
269
|
+
</TooltipProvider>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return tag;
|
|
251
273
|
}
|
|
252
274
|
|
|
253
275
|
return (
|
|
@@ -293,5 +315,5 @@ export function TicketStatusTag({
|
|
|
293
315
|
</button>
|
|
294
316
|
}
|
|
295
317
|
/>
|
|
296
|
-
)
|
|
318
|
+
);
|
|
297
319
|
}
|
|
@@ -16,7 +16,7 @@ Storybook stories for the `Board` component, covering all major interaction stat
|
|
|
16
16
|
| Story | Description |
|
|
17
17
|
|---|---|
|
|
18
18
|
| `Default` | Five canonical-status columns with full DnD wired up |
|
|
19
|
-
| `SystemAndCustom` | Mixed system + custom columns; AI
|
|
19
|
+
| `SystemAndCustom` | Mixed system + custom columns; AI Handling refuses drops, Resolved tickets are pinned |
|
|
20
20
|
| `CustomColumns` | Fully custom columns decoupled from `TicketStatus` (Backlog, In Review, Blocked, Done) |
|
|
21
21
|
| `Collapsible` | Column collapse/expand with `localStorage` persistence via `collapseStorageKey` |
|
|
22
22
|
| `Pagination` | One column loads 10 of 50 tickets; `onLoadMore` appends pages after a 600ms simulated delay |
|
|
@@ -112,11 +112,11 @@ export const Default: Story = {
|
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Mixed system + custom columns matching the OpenFrame Tickets layout:
|
|
115
|
-
* AI
|
|
115
|
+
* AI Handling and Tech Required are system columns (default surface, joined
|
|
116
116
|
* with no gap), middle columns are custom (tinted by color), Resolved is a
|
|
117
117
|
* system column whose `RESOLVED` status gives the green-checkmark tag.
|
|
118
118
|
*
|
|
119
|
-
* DnD restrictions: AI
|
|
119
|
+
* DnD restrictions: AI Handling refuses drops (tickets enter via the AI
|
|
120
120
|
* pipeline, not by dragging), but tickets in it can still be dragged out.
|
|
121
121
|
* Resolved tickets are pinned (`dragDisabled`).
|
|
122
122
|
*/
|
|
@@ -124,8 +124,8 @@ export const SystemAndCustom: Story = {
|
|
|
124
124
|
render: function Render() {
|
|
125
125
|
const [columns, setColumns] = React.useState<BoardColumnDef[]>([
|
|
126
126
|
{
|
|
127
|
-
id: 'ai-
|
|
128
|
-
label: 'AI
|
|
127
|
+
id: 'ai-handling',
|
|
128
|
+
label: 'AI Handling',
|
|
129
129
|
color: '#f357bb',
|
|
130
130
|
system: true,
|
|
131
131
|
dropDisabled: true,
|
|
@@ -356,7 +356,7 @@ export const ProgressSizeComparison: Story = {
|
|
|
356
356
|
export const WithTitleTag: Story = {
|
|
357
357
|
args: {
|
|
358
358
|
value: 75,
|
|
359
|
-
titleTag: <Tag variant="outline" label="AI
|
|
359
|
+
titleTag: <Tag variant="outline" label="AI Handling" />,
|
|
360
360
|
},
|
|
361
361
|
parameters: {
|
|
362
362
|
docs: {
|
|
@@ -35,7 +35,7 @@ const assigneeOptions = [
|
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
const statusOptions = [
|
|
38
|
-
{ id: 's1', name: 'AI
|
|
38
|
+
{ id: 's1', name: 'AI Handling', color: '#3a3a3a' },
|
|
39
39
|
{ id: 's2', name: 'Tech Required', color: '#f59e0b' },
|
|
40
40
|
{ id: 's3', name: 'Resolved', color: '#5ea62e' },
|
|
41
41
|
];
|
|
@@ -71,7 +71,7 @@ export const ReopenedWithReason: Story = {
|
|
|
71
71
|
},
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
/** Reopened into AI
|
|
74
|
+
/** Reopened into AI Handling (targetStatusKind decides) — Figma type=reopened-fae. */
|
|
75
75
|
export const ReopenedFaeContinues: Story = {
|
|
76
76
|
args: {
|
|
77
77
|
data: { kind: "REOPENED", actorId: "u-7", actorName: "John Smith", actorType: "CLIENT", targetStatusKind: "AI_ASSISTANCE" },
|