chore(studio): delete orphaned first-iteration scoped token form

Scoped/Form/Permissions/, Scoped/Form/ResourceAccess/, and
AccessToken.schemas.ts (+ test) belonged to the superseded scoped-form
design and had no live importers; the schemas file also still encoded the
removed non-expiring token semantics. PERMISSION_LIST /
ACCESS_TOKEN_RESOURCES stay: AccessTokenNewBanner still consumes them via
useGroupedPermissions.
This commit is contained in:
Wen Bo Xie committed 2026-08-05 14:03:19 +07:00
1 parent 7a610d224e
commit ce46509772
11 files changed
-942

No files matched your search

@@ -1,193 +0,0 @@
import { describe, expect, it } from 'vitest'
import { PermissionRowSchema, TokenSchema } from './AccessToken.schemas'
const validTokenData = {
tokenName: 'My Token',
expiresAt: 'day',
resourceAccess: 'all-orgs' as const,
permissionRows: [{ resource: 'organization:billing', actions: ['read'] }],
}
// --- PermissionRowSchema ---
describe('PermissionRowSchema', () => {
it('should pass for a valid permission row', () => {
const result = PermissionRowSchema.safeParse({
resource: 'organization:billing',
actions: ['read'],
})
expect(result.success).toBe(true)
})
it('should fail when resource is empty', () => {
const result = PermissionRowSchema.safeParse({
resource: '',
actions: ['read'],
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0].message).toBe('Please select a resource')
}
})
it('should fail when actions is empty', () => {
const result = PermissionRowSchema.safeParse({
resource: 'organization:billing',
actions: [],
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0].message).toBe('Please select at least one action')
}
})
it('should fail when resource is missing', () => {
const result = PermissionRowSchema.safeParse({ actions: ['read'] })
expect(result.success).toBe(false)
})
it('should fail when actions is missing', () => {
const result = PermissionRowSchema.safeParse({ resource: 'organization:billing' })
expect(result.success).toBe(false)
})
})
// --- TokenSchema ---
describe('TokenSchema', () => {
it('should pass for valid token data', () => {
const result = TokenSchema.safeParse(validTokenData)
expect(result.success).toBe(true)
})
it('should fail when tokenName is empty', () => {
const result = TokenSchema.safeParse({ ...validTokenData, tokenName: '' })
expect(result.success).toBe(false)
if (!result.success) {
const nameError = result.error.issues.find((i) => i.path.includes('tokenName'))
expect(nameError?.message).toBe('Please enter a name for the token')
}
})
it('should fail when tokenName is missing', () => {
const { tokenName, ...rest } = validTokenData
const result = TokenSchema.safeParse(rest)
expect(result.success).toBe(false)
})
it('should fail when permissionRows is empty', () => {
const result = TokenSchema.safeParse({ ...validTokenData, permissionRows: [] })
expect(result.success).toBe(false)
if (!result.success) {
const permError = result.error.issues.find((i) => i.path.includes('permissionRows'))
expect(permError?.message).toBe('Please configure at least one permission')
}
})
it('should fail when resourceAccess is not a valid enum value', () => {
const result = TokenSchema.safeParse({ ...validTokenData, resourceAccess: 'invalid' })
expect(result.success).toBe(false)
})
it('should accept all valid resourceAccess enum values', () => {
for (const value of ['all-orgs', 'selected-orgs', 'selected-projects'] as const) {
const result = TokenSchema.safeParse({ ...validTokenData, resourceAccess: value })
expect(result.success).toBe(true)
}
})
describe('expiresAt preprocessing', () => {
it('should convert "never" to undefined', () => {
const result = TokenSchema.safeParse({ ...validTokenData, expiresAt: 'never' })
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.expiresAt).toBeUndefined()
}
})
it('should pass through other string values', () => {
const result = TokenSchema.safeParse({ ...validTokenData, expiresAt: 'day' })
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.expiresAt).toBe('day')
}
})
it('should allow expiresAt to be omitted', () => {
const { expiresAt, ...rest } = validTokenData
const result = TokenSchema.safeParse(rest)
expect(result.success).toBe(true)
})
})
describe('custom expiry refinement', () => {
it('should fail when expiresAt is "custom" and customExpiryDate is not provided', () => {
const result = TokenSchema.safeParse({
...validTokenData,
expiresAt: 'custom',
customExpiryDate: undefined,
})
expect(result.success).toBe(false)
if (!result.success) {
const customError = result.error.issues.find((i) => i.path.includes('expiresAt'))
expect(customError?.message).toBe('Please select a custom expiry date')
}
})
it('should fail when expiresAt is "custom" and customExpiryDate is empty string', () => {
const result = TokenSchema.safeParse({
...validTokenData,
expiresAt: 'custom',
customExpiryDate: '',
})
expect(result.success).toBe(false)
})
it('should pass when expiresAt is "custom" and customExpiryDate is provided', () => {
const result = TokenSchema.safeParse({
...validTokenData,
expiresAt: 'custom',
customExpiryDate: '2026-12-31T00:00:00Z',
})
expect(result.success).toBe(true)
})
it('should pass when expiresAt is not "custom" even without customExpiryDate', () => {
const result = TokenSchema.safeParse({
...validTokenData,
expiresAt: 'day',
})
expect(result.success).toBe(true)
})
})
describe('nested permissionRows validation', () => {
it('should fail when a permission row has an empty resource', () => {
const result = TokenSchema.safeParse({
...validTokenData,
permissionRows: [{ resource: '', actions: ['read'] }],
})
expect(result.success).toBe(false)
})
it('should fail when a permission row has empty actions', () => {
const result = TokenSchema.safeParse({
...validTokenData,
permissionRows: [{ resource: 'organization:billing', actions: [] }],
})
expect(result.success).toBe(false)
})
it('should pass with multiple valid permission rows', () => {
const result = TokenSchema.safeParse({
...validTokenData,
permissionRows: [
{ resource: 'organization:billing', actions: ['read'] },
{ resource: 'organization:members', actions: ['read', 'write'] },
],
})
expect(result.success).toBe(true)
})
})
})
@@ -1,23 +0,0 @@
import { z } from 'zod'
export const PermissionRowSchema = z.object({
resource: z.string().min(1, 'Please select a resource'),
actions: z.array(z.string()).min(1, 'Please select at least one action'),
})
export const TokenSchema = z
.object({
tokenName: z.string().min(1, 'Please enter a name for the token'),
expiresAt: z.preprocess((val) => (val === 'never' ? undefined : val), z.string().optional()),
customExpiryDate: z.string().optional(),
resourceAccess: z.enum(['all-orgs', 'selected-orgs', 'selected-projects']),
selectedOrganizations: z.array(z.string()).optional(),
selectedProjects: z.array(z.string()).optional(),
permissionRows: z.array(PermissionRowSchema).min(1, 'Please configure at least one permission'),
})
.refine((data) => !(data.expiresAt === 'custom' && !data.customExpiryDate), {
message: 'Please select a custom expiry date',
path: ['expiresAt'],
})
export type TokenFormValues = z.infer<typeof TokenSchema>
@@ -1,75 +0,0 @@
import { Key, Plus } from 'lucide-react'
import {
Button,
Checkbox,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
} from 'ui'
import { PermissionResourceSelectorProps, PermissionRow } from './Permissions.types'
import { ACCESS_TOKEN_RESOURCES } from '@/components/interfaces/Account/AccessTokens/AccessToken.constants'
export const PermissionResourceSelector = ({
open,
onOpenChange,
onResourceToggled,
permissionRows,
align = 'center',
}: PermissionResourceSelectorProps) => {
return (
<Popover open={open} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild>
<Button variant="default" size="tiny" icon={<Plus className="h-4 w-4" />}>
Add permission
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0" align={align}>
<Command>
<CommandInput placeholder="Search resources..." />
<CommandList>
<CommandEmpty>No resources found.</CommandEmpty>
<CommandGroup className="[&>div]:text-left">
<div className="max-h-[210px] overflow-y-auto">
{ACCESS_TOKEN_RESOURCES.map((resource) => {
const isChecked = permissionRows.some(
(row: PermissionRow) => row.resource === resource.resource
)
return (
<CommandItem
key={resource.resource}
value={`${resource.resource} ${resource.title}`}
onSelect={() => onResourceToggled(resource)}
className="text-foreground"
>
<div className="flex items-center gap-3 w-full">
<Checkbox
checked={isChecked}
onCheckedChange={() => onResourceToggled(resource)}
onClick={(e) => e.stopPropagation()}
/>
<Key size={12} className="text-foreground-lighter" />
<div className="flex flex-col text-left flex-1">
<span className="font-medium text-foreground capitalize">
{resource.title}
</span>
</div>
</div>
</CommandItem>
)
})}
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
@@ -1,202 +0,0 @@
import { ChevronDown, RotateCcw, X } from 'lucide-react'
import { useFieldArray, useFormState } from 'react-hook-form'
import {
Button,
Checkbox,
FormControl,
FormField,
FormMessage,
Popover,
PopoverContent,
PopoverTrigger,
WarningIcon,
} from 'ui'
import { TokenFormValues } from '../../../AccessToken.schemas'
import { PermissionResourceSelector } from './PermissionResourceSelector'
import { PermissionsProps } from './Permissions.types'
import { sortActions } from './Permissions.utils'
import { ACCESS_TOKEN_RESOURCES } from '@/components/interfaces/Account/AccessTokens/AccessToken.constants'
import { formatAccessText } from '@/components/interfaces/Account/AccessTokens/AccessToken.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
export const Permissions = ({
control,
resourceSearchOpen,
setResourceSearchOpen,
}: PermissionsProps) => {
const {
fields: permissionRows,
append,
remove,
} = useFieldArray<TokenFormValues>({
name: 'permissionRows',
control,
})
const { errors } = useFormState({ control, name: 'permissionRows' })
return (
<div className="space-y-4 px-5 sm:px-6 py-6">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm">Configure permissions</span>
<div className="flex items-center gap-2">
{permissionRows.length > 0 && (
<ButtonTooltip
variant="default"
size="tiny"
className="p-1"
onClick={() => remove()}
icon={<RotateCcw size={16} />}
tooltip={{
content: {
side: 'top',
align: 'center',
alignOffset: -10,
text: 'Reset all permissions',
},
}}
/>
)}
<PermissionResourceSelector
open={resourceSearchOpen}
onOpenChange={setResourceSearchOpen}
permissionRows={permissionRows}
onResourceToggled={(resource) => {
const index = permissionRows.findIndex((p) => p.resource === resource.resource)
if (index > -1) {
return remove(index)
}
append(resource)
}}
align="end"
/>
</div>
</div>
{permissionRows.length === 0 ? (
<div className="text-center py-8 border border-dashed border-border rounded-lg">
<p className="text-sm text-foreground-light">No permissions configured yet.</p>
</div>
) : (
<div className="border border-border rounded-lg">
{permissionRows.map((row, index) => {
const selectedResource = ACCESS_TOKEN_RESOURCES.find(
(r) => r.resource === row.resource
)
return (
<FormField
key={row.id}
name={`permissionRows.${index}.actions`}
render={({ field, fieldState }) => {
const fieldValue = field.value || []
return (
<div>
<div className="flex items-center gap-3 p-3">
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="flex flex-col">
<span className="text-sm font-medium truncate max-w-[36ch] capitalize">
{selectedResource?.title}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
{selectedResource && (
<Popover>
<FormControl>
<PopoverTrigger asChild>
<Button
id={`permissionRows.${index}.actions`}
aria-describedby={
fieldState.invalid
? `permissionRows.${index}.actions.error`
: undefined
}
variant="default"
size="tiny"
className="w-[150px] flex text-sm justify-between h-7 "
iconRight={
<ChevronDown size={14} className="text-foreground-muted" />
}
ref={field.ref}
>
{fieldValue.length === 0 ? (
<span className="text-foreground-lighter">
Select access
</span>
) : fieldValue.length === 1 ? (
formatAccessText(fieldValue[0])
) : (
`${fieldValue.length} selected`
)}
</Button>
</PopoverTrigger>
</FormControl>
<PopoverContent className="w-[180px] p-2" align="end">
<div className="space-y-2">
{sortActions(selectedResource.actions).map((action) => (
<label
key={action}
className="flex items-center gap-2 cursor-pointer"
>
<Checkbox
checked={fieldValue.includes(action)}
onCheckedChange={(checked) => {
const newActions = checked
? [...fieldValue, action]
: fieldValue.filter((a: string) => a !== action)
field.onChange(newActions)
}}
/>
<span className="text-sm">{formatAccessText(action)}</span>
</label>
))}
</div>
</PopoverContent>
</Popover>
)}
<Button
variant="text"
size="tiny"
className="p-1"
onClick={() => {
remove(index)
}}
icon={<X size={16} />}
aria-label="Remove"
/>
</div>
</div>
<div className="p-3 pt-0">
<FormMessage id={`permissionRows.${index}.actions.error`} />
</div>
{index < permissionRows.length - 1 && (
<div className="border-t border-border" />
)}
</div>
)
}}
/>
)
})}
</div>
)}
</div>
<div className="w-full flex gap-x-2 items-center">
<WarningIcon />
<span className="text-xs text-left text-foreground-lighter">
Once you've set these permissions, you cannot edit them.
</span>
</div>
{errors.permissionRows?.message || errors.permissionRows?.root?.message ? (
<p role="alert" className="mt-2 text-sm text-destructive">
{errors.permissionRows?.message || errors.permissionRows?.root?.message}
</p>
) : null}
</div>
)
}
@@ -1,32 +0,0 @@
import { Control, FieldValues } from 'react-hook-form'
import { TokenFormValues } from '../../../AccessToken.schemas'
export interface PermissionResource {
resource: string
title: string
actions: string[]
}
export interface PermissionRow {
resource: string
actions: string[]
}
export interface PermissionsFormValues extends FieldValues {
permissionRows?: PermissionRow[]
}
export interface PermissionsProps {
control: Control<TokenFormValues>
resourceSearchOpen: boolean
setResourceSearchOpen: (open: boolean) => void
}
export interface PermissionResourceSelectorProps {
open: boolean
onOpenChange: (open: boolean) => void
permissionRows: PermissionRow[]
onResourceToggled: (resource: PermissionResource) => void
align?: 'center' | 'end' | 'start'
}
@@ -1,128 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { PermissionResource, PermissionRow } from './Permissions.types'
import { sortActions, togglePermissionResource } from './Permissions.utils'
// --- sortActions ---
describe('sortActions', () => {
it('should sort actions by priority order: read, write, create, delete', () => {
expect(sortActions(['delete', 'create', 'write', 'read'])).toEqual([
'read',
'write',
'create',
'delete',
])
})
it('should return empty array for empty input', () => {
expect(sortActions([])).toEqual([])
})
it('should keep a single action as-is', () => {
expect(sortActions(['write'])).toEqual(['write'])
})
it('should place unknown actions after the priority ones', () => {
expect(sortActions(['custom-action', 'read', 'delete'])).toEqual([
'read',
'delete',
'custom-action',
])
})
it('should preserve relative order of multiple unknown actions', () => {
expect(sortActions(['zeta', 'alpha', 'read'])).toEqual(['read', 'zeta', 'alpha'])
})
it('should handle only unknown actions', () => {
expect(sortActions(['foo', 'bar'])).toEqual(['foo', 'bar'])
})
it('should handle a subset of priority actions', () => {
expect(sortActions(['delete', 'read'])).toEqual(['read', 'delete'])
})
it('should not mutate the original array', () => {
const original = ['delete', 'read']
sortActions(original)
expect(original).toEqual(['delete', 'read'])
})
})
// --- togglePermissionResource ---
describe('togglePermissionResource', () => {
const billingResource: PermissionResource = {
resource: 'organization:billing',
title: 'Billing',
actions: ['read', 'write'],
}
const membersResource: PermissionResource = {
resource: 'organization:members',
title: 'Members',
actions: ['read', 'write', 'create', 'delete'],
}
const storageResource: PermissionResource = {
resource: 'project:storage',
title: 'Storage',
actions: ['write', 'create'],
}
it('should add a resource with "read" as the default action when available', () => {
const result = togglePermissionResource([], billingResource)
expect(result).toEqual([{ resource: 'organization:billing', actions: ['read'] }])
})
it('should add a resource with the first action as default when "read" is not available', () => {
const result = togglePermissionResource([], storageResource)
expect(result).toEqual([{ resource: 'project:storage', actions: ['write'] }])
})
it('should remove a resource if it is already in the list', () => {
const existing: PermissionRow[] = [
{ resource: 'organization:billing', actions: ['read'] },
{ resource: 'organization:members', actions: ['read', 'write'] },
]
const result = togglePermissionResource(existing, billingResource)
expect(result).toEqual([{ resource: 'organization:members', actions: ['read', 'write'] }])
})
it('should not modify other rows when removing a resource', () => {
const existing: PermissionRow[] = [
{ resource: 'organization:billing', actions: ['read'] },
{ resource: 'organization:members', actions: ['read', 'write'] },
]
const result = togglePermissionResource(existing, billingResource)
expect(result).toHaveLength(1)
expect(result[0]).toEqual({ resource: 'organization:members', actions: ['read', 'write'] })
})
it('should append to existing rows when adding', () => {
const existing: PermissionRow[] = [{ resource: 'organization:billing', actions: ['read'] }]
const result = togglePermissionResource(existing, membersResource)
expect(result).toHaveLength(2)
expect(result[1]).toEqual({ resource: 'organization:members', actions: ['read'] })
})
it('should not mutate the original array', () => {
const existing: PermissionRow[] = [{ resource: 'organization:billing', actions: ['read'] }]
const original = [...existing]
togglePermissionResource(existing, membersResource)
expect(existing).toEqual(original)
})
it('should toggle off then on again correctly', () => {
const step1 = togglePermissionResource([], billingResource)
expect(step1).toHaveLength(1)
const step2 = togglePermissionResource(step1, billingResource)
expect(step2).toHaveLength(0)
const step3 = togglePermissionResource(step2, billingResource)
expect(step3).toHaveLength(1)
expect(step3[0]).toEqual({ resource: 'organization:billing', actions: ['read'] })
})
})
@@ -1,32 +0,0 @@
import { PermissionResource, PermissionRow } from './Permissions.types'
export const sortActions = (actions: string[]): string[] => {
const sorted: string[] = []
const remaining = [...actions]
const priority = ['read', 'write', 'create', 'delete']
for (const action of priority) {
const index = remaining.indexOf(action)
if (index !== -1) {
sorted.push(action)
remaining.splice(index, 1)
}
}
sorted.push(...remaining)
return sorted
}
export const togglePermissionResource = (
permissionRows: PermissionRow[],
resource: PermissionResource
): PermissionRow[] => {
const isAlreadyAdded = permissionRows.some((row) => row.resource === resource.resource)
if (isAlreadyAdded) {
return permissionRows.filter((row) => row.resource !== resource.resource)
}
const defaultActions = resource.actions.includes('read') ? ['read'] : [resource.actions[0]]
return [...permissionRows, { resource: resource.resource, actions: defaultActions }]
}
@@ -1,116 +0,0 @@
import { FormControl, FormField } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ResourceAccessProps } from './ResourceAccess.types'
import { ResourceMultiSelector } from './ResourceMultiSelector'
import { ResourceOption } from './ResourceOption'
import { useOrgAndProjectData } from '@/components/interfaces/Account/AccessTokens/hooks/useOrgAndProjectData'
import { useProfile } from '@/lib/profile'
const RESOURCE_OPTIONS = [
{
value: 'all-orgs' as const,
label: 'Everything',
description: 'Access to all projects across all organizations you have access to.',
},
{
value: 'selected-orgs' as const,
label: 'Selected orgs',
description: 'Access only to the organizations you have specified.',
},
{
value: 'selected-projects' as const,
label: 'Selected projects',
description: 'Access only to the projects you have specified.',
},
]
export const ResourceAccess = ({ control, resourceAccess, setValue }: ResourceAccessProps) => {
const { profile } = useProfile()
const { organizations, projects, isLoadingOrgs, isLoadingProjects } = useOrgAndProjectData({
enabled: !!profile,
})
const handleResourceAccessChange = (newValue: string, currentValue: string) => {
if (newValue !== currentValue) {
setValue('selectedOrganizations', [])
setValue('selectedProjects', [])
}
}
return (
<div className="space-y-4 px-5 sm:px-6 py-6">
<FormField
key="resourceAccess"
name="resourceAccess"
control={control}
render={({ field }) => (
<FormItemLayout name="resourceAccess" label="Resource access">
<FormControl>
<div className="space-y-3">
<fieldset className="flex gap-3">
<legend className="sr-only">Resource access level</legend>
{RESOURCE_OPTIONS.map((option) => (
<ResourceOption
key={option.value}
value={option.value}
label={option.label}
isSelected={field.value === option.value}
onChange={() => {
handleResourceAccessChange(option.value, field.value)
field.onChange(option.value)
}}
/>
))}
</fieldset>
<p className="text-foreground-light text-sm">
{RESOURCE_OPTIONS.find((opt) => opt.value === field.value)?.description}
</p>
</div>
</FormControl>
</FormItemLayout>
)}
/>
{resourceAccess === 'selected-orgs' && (
<FormField
key="selectedOrganizations"
name="selectedOrganizations"
control={control}
render={({ field }) => (
<ResourceMultiSelector
field={field}
items={organizations.map((org) => ({ id: org.slug, name: org.name }))}
isLoading={isLoadingOrgs}
fieldName="selectedOrganizations"
label="Select organizations"
loadingMessage="Loading organizations..."
emptyMessage="No organizations available"
/>
)}
/>
)}
{resourceAccess === 'selected-projects' && (
<FormField
key="selectedProjects"
name="selectedProjects"
control={control}
render={({ field }) => (
<ResourceMultiSelector
field={field}
items={projects.map((project) => ({ id: project.ref, name: project.name }))}
isLoading={isLoadingProjects}
fieldName="selectedProjects"
label="Select projects"
loadingMessage="Loading projects..."
emptyMessage="No projects available"
/>
)}
/>
)}
</div>
)
}
@@ -1,31 +0,0 @@
import { Control, ControllerRenderProps, UseFormSetValue } from 'react-hook-form'
import { TokenFormValues } from '@/components/interfaces/Account/AccessTokens/AccessToken.schemas'
export interface ResourceAccessProps {
control: Control<TokenFormValues>
resourceAccess: string
setValue: UseFormSetValue<TokenFormValues>
}
export interface ResourceItem {
id: string
name: string
}
export interface ResourceMultiSelectorProps {
field: ControllerRenderProps<any, any>
items: ResourceItem[]
isLoading: boolean
fieldName: string
label: string
loadingMessage: string
emptyMessage: string
}
export interface ResourceOptionProps {
value: string
label: string
isSelected: boolean
onChange: () => void
}
@@ -1,69 +0,0 @@
import { useMemo } from 'react'
import { FormControl } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
import { ResourceMultiSelectorProps } from './ResourceAccess.types'
export const ResourceMultiSelector = ({
field,
items,
isLoading,
fieldName,
label,
loadingMessage,
emptyMessage,
}: ResourceMultiSelectorProps) => {
const idToName = useMemo(() => new Map(items.map((item) => [item.id, item.name])), [items])
const displayValues = (field.value || []).map((id: string) => idToName.get(id) || id)
const handleValuesChange = (names: string[]) => {
const ids = names
.map((name) => {
for (const [id, itemName] of idToName.entries()) {
if (itemName === name) return id
}
return name
})
.filter(Boolean)
field.onChange(ids)
}
return (
<FormItemLayout name={fieldName} label={label}>
<FormControl className="overflow-visible">
<MultiSelector values={displayValues} onValuesChange={handleValuesChange}>
<MultiSelectorTrigger
deletableBadge
showIcon={false}
mode="inline-combobox"
label={label}
badgeLimit="wrap"
/>
<MultiSelectorContent className="z-50">
{isLoading ? (
<div className="px-3 py-2 text-sm text-foreground-light">{loadingMessage}</div>
) : items.length === 0 ? (
<div className="px-3 py-2 text-sm text-foreground-light">{emptyMessage}</div>
) : (
<MultiSelectorList>
{items.map((item) => (
<MultiSelectorItem key={item.id} value={item.name}>
{item.name}
</MultiSelectorItem>
))}
</MultiSelectorList>
)}
</MultiSelectorContent>
</MultiSelector>
</FormControl>
</FormItemLayout>
)
}
@@ -1,41 +0,0 @@
import { Box, Check } from 'lucide-react'
import { cn } from 'ui'
export const ResourceOption = ({
value,
label,
isSelected,
onChange,
}: {
value: string
label: string
isSelected: boolean
onChange: () => void
}) => (
<label
className={cn(
'border border-default rounded-md bg-surface-200 hover:bg-overlay-hover hover:border-control px-4 py-3 cursor-pointer transition-colors flex-1 flex flex-col',
isSelected && 'border-foreground-muted hover:border-foreground-muted bg-surface-300'
)}
>
<div className="flex justify-between items-start mb-3">
<Box size={16} className="text-foreground-light" />
{isSelected && (
<div className="flex items-center justify-center p-0.5 bg-foreground text-background rounded-full">
<Check size={12} strokeWidth="4" className="text-background" />
</div>
)}
</div>
<span className={cn('text-sm', isSelected ? 'text-foreground' : 'text-foreground-light')}>
{label}
</span>
<input
type="radio"
name="resourceAccess"
value={value}
checked={isSelected}
onChange={onChange}
className="sr-only"
/>
</label>
)