Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RequirementIcon } from 'lib/seam/components/SupportedDevices/RequirementIcon.js'

import {
getRequirementKind,
getRequirementKinds,
type RequirementKind,
type ReviewDevice,
} from './model.js'
Expand All @@ -12,6 +12,10 @@ const columns: Array<{ kind: RequirementKind; label: string }> = [
{ kind: 'bridge', label: 'Bridge' },
{ kind: 'keypad', label: 'Keypad' },
]
const additionalColumns: typeof columns = [
{ kind: 'legal', label: 'Legal' },
{ kind: 'other', label: 'Other' },
]

export function ProviderRequirements({
provider,
Expand All @@ -20,11 +24,14 @@ export function ProviderRequirements({
provider: ReviewDevice['providers'][number]
view: 'matrix' | 'list'
}): JSX.Element {
const visibleColumns = provider.requirements.some(
({ message }) => getRequirementKind(message) === 'other'
)
? [...columns, { kind: 'other' as const, label: 'Other' }]
: columns
const visibleColumns = [
...columns,
...additionalColumns.filter(({ kind }) =>
provider.requirements.some(({ message }) =>
getRequirementKinds(message).includes(kind)
)
),
]
return (
<>
{provider.capabilities.length === 0 ? (
Expand All @@ -42,7 +49,7 @@ export function ProviderRequirements({
>
<table
className='seam-proto-requirement-matrix'
data-has-other={visibleColumns.length > 3 ? '' : undefined}
data-extra-columns={visibleColumns.length - columns.length}
>
<caption className='seam-compact-sr-only'>
Supported capabilities and their additional requirements
Expand All @@ -63,8 +70,8 @@ export function ProviderRequirements({
<tr key={capability.name}>
<th scope='row'>{capability.label}</th>
{visibleColumns.map(({ kind, label }) => {
const required = capability.messages.some(
(message) => getRequirementKind(message) === kind
const required = capability.messages.some((message) =>
getRequirementKinds(message).includes(kind)
)
return (
<td key={kind}>
Expand Down Expand Up @@ -127,8 +134,8 @@ export function ProviderRequirements({
<div className='seam-proto-capability-pills'>
{visibleColumns
.filter(({ kind }) =>
capability.messages.some(
(message) => getRequirementKind(message) === kind
capability.messages.some((message) =>
getRequirementKinds(message).includes(kind)
)
)
.map(({ kind, label }) => (
Expand Down
43 changes: 43 additions & 0 deletions examples/supported-devices-prototypes/src/model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, test } from 'vitest'

import { getRequirementKinds, getRequirementPills } from './model.js'

const legal =
'To be able to use this system, you will need to first sign an EULA with Salto.'
const combined =
'To be able to use this system, you will both need to first sign an EULA with Salto and install a Seam bridge to make it cloud-ready.'

test('an EULA is a legal prerequisite, distinct from a subscription', () => {
expect(getRequirementKinds(legal)).toEqual(['legal'])
expect(getRequirementKinds('Sign an end-user license agreement.')).toEqual([
'legal',
])
expect(getRequirementKinds('Sign a waiver.')).toEqual(['legal'])
})

test('a combined provider requirement preserves legal and bridge categories and source copy', () => {
expect(getRequirementKinds(combined)).toEqual(['legal', 'bridge'])
expect(
getRequirementPills({
requirements: [{ message: combined, capabilities: ['Access codes'] }],
})
).toEqual([
{ label: 'Accessories Required', messages: [combined] },
{ label: 'Legal Requirements', messages: [combined] },
])
})

test('existing accessory and subscription categories remain distinct; unknown prose stays explicit', () => {
expect(getRequirementKinds('A compatible bridge is required.')).toEqual([
'bridge',
])
expect(getRequirementKinds('An external keypad is required.')).toEqual([
'keypad',
])
expect(
getRequirementKinds('A paying Nuki SmartHosting subscription is required.')
).toEqual(['subscription'])
expect(
getRequirementKinds('Contact the provider to configure access.')
).toEqual(['other'])
})
29 changes: 23 additions & 6 deletions examples/supported-devices-prototypes/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,28 @@ export interface VariantProps {
requirementView?: 'matrix' | 'list'
}

export type RequirementKind = 'subscription' | 'bridge' | 'keypad' | 'other'
export type RequirementKind =
| 'subscription'
| 'bridge'
| 'keypad'
| 'legal'
| 'other'

export function getRequirementKind(message: string): RequirementKind {
/** UI aliases for source prose, which can describe more than one prerequisite. */
export function getRequirementKinds(message: string): RequirementKind[] {
const known: Record<string, RequirementKind> = {
'A compatible bridge is required.': 'bridge',
'An external keypad is required.': 'keypad',
'A paying Nuki SmartHosting subscription is required.': 'subscription',
}
return known[message] ?? 'other'
const kinds: RequirementKind[] = []
const knownKind = known[message.trim()]
if (knownKind != null) kinds.push(knownKind)
if (/\b(?:eula|waiver|end[- ]user licen[cs]e agreement)\b/i.test(message))
kinds.push('legal')
if (/\binstall\s+(?:a\s+)?seam\s+bridge\b/i.test(message))
kinds.push('bridge')
return kinds.length === 0 ? ['other'] : [...new Set(kinds)]
}

export function reviewDevice(
Expand Down Expand Up @@ -116,22 +129,26 @@ function reviewRequirements(

/** Presentation aliases for known requirements; unfamiliar source copy stays explicit. */
export function getRequirementPills(
device: ReviewDevice
device: Pick<ReviewDevice, 'requirements'>
): Array<{ label: string; messages: string[] }> {
const labels: Record<RequirementKind, string> = {
bridge: 'Accessories Required',
keypad: 'Accessories Required',
subscription: 'Subscription Required',
legal: 'Legal Requirements',
other: 'Other Requirements',
}
const groups = new Map<string, string[]>()
for (const { message } of device.requirements) {
const label = labels[getRequirementKind(message)]
groups.set(label, [...(groups.get(label) ?? []), message])
for (const kind of getRequirementKinds(message)) {
const label = labels[kind]
groups.set(label, [...(groups.get(label) ?? []), message])
}
}
return [
'Accessories Required',
'Subscription Required',
'Legal Requirements',
'Other Requirements',
].flatMap((label) => {
const messages = groups.get(label)
Expand Down
5 changes: 4 additions & 1 deletion examples/supported-devices-prototypes/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -839,9 +839,12 @@ body {
border-collapse: collapse;
font-size: 12px;
}
.seam-proto-requirement-matrix[data-has-other] {
.seam-proto-requirement-matrix[data-extra-columns='1'] {
min-width: 356px;
}
.seam-proto-requirement-matrix[data-extra-columns='2'] {
min-width: 420px;
}
.seam-proto-requirement-matrix th,
.seam-proto-requirement-matrix td {
padding: 10px 4px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ export function RequirementIcon({ message }: { message: string }): JSX.Element {
strokeLinecap='round'
strokeLinejoin='round'
>
{/subscription|licen[sc]e|paying|paid|fees?/i.test(message) ? (
{/\b(?:legal|eula|waiver|end[- ]user licen[cs]e agreement)\b/i.test(
message
) ? (
<>
<path d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z' />
<path d='M14 2v6h6M8 15l2 2 5-5' />
</>
) : /subscription|licen[sc]e|paying|paid|fees?/i.test(message) ? (
<>
<rect x='2' y='4' width='20' height='16' rx='3' />
<path d='M2 9h20M6 15h4m4 0h4' />
Expand Down
Loading