@@ -8,7 +8,6 @@ import { Plus } from 'lucide-react'
88import {
99 Checkbox ,
1010 Chip ,
11- ChipDropdown ,
1211 ChipInput ,
1312 ChipModal ,
1413 ChipModalBody ,
@@ -35,6 +34,9 @@ import { useWorkspacesQuery } from '@/hooks/queries/workspace'
3534
3635const logger = createLogger ( 'DataRetentionSettings' )
3736
37+ /** Sentinel ChipSelect value representing the all-workspaces scope (`workspaceId: null`). */
38+ const ALL_WORKSPACES = '__all__'
39+
3840const DAY_OPTIONS = [
3941 { value : '1' , label : '1 day' } ,
4042 { value : '3' , label : '3 days' } ,
@@ -54,8 +56,8 @@ interface RuleDraft {
5456 id : string
5557 name : string
5658 entityTypes : string [ ]
57- appliesToAllWorkspaces : boolean
58- workspaceIds : string [ ]
59+ /** null = all workspaces; otherwise the single targeted workspace. */
60+ workspaceId : string | null
5961}
6062
6163function hoursToDisplayDays ( hours : number | null ) : string {
@@ -72,8 +74,7 @@ function normalizeRule(rule: RuleDraft): string {
7274 return JSON . stringify ( {
7375 name : rule . name . trim ( ) ,
7476 entityTypes : [ ...rule . entityTypes ] . sort ( ) ,
75- appliesToAllWorkspaces : rule . appliesToAllWorkspaces ,
76- workspaceIds : rule . appliesToAllWorkspaces ? [ ] : [ ...rule . workspaceIds ] . sort ( ) ,
77+ workspaceId : rule . workspaceId ,
7778 } )
7879}
7980
@@ -82,12 +83,6 @@ function entitySummary(rule: RuleDraft): string {
8283 return `${ rule . entityTypes . length } entity type${ rule . entityTypes . length === 1 ? '' : 's' } `
8384}
8485
85- function workspaceSummary ( rule : RuleDraft ) : string {
86- if ( rule . appliesToAllWorkspaces ) return 'All workspaces'
87- const n = rule . workspaceIds . length
88- return `${ n } workspace${ n === 1 ? '' : 's' } `
89- }
90-
9186interface RetentionSelectProps {
9287 value : string
9388 onChange : ( value : string ) => void
@@ -189,7 +184,8 @@ interface RuleModalProps {
189184 draft : RuleDraft
190185 isNew : boolean
191186 isSaving : boolean
192- workspaceOptions : { value : string ; label : string } [ ]
187+ /** Selectable scopes for this rule (excludes scopes taken by other rules). */
188+ scopeOptions : { value : string ; label : string } [ ]
193189 onChange : ( draft : RuleDraft ) => void
194190 onClose : ( ) => void
195191 onSave : ( ) => void
@@ -199,7 +195,7 @@ function RuleModal({
199195 draft,
200196 isNew,
201197 isSaving,
202- workspaceOptions ,
198+ scopeOptions ,
203199 onChange,
204200 onClose,
205201 onSave,
@@ -218,22 +214,12 @@ function RuleModal({
218214 placeholder = 'e.g., Mask customer contact info'
219215 />
220216 < ChipModalField type = 'custom' title = 'Applies to' >
221- < ChipDropdown
222- multiple
223- searchable
224- value = { draft . workspaceIds }
225- onChange = { ( workspaceIds ) =>
226- onChange ( {
227- ...draft ,
228- workspaceIds,
229- appliesToAllWorkspaces : workspaceIds . length === 0 ,
230- } )
217+ < ChipSelect
218+ value = { draft . workspaceId ?? ALL_WORKSPACES }
219+ onChange = { ( value ) =>
220+ onChange ( { ...draft , workspaceId : value === ALL_WORKSPACES ? null : value } )
231221 }
232- options = { workspaceOptions }
233- allLabel = 'All workspaces'
234- placeholder = 'All workspaces'
235- searchPlaceholder = 'Search workspaces'
236- matchTriggerWidth = { false }
222+ options = { scopeOptions }
237223 align = 'start'
238224 />
239225 </ ChipModalField >
@@ -305,8 +291,7 @@ export function DataRetentionSettings() {
305291 id : r . id ,
306292 name : r . name ?? '' ,
307293 entityTypes : r . entityTypes ,
308- appliesToAllWorkspaces : r . appliesToAllWorkspaces ,
309- workspaceIds : r . workspaceIds ,
294+ workspaceId : r . workspaceId ,
310295 } ) )
311296 )
312297 formInitializedRef . current = true
@@ -318,6 +303,27 @@ export function DataRetentionSettings() {
318303 modalOriginal !== null &&
319304 normalizeRule ( modalDraft ) !== normalizeRule ( modalOriginal )
320305
306+ // Scope availability: at most one all-workspaces rule and one rule per workspace.
307+ const allScopeTaken = rules . some ( ( r ) => r . workspaceId === null )
308+ const takenWorkspaceIds = new Set (
309+ rules . flatMap ( ( r ) => ( r . workspaceId === null ? [ ] : [ r . workspaceId ] ) )
310+ )
311+ const freeWorkspaces = workspaceOptions . filter ( ( w ) => ! takenWorkspaceIds . has ( w . value ) )
312+ const hasAvailableScope = ! allScopeTaken || freeWorkspaces . length > 0
313+
314+ /** Scopes selectable for `draft` — excludes scopes taken by OTHER rules. */
315+ function scopeOptionsForDraft ( draft : RuleDraft ) : { value : string ; label : string } [ ] {
316+ const others = rules . filter ( ( r ) => r . id !== draft . id )
317+ const otherAll = others . some ( ( r ) => r . workspaceId === null )
318+ const otherWs = new Set ( others . flatMap ( ( r ) => ( r . workspaceId === null ? [ ] : [ r . workspaceId ] ) ) )
319+ const options : { value : string ; label : string } [ ] = [ ]
320+ if ( ! otherAll ) options . push ( { value : ALL_WORKSPACES , label : 'All workspaces' } )
321+ for ( const w of workspaceOptions ) {
322+ if ( ! otherWs . has ( w . value ) ) options . push ( w )
323+ }
324+ return options
325+ }
326+
321327 async function persistRules ( nextRules : RuleDraft [ ] ) {
322328 if ( ! orgId ) return
323329 await updateMutation . mutateAsync ( {
@@ -328,8 +334,7 @@ export function DataRetentionSettings() {
328334 id : r . id ,
329335 name : r . name . trim ( ) || undefined ,
330336 entityTypes : r . entityTypes ,
331- appliesToAllWorkspaces : r . appliesToAllWorkspaces ,
332- workspaceIds : r . appliesToAllWorkspaces ? [ ] : r . workspaceIds ,
337+ workspaceId : r . workspaceId ,
333338 } ) ) ,
334339 } ,
335340 } ,
@@ -342,8 +347,7 @@ export function DataRetentionSettings() {
342347 id : generateId ( ) ,
343348 name : '' ,
344349 entityTypes : [ ] ,
345- appliesToAllWorkspaces : true ,
346- workspaceIds : [ ] ,
350+ workspaceId : allScopeTaken ? ( freeWorkspaces [ 0 ] ?. value ?? null ) : null ,
347351 }
348352 setModalIsNew ( true )
349353 setModalOriginal ( blank )
@@ -506,10 +510,10 @@ export function DataRetentionSettings() {
506510 { rule . name . trim ( ) || 'Untitled rule' }
507511 </ span >
508512 < span className = 'truncate text-[var(--text-muted)] text-caption' >
509- { entitySummary ( rule ) } · { ' ' }
510- { rule . appliesToAllWorkspaces || rule . workspaceIds . length !== 1
511- ? workspaceSummary ( rule )
512- : workspaceName ( rule . workspaceIds [ 0 ] ) }
513+ { rule . workspaceId === null
514+ ? 'All workspaces'
515+ : workspaceName ( rule . workspaceId ) } { ' ' }
516+ · { entitySummary ( rule ) }
513517 </ span >
514518 </ div >
515519 < div className = 'flex flex-shrink-0 items-center gap-2' >
@@ -526,7 +530,7 @@ export function DataRetentionSettings() {
526530 </ div >
527531 ) }
528532 < div >
529- < Chip leftIcon = { Plus } onClick = { openAddRule } >
533+ < Chip leftIcon = { Plus } onClick = { openAddRule } disabled = { ! hasAvailableScope } >
530534 Add rule
531535 </ Chip >
532536 </ div >
@@ -539,7 +543,7 @@ export function DataRetentionSettings() {
539543 draft = { modalDraft }
540544 isNew = { modalIsNew }
541545 isSaving = { updateMutation . isPending }
542- workspaceOptions = { workspaceOptions }
546+ scopeOptions = { scopeOptionsForDraft ( modalDraft ) }
543547 onChange = { setModalDraft }
544548 onClose = { requestCloseModal }
545549 onSave = { saveModalRule }
0 commit comments