@flamingo-stack/openframe-frontend-core 0.0.442 → 0.0.443-snapshot.20260717175152
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-J5NO7PAG.cjs → chunk-2CJ734N2.cjs} +3 -3
- package/dist/{chunk-J5NO7PAG.cjs.map → chunk-2CJ734N2.cjs.map} +1 -1
- package/dist/{chunk-KUXGQ2FQ.cjs → chunk-2PLNDTRG.cjs} +32 -32
- package/dist/{chunk-KUXGQ2FQ.cjs.map → chunk-2PLNDTRG.cjs.map} +1 -1
- package/dist/{chunk-Y3NFB6HR.js → chunk-37ZZQXHK.js} +4 -4
- package/dist/{chunk-YZJUHEPZ.js → chunk-3SCNEGL2.js} +5 -5
- package/dist/{chunk-DUQQEQ7G.cjs → chunk-4R3SVOKH.cjs} +190 -156
- package/dist/chunk-4R3SVOKH.cjs.map +1 -0
- package/dist/{chunk-QO6MCQYE.cjs → chunk-7CZ4MJSI.cjs} +13 -10
- package/dist/chunk-7CZ4MJSI.cjs.map +1 -0
- package/dist/{chunk-W3JZXLJ2.js → chunk-ABIRMYG5.js} +6 -6
- package/dist/{chunk-TAQMYVO6.cjs → chunk-BYTJNRJW.cjs} +40 -40
- package/dist/{chunk-TAQMYVO6.cjs.map → chunk-BYTJNRJW.cjs.map} +1 -1
- package/dist/{chunk-IAZ5AN2E.js → chunk-CE3AITV2.js} +6 -6
- package/dist/{chunk-FPE3SQ5D.js → chunk-DCDB3SM4.js} +3 -3
- package/dist/{chunk-LFWPWIAC.cjs → chunk-DSW6FIEF.cjs} +7 -7
- package/dist/{chunk-LFWPWIAC.cjs.map → chunk-DSW6FIEF.cjs.map} +1 -1
- package/dist/{chunk-JYI4R25Q.js → chunk-DTMJWBDQ.js} +3 -3
- package/dist/{chunk-SM26C2L7.js → chunk-F2XHBAHT.js} +2 -2
- package/dist/{chunk-JNC4LVD6.cjs → chunk-F6NMMPDL.cjs} +29 -29
- package/dist/{chunk-JNC4LVD6.cjs.map → chunk-F6NMMPDL.cjs.map} +1 -1
- package/dist/{chunk-7QP65IE4.cjs → chunk-FO4CI4R5.cjs} +5 -5
- package/dist/{chunk-7QP65IE4.cjs.map → chunk-FO4CI4R5.cjs.map} +1 -1
- package/dist/{chunk-7KDR5P23.js → chunk-FQASXKBK.js} +2 -2
- package/dist/{chunk-7KNOHGMI.js → chunk-H7H3AFLF.js} +2 -2
- package/dist/{chunk-QAG357PE.js → chunk-LCJGOV6P.js} +2 -2
- package/dist/{chunk-2BTDPDME.js → chunk-MG4AMIFX.js} +4 -1
- package/dist/chunk-MG4AMIFX.js.map +1 -0
- package/dist/{chunk-ZQYFDYWV.js → chunk-NDWIFCLS.js} +120 -86
- package/dist/chunk-NDWIFCLS.js.map +1 -0
- package/dist/{chunk-UFBUQO7Z.cjs → chunk-OXZLBIR7.cjs} +18 -18
- package/dist/{chunk-UFBUQO7Z.cjs.map → chunk-OXZLBIR7.cjs.map} +1 -1
- package/dist/{chunk-63TSTDME.js → chunk-PMTR5W5G.js} +5 -5
- package/dist/{chunk-XMCVUYKC.cjs → chunk-Q6XOEVPH.cjs} +12 -12
- package/dist/{chunk-XMCVUYKC.cjs.map → chunk-Q6XOEVPH.cjs.map} +1 -1
- package/dist/{chunk-IZ6A6K6N.cjs → chunk-RALJEQWM.cjs} +15 -15
- package/dist/{chunk-IZ6A6K6N.cjs.map → chunk-RALJEQWM.cjs.map} +1 -1
- package/dist/{chunk-AGV26PUR.js → chunk-U7U2CZBZ.js} +3 -3
- package/dist/{chunk-LUKRPHEW.cjs → chunk-VI2QQIGR.cjs} +73 -73
- package/dist/{chunk-LUKRPHEW.cjs.map → chunk-VI2QQIGR.cjs.map} +1 -1
- package/dist/{chunk-JOH5D76X.cjs → chunk-WXU6A6CA.cjs} +73 -73
- package/dist/{chunk-JOH5D76X.cjs.map → chunk-WXU6A6CA.cjs.map} +1 -1
- package/dist/{chunk-IPEIOR77.cjs → chunk-YP7VJPCJ.cjs} +40 -40
- package/dist/{chunk-IPEIOR77.cjs.map → chunk-YP7VJPCJ.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/features/policy-configuration-panel.d.ts +11 -0
- package/dist/components/features/policy-configuration-panel.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 +135 -135
- package/dist/components/index.js +11 -11
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/checkbox-block.d.ts +6 -0
- package/dist/components/ui/checkbox-block.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +2 -2
- package/dist/types/permissions.d.ts +9 -6
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.d.ts +19 -3
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs +3 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/features/policy-configuration-panel.tsx +182 -122
- package/src/components/ui/checkbox-block.tsx +12 -1
- package/src/stories/CheckboxBlock.stories.tsx +18 -0
- package/src/stories/PolicyConfigurationPanel.stories.tsx +254 -0
- package/src/types/permissions.ts +10 -7
- package/src/utils/__tests__/embed-authed-fetch.test.ts +39 -0
- package/src/utils/embed-authed-fetch.ts +27 -3
- package/dist/chunk-2BTDPDME.js.map +0 -1
- package/dist/chunk-DUQQEQ7G.cjs.map +0 -1
- package/dist/chunk-QO6MCQYE.cjs.map +0 -1
- package/dist/chunk-ZQYFDYWV.js.map +0 -1
- /package/dist/{chunk-Y3NFB6HR.js.map → chunk-37ZZQXHK.js.map} +0 -0
- /package/dist/{chunk-YZJUHEPZ.js.map → chunk-3SCNEGL2.js.map} +0 -0
- /package/dist/{chunk-W3JZXLJ2.js.map → chunk-ABIRMYG5.js.map} +0 -0
- /package/dist/{chunk-IAZ5AN2E.js.map → chunk-CE3AITV2.js.map} +0 -0
- /package/dist/{chunk-FPE3SQ5D.js.map → chunk-DCDB3SM4.js.map} +0 -0
- /package/dist/{chunk-JYI4R25Q.js.map → chunk-DTMJWBDQ.js.map} +0 -0
- /package/dist/{chunk-SM26C2L7.js.map → chunk-F2XHBAHT.js.map} +0 -0
- /package/dist/{chunk-7KDR5P23.js.map → chunk-FQASXKBK.js.map} +0 -0
- /package/dist/{chunk-7KNOHGMI.js.map → chunk-H7H3AFLF.js.map} +0 -0
- /package/dist/{chunk-QAG357PE.js.map → chunk-LCJGOV6P.js.map} +0 -0
- /package/dist/{chunk-63TSTDME.js.map → chunk-PMTR5W5G.js.map} +0 -0
- /package/dist/{chunk-AGV26PUR.js.map → chunk-U7U2CZBZ.js.map} +0 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { useRef, useEffect, useState } from 'react'
|
|
5
|
-
import { ChevronDown } from 'lucide-react'
|
|
5
|
+
import { ChevronDown, Shield } from 'lucide-react'
|
|
6
6
|
import { cn } from '../../utils/cn'
|
|
7
7
|
import { ToolIcon } from '../tool-icon'
|
|
8
8
|
import { Button } from '../ui/button'
|
|
@@ -34,62 +34,107 @@ const getApprovalLevelLabel = (level: ApprovalLevel | undefined, editMode: boole
|
|
|
34
34
|
return option?.label || level
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const ApprovalLevelDropdown: React.FC<{
|
|
38
|
+
value: ApprovalLevel | undefined;
|
|
39
|
+
onChange: (level: ApprovalLevel | undefined) => void;
|
|
40
|
+
/** Adds the "Clear Global Permission" item (category bulk dropdown). */
|
|
41
|
+
allowClear?: boolean;
|
|
42
|
+
triggerClassName?: string;
|
|
43
|
+
contentClassName?: string;
|
|
44
|
+
}> = ({ value, onChange, allowClear = false, triggerClassName, contentClassName }) => (
|
|
45
|
+
<DropdownMenu>
|
|
46
|
+
<DropdownMenuTrigger asChild>
|
|
47
|
+
<Button
|
|
48
|
+
variant="outline"
|
|
49
|
+
rightIcon={<ChevronDown className="h-6 w-6 text-ods-text-secondary" />}
|
|
50
|
+
className={cn(
|
|
51
|
+
"!text-h6 bg-ods-card border border-ods-border rounded-md flex gap-[var(--spacing-system-xsf)] items-center justify-between !px-[var(--spacing-system-xsf)] py-[var(--spacing-system-xsf)] hover:bg-ods-bg-hover transition-colors h-auto",
|
|
52
|
+
value ? "text-ods-text-primary" : "text-ods-text-secondary",
|
|
53
|
+
triggerClassName
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{getApprovalLevelLabel(value, true)}
|
|
57
|
+
</Button>
|
|
58
|
+
</DropdownMenuTrigger>
|
|
59
|
+
<DropdownMenuContent align="end" className={contentClassName}>
|
|
60
|
+
{allowClear && (
|
|
61
|
+
<DropdownMenuItem onClick={() => onChange(undefined)} className="text-h6">
|
|
62
|
+
Clear Global Permission
|
|
63
|
+
</DropdownMenuItem>
|
|
64
|
+
)}
|
|
65
|
+
{approvalLevelOptions.map(option => (
|
|
66
|
+
<DropdownMenuItem
|
|
67
|
+
key={option.value}
|
|
68
|
+
onClick={() => onChange(option.value)}
|
|
69
|
+
className="text-h6"
|
|
70
|
+
>
|
|
71
|
+
{option.label}
|
|
72
|
+
</DropdownMenuItem>
|
|
73
|
+
))}
|
|
74
|
+
</DropdownMenuContent>
|
|
75
|
+
</DropdownMenu>
|
|
76
|
+
)
|
|
77
|
+
|
|
37
78
|
const PolicyRow: React.FC<{
|
|
38
79
|
policy: PermissionPolicy;
|
|
39
80
|
categoryId: string;
|
|
40
81
|
editMode: boolean;
|
|
41
82
|
onPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
|
|
42
83
|
}> = ({ policy, categoryId, editMode, onPermissionChange }) => {
|
|
84
|
+
const handleChange = (level: ApprovalLevel | undefined) => {
|
|
85
|
+
if (level) onPermissionChange(categoryId, policy.id, level)
|
|
86
|
+
}
|
|
87
|
+
|
|
43
88
|
return (
|
|
44
|
-
<div className="bg-ods-bg border-b border-ods-border flex gap-
|
|
89
|
+
<div className="bg-ods-bg border-b last:border-b-0 border-ods-border flex gap-[var(--spacing-system-m)] items-start md:items-center px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)]">
|
|
45
90
|
{/* Tool Icon */}
|
|
46
|
-
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
|
|
91
|
+
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0">
|
|
47
92
|
<ToolIcon toolType={policy.toolName} size={16} />
|
|
48
93
|
</div>
|
|
49
94
|
|
|
50
|
-
{/* Policy Info */}
|
|
95
|
+
{/* Policy Info — on mobile the approval level stacks below the pattern */}
|
|
51
96
|
<div className="flex-1 flex flex-col min-w-0">
|
|
52
|
-
<p className="text-
|
|
97
|
+
<p className="text-h4 text-ods-text-primary truncate" title={policy.name}>
|
|
53
98
|
{policy.name}
|
|
54
99
|
</p>
|
|
55
|
-
<p className="text-
|
|
100
|
+
<p className="text-h6 text-ods-text-secondary break-all">
|
|
56
101
|
{policy.commandPattern}
|
|
57
102
|
</p>
|
|
103
|
+
{editMode ? (
|
|
104
|
+
<div className="md:hidden mt-[var(--spacing-system-xsf)]">
|
|
105
|
+
<ApprovalLevelDropdown
|
|
106
|
+
value={policy.approvalLevel}
|
|
107
|
+
onChange={handleChange}
|
|
108
|
+
triggerClassName="w-full"
|
|
109
|
+
contentClassName="w-[180px]"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
) : policy.approvalLevel ? (
|
|
113
|
+
<span className="md:hidden text-h4 text-ods-text-primary">
|
|
114
|
+
{getApprovalLevelLabel(policy.approvalLevel)}
|
|
115
|
+
</span>
|
|
116
|
+
) : null}
|
|
58
117
|
</div>
|
|
59
118
|
|
|
60
|
-
{/* Approval Level
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{option.label}
|
|
80
|
-
</DropdownMenuItem>
|
|
81
|
-
))}
|
|
82
|
-
</DropdownMenuContent>
|
|
83
|
-
</DropdownMenu>
|
|
84
|
-
) : policy.approvalLevel ? (
|
|
85
|
-
<div className="px-3 py-2 w-[180px]">
|
|
86
|
-
<span className="text-h6 text-ods-text-primary">
|
|
87
|
-
{getApprovalLevelLabel(policy.approvalLevel, editMode)}
|
|
88
|
-
</span>
|
|
89
|
-
</div>
|
|
90
|
-
) : (
|
|
91
|
-
<div className="px-3 py-2 w-[180px]" />
|
|
92
|
-
)}
|
|
119
|
+
{/* Approval Level column (desktop only) */}
|
|
120
|
+
<div className="hidden md:block shrink-0">
|
|
121
|
+
{editMode ? (
|
|
122
|
+
<ApprovalLevelDropdown
|
|
123
|
+
value={policy.approvalLevel}
|
|
124
|
+
onChange={handleChange}
|
|
125
|
+
triggerClassName="w-[180px]"
|
|
126
|
+
contentClassName="w-[180px]"
|
|
127
|
+
/>
|
|
128
|
+
) : (
|
|
129
|
+
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] w-[180px]">
|
|
130
|
+
{policy.approvalLevel && (
|
|
131
|
+
<span className="text-h4 text-ods-text-primary">
|
|
132
|
+
{getApprovalLevelLabel(policy.approvalLevel)}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
93
138
|
</div>
|
|
94
139
|
)
|
|
95
140
|
}
|
|
@@ -97,7 +142,7 @@ const PolicyRow: React.FC<{
|
|
|
97
142
|
const useAnimatedHeight = (isExpanded: boolean) => {
|
|
98
143
|
const [height, setHeight] = useState<number | 'auto'>(0);
|
|
99
144
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
100
|
-
|
|
145
|
+
|
|
101
146
|
useEffect(() => {
|
|
102
147
|
if (contentRef.current) {
|
|
103
148
|
const contentHeight = contentRef.current.scrollHeight;
|
|
@@ -111,93 +156,73 @@ const useAnimatedHeight = (isExpanded: boolean) => {
|
|
|
111
156
|
const CategorySection: React.FC<{
|
|
112
157
|
category: PermissionCategory;
|
|
113
158
|
editMode: boolean;
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
isExpanded: boolean;
|
|
160
|
+
/** Last bulk level applied to this category (display-only, edit mode). */
|
|
161
|
+
bulkLevel: ApprovalLevel | undefined;
|
|
162
|
+
onToggle: (categoryId: string) => void;
|
|
163
|
+
onBulkLevelChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
|
|
116
164
|
onPolicyPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
|
|
117
|
-
}> = ({
|
|
118
|
-
category,
|
|
119
|
-
editMode,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
165
|
+
}> = ({
|
|
166
|
+
category,
|
|
167
|
+
editMode,
|
|
168
|
+
isExpanded,
|
|
169
|
+
bulkLevel,
|
|
170
|
+
onToggle,
|
|
171
|
+
onBulkLevelChange,
|
|
172
|
+
onPolicyPermissionChange
|
|
123
173
|
}) => {
|
|
124
|
-
const { contentRef, height } = useAnimatedHeight(
|
|
125
|
-
|
|
174
|
+
const { contentRef, height } = useAnimatedHeight(isExpanded);
|
|
175
|
+
const handleBulkChange = (level: ApprovalLevel | undefined) => onBulkLevelChange(category.id, level)
|
|
176
|
+
|
|
126
177
|
return (
|
|
127
178
|
<>
|
|
128
179
|
{/* Category Header */}
|
|
129
|
-
<div
|
|
130
|
-
className="bg-ods-card border-t border-ods-border flex gap-
|
|
131
|
-
onClick={() =>
|
|
180
|
+
<div
|
|
181
|
+
className="bg-ods-card border-t first:border-t-0 border-ods-border flex gap-[var(--spacing-system-m)] items-center pl-[var(--spacing-system-m)] pr-[var(--spacing-system-xsf)] py-[var(--spacing-system-s)] cursor-pointer hover:bg-ods-bg-hover transition-colors"
|
|
182
|
+
onClick={() => onToggle(category.id)}
|
|
132
183
|
>
|
|
133
184
|
{/* Category Icon */}
|
|
134
|
-
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
|
|
185
|
+
<div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0">
|
|
135
186
|
<div className="text-ods-text-secondary">
|
|
136
|
-
{category.icon}
|
|
187
|
+
{category.icon ?? <Shield className="w-4 h-4" />}
|
|
137
188
|
</div>
|
|
138
189
|
</div>
|
|
139
190
|
|
|
140
|
-
{/* Category Info */}
|
|
141
|
-
<div className="flex-1 flex flex-col">
|
|
142
|
-
<p className="
|
|
191
|
+
{/* Category Info — on mobile the bulk dropdown stacks below the count */}
|
|
192
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
193
|
+
<p className="text-h4 text-ods-text-primary truncate" title={category.name}>
|
|
143
194
|
{category.name}
|
|
144
195
|
</p>
|
|
145
196
|
<p className="text-h6 text-ods-text-secondary">
|
|
146
|
-
{category.
|
|
197
|
+
{category.policies.length} Configurations
|
|
147
198
|
</p>
|
|
199
|
+
{editMode && (
|
|
200
|
+
<div className="md:hidden mt-[var(--spacing-system-xsf)]" onClick={(e) => e.stopPropagation()}>
|
|
201
|
+
<ApprovalLevelDropdown
|
|
202
|
+
value={bulkLevel}
|
|
203
|
+
onChange={handleBulkChange}
|
|
204
|
+
allowClear
|
|
205
|
+
triggerClassName="w-full"
|
|
206
|
+
contentClassName="w-[256px]"
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
148
210
|
</div>
|
|
149
211
|
|
|
150
|
-
{/* Global Permission
|
|
212
|
+
{/* Global Permission column (desktop only) */}
|
|
151
213
|
{editMode ? (
|
|
152
|
-
<div onClick={(e) => e.stopPropagation()}>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<span className={cn(
|
|
161
|
-
"md:!text-sm font-medium",
|
|
162
|
-
category.globalPermission
|
|
163
|
-
? "text-ods-text-primary"
|
|
164
|
-
: "text-ods-text-secondary"
|
|
165
|
-
)}>
|
|
166
|
-
{getApprovalLevelLabel(category.globalPermission, editMode)}
|
|
167
|
-
</span>
|
|
168
|
-
</Button>
|
|
169
|
-
</DropdownMenuTrigger>
|
|
170
|
-
<DropdownMenuContent align="end" className="w-[256px]">
|
|
171
|
-
<DropdownMenuItem
|
|
172
|
-
onClick={() => onGlobalPermissionChange(category.id, undefined)}
|
|
173
|
-
className="md:!text-sm"
|
|
174
|
-
>
|
|
175
|
-
Clear Global Permission
|
|
176
|
-
</DropdownMenuItem>
|
|
177
|
-
{approvalLevelOptions.map(option => (
|
|
178
|
-
<DropdownMenuItem
|
|
179
|
-
key={option.value}
|
|
180
|
-
onClick={() => onGlobalPermissionChange(category.id, option.value)}
|
|
181
|
-
className="md:!text-sm"
|
|
182
|
-
>
|
|
183
|
-
{option.label}
|
|
184
|
-
</DropdownMenuItem>
|
|
185
|
-
))}
|
|
186
|
-
</DropdownMenuContent>
|
|
187
|
-
</DropdownMenu>
|
|
188
|
-
</div>
|
|
189
|
-
) : category.globalPermission ? (
|
|
190
|
-
<div
|
|
191
|
-
className="px-3 py-2 w-[256px]"
|
|
192
|
-
onClick={(e) => e.stopPropagation()}
|
|
193
|
-
>
|
|
194
|
-
<span className="md:!text-sm font-medium text-ods-text-primary">
|
|
195
|
-
{getApprovalLevelLabel(category.globalPermission, editMode)}
|
|
196
|
-
</span>
|
|
214
|
+
<div className="hidden md:block shrink-0" onClick={(e) => e.stopPropagation()}>
|
|
215
|
+
<ApprovalLevelDropdown
|
|
216
|
+
value={bulkLevel}
|
|
217
|
+
onChange={handleBulkChange}
|
|
218
|
+
allowClear
|
|
219
|
+
triggerClassName="w-[256px]"
|
|
220
|
+
contentClassName="w-[256px]"
|
|
221
|
+
/>
|
|
197
222
|
</div>
|
|
198
223
|
) : (
|
|
199
|
-
<div
|
|
200
|
-
className="px-
|
|
224
|
+
<div
|
|
225
|
+
className="hidden md:block px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] w-[256px] shrink-0"
|
|
201
226
|
onClick={(e) => e.stopPropagation()}
|
|
202
227
|
/>
|
|
203
228
|
)}
|
|
@@ -208,32 +233,32 @@ const CategorySection: React.FC<{
|
|
|
208
233
|
variant="transparent"
|
|
209
234
|
onClick={(e) => {
|
|
210
235
|
e.stopPropagation();
|
|
211
|
-
|
|
236
|
+
onToggle(category.id);
|
|
212
237
|
}}
|
|
213
|
-
leftIcon={<ChevronDown
|
|
238
|
+
leftIcon={<ChevronDown
|
|
214
239
|
className={cn(
|
|
215
240
|
"h-6 w-6 text-ods-text-primary transition-transform duration-300",
|
|
216
|
-
|
|
217
|
-
)}
|
|
241
|
+
isExpanded && "rotate-180"
|
|
242
|
+
)}
|
|
218
243
|
/>}
|
|
219
|
-
className="rounded-md h-auto w-auto"
|
|
220
|
-
aria-label={
|
|
244
|
+
className="rounded-md h-auto w-auto shrink-0"
|
|
245
|
+
aria-label={isExpanded ? "Collapse" : "Expand"}
|
|
221
246
|
/>
|
|
222
247
|
</div>
|
|
223
248
|
|
|
224
249
|
{/* Policies List */}
|
|
225
|
-
<div
|
|
250
|
+
<div
|
|
226
251
|
ref={contentRef}
|
|
227
|
-
style={{
|
|
252
|
+
style={{
|
|
228
253
|
height: height === 'auto' ? 'auto' : `${height}px`,
|
|
229
254
|
transition: 'height 300ms ease-in-out, opacity 300ms ease-in-out'
|
|
230
255
|
}}
|
|
231
256
|
className={cn(
|
|
232
257
|
"overflow-hidden",
|
|
233
|
-
|
|
258
|
+
isExpanded ? "opacity-100" : "opacity-0"
|
|
234
259
|
)}
|
|
235
260
|
>
|
|
236
|
-
<div className="px-
|
|
261
|
+
<div className="px-[var(--spacing-system-mf)] pb-[var(--spacing-system-mf)] pt-0">
|
|
237
262
|
<div className="border border-ods-border rounded-md overflow-hidden">
|
|
238
263
|
{category.policies.map((policy) => (
|
|
239
264
|
<PolicyRow
|
|
@@ -251,14 +276,47 @@ const CategorySection: React.FC<{
|
|
|
251
276
|
)
|
|
252
277
|
}
|
|
253
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Grouped list of permission categories with per-policy approval dropdowns.
|
|
281
|
+
* Presentation state — which categories are expanded and the last bulk level
|
|
282
|
+
* picked per category — lives inside the panel, so callers pass pure data and
|
|
283
|
+
* background data refreshes never collapse the user's view. Bulk selections
|
|
284
|
+
* reset when `editMode` changes.
|
|
285
|
+
*
|
|
286
|
+
* Responsive: below `md` the approval level / dropdowns stack inside the text
|
|
287
|
+
* column (per the ODS mobile design); at `md+` they render as fixed-width
|
|
288
|
+
* right-hand columns.
|
|
289
|
+
*/
|
|
254
290
|
export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> = ({
|
|
255
291
|
categories,
|
|
256
292
|
editMode,
|
|
257
|
-
onCategoryToggle,
|
|
258
|
-
onGlobalPermissionChange,
|
|
259
293
|
onPolicyPermissionChange,
|
|
294
|
+
onCategoryPermissionChange,
|
|
260
295
|
className
|
|
261
296
|
}) => {
|
|
297
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set())
|
|
298
|
+
const [bulkLevels, setBulkLevels] = useState<Record<string, ApprovalLevel | undefined>>({})
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
setBulkLevels({})
|
|
302
|
+
}, [editMode])
|
|
303
|
+
|
|
304
|
+
const handleToggle = (categoryId: string) => {
|
|
305
|
+
setExpandedIds(prev => {
|
|
306
|
+
const next = new Set(prev)
|
|
307
|
+
if (next.has(categoryId)) next.delete(categoryId)
|
|
308
|
+
else next.add(categoryId)
|
|
309
|
+
return next
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const handleBulkLevelChange = (categoryId: string, level: ApprovalLevel | undefined) => {
|
|
314
|
+
setBulkLevels(prev => ({ ...prev, [categoryId]: level }))
|
|
315
|
+
if (level) {
|
|
316
|
+
onCategoryPermissionChange(categoryId, level)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
262
320
|
return (
|
|
263
321
|
<div className={cn(
|
|
264
322
|
"bg-ods-card border border-ods-border rounded-md overflow-hidden",
|
|
@@ -269,8 +327,10 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
|
|
|
269
327
|
key={category.id}
|
|
270
328
|
category={category}
|
|
271
329
|
editMode={editMode}
|
|
272
|
-
|
|
273
|
-
|
|
330
|
+
isExpanded={expandedIds.has(category.id)}
|
|
331
|
+
bulkLevel={bulkLevels[category.id]}
|
|
332
|
+
onToggle={handleToggle}
|
|
333
|
+
onBulkLevelChange={handleBulkLevelChange}
|
|
274
334
|
onPolicyPermissionChange={onPolicyPermissionChange}
|
|
275
335
|
/>
|
|
276
336
|
))}
|
|
@@ -278,4 +338,4 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
|
|
|
278
338
|
)
|
|
279
339
|
}
|
|
280
340
|
|
|
281
|
-
PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
|
|
341
|
+
PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
|
|
@@ -17,6 +17,12 @@ interface CheckboxBlockProps {
|
|
|
17
17
|
description?: React.ReactNode
|
|
18
18
|
/** Keep the label on one line and ellipsize on overflow instead of wrapping */
|
|
19
19
|
truncateLabel?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* Optional trailing slot rendered at the end of the row (e.g. an action
|
|
22
|
+
* button). Interactive content must call `preventDefault` on click to stop
|
|
23
|
+
* the wrapping label from toggling the checkbox.
|
|
24
|
+
*/
|
|
25
|
+
trailing?: React.ReactNode
|
|
20
26
|
disabled?: boolean
|
|
21
27
|
/** Error message displayed below the block (also triggers red border) */
|
|
22
28
|
error?: string
|
|
@@ -26,12 +32,14 @@ interface CheckboxBlockProps {
|
|
|
26
32
|
const CheckboxBlock = React.forwardRef<
|
|
27
33
|
React.ComponentRef<typeof CheckboxPrimitive.Root>,
|
|
28
34
|
CheckboxBlockProps
|
|
29
|
-
>(({ id, checked, defaultChecked, onCheckedChange, label, description, truncateLabel, disabled, error, className }, ref) => (
|
|
35
|
+
>(({ id, checked, defaultChecked, onCheckedChange, label, description, truncateLabel, trailing, disabled, error, className }, ref) => (
|
|
30
36
|
<div className={cn("relative flex w-full flex-col", className)}>
|
|
31
37
|
<label
|
|
32
38
|
htmlFor={id}
|
|
33
39
|
className={cn(
|
|
34
40
|
"flex items-center gap-[var(--spacing-system-s)] rounded-md ring-1 ring-inset w-full",
|
|
41
|
+
// Trailing content stacks full-width below the text on mobile.
|
|
42
|
+
trailing && "flex-wrap md:flex-nowrap",
|
|
35
43
|
"p-[var(--spacing-system-sf)]",
|
|
36
44
|
!description && "min-h-[44px] md:min-h-[48px]",
|
|
37
45
|
description && "min-h-[60px] md:min-h-[64px]",
|
|
@@ -82,6 +90,9 @@ const CheckboxBlock = React.forwardRef<
|
|
|
82
90
|
</span>
|
|
83
91
|
)}
|
|
84
92
|
</div>
|
|
93
|
+
{trailing && (
|
|
94
|
+
<div className="flex w-full shrink-0 items-center md:ml-auto md:w-auto">{trailing}</div>
|
|
95
|
+
)}
|
|
85
96
|
</label>
|
|
86
97
|
{error && (
|
|
87
98
|
<p className="absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate text-ods-error" title={error}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
+
import { Button } from '../components/ui/button';
|
|
3
4
|
import { CheckboxBlock } from '../components/ui/checkbox-block';
|
|
4
5
|
|
|
5
6
|
const meta = {
|
|
@@ -142,6 +143,23 @@ export const WithDescription: Story = {
|
|
|
142
143
|
},
|
|
143
144
|
};
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Checkbox block with a trailing action. Interactive trailing content must
|
|
148
|
+
* call `preventDefault` on click so the wrapping label doesn't toggle the
|
|
149
|
+
* checkbox (see the OpenFrame customer guardrails tab for a real usage).
|
|
150
|
+
*/
|
|
151
|
+
export const WithTrailingAction: Story = {
|
|
152
|
+
args: {
|
|
153
|
+
label: 'Use the default guardrails settings',
|
|
154
|
+
description: 'This customer follows guardrails defaults.',
|
|
155
|
+
trailing: (
|
|
156
|
+
<Button variant="outline" onClick={e => e.preventDefault()}>
|
|
157
|
+
Edit Default Guardrails
|
|
158
|
+
</Button>
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
145
163
|
/**
|
|
146
164
|
* Checkbox block with description, checked.
|
|
147
165
|
*/
|