Skip to content

Commit 8b5109f

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
improvement(ui): reuse chip inputs for remaining settings fields (#8030)
* improvement(ui): reuse chip inputs for remaining settings fields * improvement(ui): reuse the shared branding upload drop zone (#8031) Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent 526e385 commit 8b5109f

7 files changed

Lines changed: 83 additions & 109 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/chip-field.ts‎

Lines changed: 0 additions & 7 deletions
This file was deleted.

‎apps/sim/app/workspace/[workspaceId]/components/credential-detail/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { AddPeopleModal } from './components/add-people-modal'
2-
export { CHIP_FIELD_INPUT, CHIP_FIELD_SHELL } from './components/chip-field'
32
export { CredentialDetailHeading } from './components/credential-detail-heading'
43
export { CredentialDetailLayout } from './components/credential-detail-layout'
54
export { CredentialMembersSection } from './components/credential-members-section'

‎apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx‎

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ import {
1616
import { Eye, EyeOff, Search } from '@sim/emcn/icons'
1717
import { createLogger } from '@sim/logger'
1818
import { getErrorMessage } from '@sim/utils/errors'
19-
import {
20-
CHIP_FIELD_INPUT,
21-
CHIP_FIELD_SHELL,
22-
} from '@/app/workspace/[workspaceId]/components/credential-detail/components/chip-field'
2319
import { BYOKProviderKeysModal } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal'
2420
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
2521
import {
@@ -432,34 +428,37 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) {
432428
tabIndex={-1}
433429
readOnly
434430
/>
435-
<div className={CHIP_FIELD_SHELL}>
436-
<input
437-
aria-label='API Key'
438-
type={showApiKey ? 'text' : 'password'}
439-
value={apiKeyInput}
440-
onChange={(e) => {
441-
setApiKeyInput(e.target.value)
442-
if (error) setError(null)
443-
}}
444-
placeholder={editingMeta?.placeholder}
445-
className={CHIP_FIELD_INPUT}
446-
name='byok_api_key'
447-
autoComplete='off'
448-
autoCorrect='off'
449-
autoCapitalize='off'
450-
data-lpignore='true'
451-
data-form-type='other'
452-
/>
453-
<Button
454-
variant='quiet'
455-
size='icon'
456-
className='shrink-0'
457-
onClick={() => setShowApiKey(!showApiKey)}
458-
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
459-
>
460-
{showApiKey ? <EyeOff className='size-[13px]' /> : <Eye className='size-[13px]' />}
461-
</Button>
462-
</div>
431+
<ChipInput
432+
aria-label='API Key'
433+
type={showApiKey ? 'text' : 'password'}
434+
value={apiKeyInput}
435+
onChange={(e) => {
436+
setApiKeyInput(e.target.value)
437+
if (error) setError(null)
438+
}}
439+
placeholder={editingMeta?.placeholder}
440+
name='byok_api_key'
441+
autoComplete='off'
442+
autoCorrect='off'
443+
autoCapitalize='off'
444+
data-lpignore='true'
445+
data-form-type='other'
446+
endAdornment={
447+
<Button
448+
variant='quiet'
449+
size='icon'
450+
className='shrink-0'
451+
onClick={() => setShowApiKey(!showApiKey)}
452+
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
453+
>
454+
{showApiKey ? (
455+
<EyeOff className='size-[13px]' />
456+
) : (
457+
<Eye className='size-[13px]' />
458+
)}
459+
</Button>
460+
}
461+
/>
463462
</ChipModalField>
464463
{props.multiKey && (
465464
<ChipModalField

‎apps/sim/ee/whitelabeling/components/whitelabeling-settings.test.tsx‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ afterAll(resetEnvFlagsMock)
1616
vi.mock('@/components/settings/save-discard-actions', () => ({
1717
saveDiscardActions: () => [],
1818
}))
19-
vi.mock('@/app/workspace/[workspaceId]/components/credential-detail', () => ({
20-
CHIP_FIELD_INPUT: '',
21-
CHIP_FIELD_SHELL: '',
22-
}))
2319
vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({
2420
SettingsEmptyState: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
2521
}))

‎apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx‎

Lines changed: 26 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ import { isEnterprise } from '@/lib/billing/plan-helpers'
1111
import { HEX_COLOR_REGEX } from '@/lib/branding'
1212
import type { OrganizationWhitelabelSettings } from '@/lib/branding/types'
1313
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
14-
import {
15-
CHIP_FIELD_INPUT,
16-
CHIP_FIELD_SHELL,
17-
} from '@/app/workspace/[workspaceId]/components/credential-detail'
14+
import { DropZone } from '@/app/workspace/[workspaceId]/components/drop-zone'
1815
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
1916
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
2017
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
@@ -31,42 +28,6 @@ import { useWorkspacesQuery } from '@/hooks/queries/workspace'
3128

3229
const logger = createLogger('WhitelabelingSettings')
3330

34-
interface DropZoneProps {
35-
onDrop: (e: React.DragEvent) => void
36-
children: React.ReactNode
37-
className?: string
38-
}
39-
40-
function DropZone({ onDrop, children, className }: DropZoneProps) {
41-
const [isDragging, setIsDragging] = useState(false)
42-
43-
return (
44-
<div
45-
className={cn('relative', className)}
46-
onDragOver={(e) => {
47-
if (e.dataTransfer.types.includes('Files')) {
48-
e.preventDefault()
49-
setIsDragging(true)
50-
}
51-
}}
52-
onDragLeave={(e) => {
53-
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
54-
setIsDragging(false)
55-
}
56-
}}
57-
onDrop={(e) => {
58-
setIsDragging(false)
59-
onDrop(e)
60-
}}
61-
>
62-
{children}
63-
{isDragging && (
64-
<div className='pointer-events-none absolute inset-0 z-10 rounded-lg border-[1.5px] border-[var(--brand-accent)] border-dashed bg-[color-mix(in_srgb,var(--brand-accent)_8%,transparent)]' />
65-
)}
66-
</div>
67-
)
68-
}
69-
7031
interface ColorInputProps {
7132
label: string
7233
value: string
@@ -81,30 +42,31 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn
8142
return (
8243
<div className='flex flex-col gap-1.5'>
8344
<Label>{label}</Label>
84-
<div className={cn(CHIP_FIELD_SHELL, !isValidHex && 'border-[var(--text-error)]')}>
85-
<div
86-
className={cn(
87-
'size-[16px] shrink-0 rounded-sm border border-[var(--border-1)]',
88-
!showColor && 'bg-[var(--surface-3)]'
89-
)}
90-
style={showColor ? { backgroundColor: value } : undefined}
91-
/>
92-
<input
93-
value={value}
94-
onChange={(e) => {
95-
let v = e.target.value.trim()
96-
if (v && !v.startsWith('#')) {
97-
v = `#${v}`
98-
}
99-
v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '')
100-
onChange(v.slice(0, 7))
101-
}}
102-
onFocus={(e) => e.target.select()}
103-
placeholder={placeholder}
104-
maxLength={7}
105-
className={cn(CHIP_FIELD_INPUT, 'font-mono')}
106-
/>
107-
</div>
45+
<ChipInput
46+
error={!isValidHex}
47+
startAdornment={
48+
<div
49+
className={cn(
50+
'size-[16px] shrink-0 rounded-sm border border-[var(--border-1)]',
51+
!showColor && 'bg-[var(--surface-3)]'
52+
)}
53+
style={showColor ? { backgroundColor: value } : undefined}
54+
/>
55+
}
56+
value={value}
57+
onChange={(e) => {
58+
let v = e.target.value.trim()
59+
if (v && !v.startsWith('#')) {
60+
v = `#${v}`
61+
}
62+
v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '')
63+
onChange(v.slice(0, 7))
64+
}}
65+
onFocus={(e) => e.target.select()}
66+
placeholder={placeholder}
67+
maxLength={7}
68+
inputClassName='font-mono'
69+
/>
10870
{!isValidHex && (
10971
<p className='text-[var(--text-error)] text-caption'>
11072
Must be a valid hex color (e.g. #33c482)

‎packages/emcn/src/components/chip-input/chip-input.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ afterEach(() => {
2929
})
3030

3131
describe('ChipInput', () => {
32+
it('keeps the focused input mounted when custom leading content changes', () => {
33+
const input = mount()
34+
const render = (color: string) => (
35+
<ChipInput
36+
aria-label='Color'
37+
startAdornment={<span aria-hidden style={{ backgroundColor: color }} />}
38+
endAdornment={<button type='button'>Reset</button>}
39+
/>
40+
)
41+
act(() => root?.render(render('#123456')))
42+
input.focus()
43+
input.value = '#123456'
44+
act(() => root?.render(render('#abcdef')))
45+
46+
expect(container?.querySelector('input')).toBe(input)
47+
expect(document.activeElement).toBe(input)
48+
expect(input.value).toBe('#123456')
49+
expect(input.previousElementSibling?.getAttribute('style')).toContain('rgb(171, 205, 239)')
50+
expect(input.nextElementSibling?.textContent).toBe('Reset')
51+
})
52+
3253
it('reserves paintable clearance for a leading glyph without shifting its alignment', () => {
3354
const input = mount()
3455

‎packages/emcn/src/components/chip-input/chip-input.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type ChipInputIcon = React.ComponentType<{ className?: string }>
3535
export interface ChipInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
3636
/** Leading icon component (e.g. `Search` from `@sim/emcn/icons`). Rendered at 14px in `--text-icon`, with the chip's 1.5 gap. */
3737
icon?: ChipInputIcon
38+
/** Custom leading content, such as a color swatch. Takes precedence over `icon`. */
39+
startAdornment?: React.ReactNode
3840
/** Trailing content rendered after the input (e.g. reveal / copy buttons). */
3941
endAdornment?: React.ReactNode
4042
/** Marks the field invalid; swaps the border to the error token. */
@@ -55,6 +57,7 @@ export const ChipInput = React.forwardRef<HTMLInputElement, ChipInputProps>(
5557
className,
5658
inputClassName,
5759
icon: Icon,
60+
startAdornment,
5861
endAdornment,
5962
error,
6063
disabled,
@@ -73,7 +76,8 @@ export const ChipInput = React.forwardRef<HTMLInputElement, ChipInputProps>(
7376
className
7477
)}
7578
>
76-
{Icon ? <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' /> : null}
79+
{startAdornment ??
80+
(Icon ? <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' /> : null)}
7781
<input
7882
ref={ref}
7983
type={type}

0 commit comments

Comments
 (0)