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
33 changes: 28 additions & 5 deletions i18n/pseudo-surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ async function openLayer(
english: string | RegExp,
options: { exact?: boolean } = {},
) {
await expandLayers(page)
const name =
typeof english !== 'string' || options.exact === false ? english : englishOrPseudo(english)
await page.getByText(name, { exact: true }).first().click()
await page.waitForTimeout(1200)
await expandInspector(page)
}

/** Opens the Scene panel and expands every row. */
async function expandLayers(page: Page) {
await openScenePanel(page)
// Expand every row: the tree's chevrons are the small square buttons beside each name.
for (let pass = 0; pass < 3; pass++) {
Expand All @@ -102,11 +112,6 @@ async function openLayer(
}
await page.waitForTimeout(700)
}
const name =
typeof english !== 'string' || options.exact === false ? english : englishOrPseudo(english)
await page.getByText(name, { exact: true }).first().click()
await page.waitForTimeout(1200)
await expandInspector(page)
}

/** The inspector opens collapsed to its header on small viewports. */
Expand Down Expand Up @@ -336,6 +341,24 @@ export const surfaces: PseudoSurface[] = [
scope: '[data-surface="site-panel"]',
data: apartmentData,
},
{
name: 'layer-room-rename',
path: apartmentPath,
steps: async (page) => {
await expandLayers(page)
// The template's "Bedroom 1" shows, and edits, as the catalog name in the UI language.
const row = page.getByText(`${pseudo('Bedroom')} 1`, { exact: true }).first()
await row.hover()
await row
.locator('xpath=..')
.getByRole('button', { name: pseudo('Rename'), exact: true })
.click()
await page.locator('[data-surface="site-panel"] input[type="text"]').first().waitFor()
await page.waitForTimeout(500)
},
scope: '[data-surface="site-panel"]',
data: apartmentData,
},
{
name: 'level-selector',
path: apartmentPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -815,8 +815,8 @@ function SystemSummaryPill({
{formatMeasurement(summary.runLengthM, unit, metricNotation)} ·{' '}
{i18n._({
id: 'editor.floatingActionMenu.runCount',
message: '{count, plural, one {# run} other {# runs}}',
values: { count: summary.runCount },
message: '{count, plural, one {{number} run} other {{number} runs}}',
values: { count: summary.runCount, number: String(summary.runCount) },
})}
</span>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,16 +311,16 @@ type Notice = {
}

export function moveTogetherLabel(count: number, lower: boolean, i18n: PascalI18n) {
const values = { count }
const values = { count, number: String(count) }
return lower
? i18n._({
id: 'editor.panels.floorFoundationPanel.lowerTogether',
message: '{count, plural, =2 {Lower both} other {Lower all #}}',
message: '{count, plural, =2 {Lower both} other {Lower all {number}}}',
values,
})
: i18n._({
id: 'editor.panels.floorFoundationPanel.raiseTogether',
message: '{count, plural, =2 {Raise both} other {Raise all #}}',
message: '{count, plural, =2 {Raise both} other {Raise all {number}}}',
values,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,13 +700,18 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
? i18n._({
id: 'editor.panels.roomConstructionRows.partialWalls',
message:
'Partial · {walls, plural, one {# wall} other {# walls}}, {openSides, plural, one {# open side} other {# open sides}}',
values: { walls: wallIds.length, openSides },
'Partial · {walls, plural, one {{wallCount} wall} other {{wallCount} walls}}, {openSides, plural, one {{openSideCount} open side} other {{openSideCount} open sides}}',
values: {
walls: wallIds.length,
openSides,
wallCount: String(wallIds.length),
openSideCount: String(openSides),
},
})
: i18n._({
id: 'editor.panels.roomConstructionRows.wallCount',
message: '{walls, plural, one {# wall} other {# walls}}',
values: { walls: wallIds.length },
message: '{walls, plural, one {{wallCount} wall} other {{wallCount} walls}}',
values: { walls: wallIds.length, wallCount: String(wallIds.length) },
})
const locked = roomOutsideFacesLocked(nodes, wallIds)

Expand Down Expand Up @@ -763,8 +768,9 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
<PartField
label={i18n._({
id: 'editor.panels.roomConstructionRows.openSides',
message: '{openSides, plural, one {# open side} other {# open sides}}',
values: { openSides },
message:
'{openSides, plural, one {{openSideCount} open side} other {{openSideCount} open sides}}',
values: { openSides, openSideCount: String(openSides) },
})}
>
<button
Expand Down
31 changes: 31 additions & 0 deletions packages/editor/src/components/ui/panels/room-name-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,37 @@ describe('roomNameOptions', () => {
expect(roomNameOptions('kitchen', SECTIONS).typed?.value).toBe('kitchen')
})

test('an exact English name wins over names that contain it', () => {
const storage: RoomNameSection[] = [
{ id: 'utility', title: 'Utility', names: ['Walk-in closet', 'Closet'] },
]
for (const typed of ['Closet', 'closet']) {
const { options, active } = roomNameOptions(typed, storage)
expect(options[active]?.value).toBe('Closet')
}
})

test('a shared label is exact only for the room’s own entry', () => {
const shared: RoomNameSection[] = [
{
id: 'living',
title: 'Séjour',
names: ['Living room', 'Kitchen', 'Lounge'],
labels: ['Salon', 'Cuisine', 'Salon'],
},
]
const activeValue = (current: string) => {
const { options, active } = roomNameOptions('Salon', shared, true, current)
return options[active]?.value
}
expect(activeValue('Lounge')).toBe('Lounge')
expect(activeValue('Lounge 2')).toBe('Lounge')
expect(activeValue('Living room')).toBe('Living room')
// Any other room falls back to the first suggestion.
expect(activeValue('Kitchen')).toBe('Living room')
expect(activeValue('')).toBe('Living room')
})

test('the keyboard order is the typed row, then the groups in order', () => {
const { groups, options } = roomNameOptions('e', SECTIONS)
expect(options.slice(1)).toEqual(groups.flatMap((g) => g.options))
Expand Down
52 changes: 42 additions & 10 deletions packages/editor/src/components/ui/panels/room-name-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useScene } from '@pascal-app/core'
import { numberedRoomName, type RoomNameSection } from '../../../lib/room-name-catalog'
import { type AnyNodeId, useScene } from '@pascal-app/core'
import {
baseRoomName,
numberedRoomName,
type RoomNameSection,
} from '../../../lib/room-name-catalog'
import {
type DescriptorTranslator,
displayRoomName,
Expand All @@ -25,12 +29,13 @@ export type RoomNameGroup = { id: string; title: string; options: RoomNameOption
* case and surrounding spaces), drops the sections left empty, and puts the
* typed text first as its own row unless it is exactly an English catalog
* name. `options` is the flat order the keyboard walks: the typed row, then
* each group's rows.
* each group's rows. `current` is the room's stored name.
*/
export function roomNameOptions(
typed: string,
sections: readonly RoomNameSection[],
filtering = true,
current = '',
): {
typed: RoomNameOption | null
groups: RoomNameGroup[]
Expand Down Expand Up @@ -62,12 +67,15 @@ export function roomNameOptions(
query && !known ? { kind: 'typed', value: typed, label: typed, match: null } : null
const options = [...(typedRow ? [typedRow] : []), ...groups.flatMap((group) => group.options)]
const suggestions = options.filter((option) => option.kind === 'suggestion')
// Labels can be shared ("Salon" for both Living room and Lounge): a label is
// only an exact match when no other name has it.
// Labels can be shared ("Salon" for both Living room and Lounge): a shared
// label is only an exact match for the room's own name.
const own = baseRoomName(current).toLowerCase()
const byLabel = suggestions.filter((option) => option.label.toLowerCase() === query)
const exactRow =
suggestions.find((option) => option.value.toLowerCase() === query) ??
(byLabel.length === 1 ? byLabel[0] : undefined)
(byLabel.length === 1
? byLabel[0]
: byLabel.find((option) => option.value.toLowerCase() === own))
const exact = exactRow ? options.indexOf(exactRow) : -1
return {
typed: typedRow,
Expand All @@ -78,20 +86,44 @@ export function roomNameOptions(
}

/**
* The room panel's commit of its edited field, on blur or Enter (unless Enter
* picks a suggestion). `committed` is what the field last committed or showed;
* text still equal to it writes nothing.
* The commit of an edited room-name field, on blur or Enter (unless Enter
* picks a suggestion): the room panel's, and the layer tree's with its own
* `write`. `committed` is what the field last committed or showed; text still
* equal to it writes nothing.
*/
export function commitRoomNameEdit(
zoneId: string,
name: string,
committed: string,
text: string,
i18n: DescriptorTranslator,
write: (zoneId: string, name: string) => void = renameRoom,
) {
if (text === committed) return
const stored = editedRoomName(name, text, i18n)
if (stored !== name) renameRoom(zoneId, stored)
if (stored !== name) write(zoneId, stored)
}

/**
* The layer tree's rename of a room or zone. Its field shows the name as
* `displayRoomName` does and passes the trimmed text, or `undefined` when
* cleared; it writes through `updateNode`, as main's tree does.
*/
export function commitTreeRoomRename(
zoneId: string,
text: string | undefined,
i18n: DescriptorTranslator,
) {
const { nodes, updateNode } = useScene.getState()
const id = zoneId as AnyNodeId
const name = nodes[id]?.name ?? ''
if (text === undefined) {
updateNode(id, { name: undefined })
return
}
commitRoomNameEdit(zoneId, name, displayRoomName(name, i18n), text, i18n, (_, stored) =>
updateNode(id, { name: stored }),
)
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/components/ui/panels/room-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ function RoomName({
const originalRef = useRef(display)
const inputRef = useRef<HTMLInputElement>(null)
const listId = useId()
const { typed: typedRow, groups, options } = roomNameOptions(value, sections, typing)
const { typed: typedRow, groups, options } = roomNameOptions(value, sections, typing, name)
const expanded = open && options.length > 0
const activeId = expanded && options[active] ? `${listId}-${active}` : undefined
const current = baseRoomName(name).toLowerCase()
Expand Down Expand Up @@ -755,7 +755,7 @@ function RoomName({
setTyping(true)
setOpen(true)
scrollToActive.current = true
setActive(roomNameOptions(event.target.value, sections, true).active)
setActive(roomNameOptions(event.target.value, sections, true, name).active)
}}
onFocus={() => {
originalRef.current = valueRef.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,13 @@ import { useUploadStore } from '../../../../../store/use-upload'
import { MetricControl } from '../../../controls/metric-control'
import { LevelDuplicateDialog } from '../../../level-duplicate-dialog'
import { CollectionsSection } from './collection-tree-node'
import { InlineRenameInput } from './inline-rename-input'
import { InlineRenameInput, RoomRenameInput } from './inline-rename-input'
import { ZoneMembershipCheckbox } from './zone-membership-checkbox'
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
import { TreeNodeDragProvider } from './tree-node-drag'
import { zoneDefaultName } from './default-names'
import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
import { authoredLevelName, localizedLevelName } from '../../../../../i18n/level-name'
import { displayRoomName } from '../../../../../lib/room-name-messages'

// ============================================================================
// PROPERTY LINE SECTION
Expand Down Expand Up @@ -1298,9 +1297,8 @@ const ZoneItem = memo(function ZoneItem({
/>
</span>
<div className="min-w-0 flex-1 pr-1">
<InlineRenameInput
<RoomRenameInput
defaultName={defaultName}
displayName={zone.name ? displayRoomName(zone.name, i18n) : undefined}
isEditing={isEditing}
nodeId={zone.id}
onStartEditing={() => setIsEditing(true)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { type AnyNodeId, usePascalI18n, useScene } from '@pascal-app/core'
import { Pencil } from 'lucide-react'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { displayRoomName } from './../../../../../lib/room-name-messages'
import { cn } from './../../../../../lib/utils'
import { commitTreeRoomRename } from './../../../panels/room-name-options'

interface InlineRenameFieldProps {
name: string | undefined
Expand Down Expand Up @@ -38,6 +40,30 @@ export const InlineRenameInput = memo(function InlineRenameInput({
return <InlineRenameField {...props} name={name} onRename={handleRename} />
})

/**
* Renames a room or zone from the layer tree. The field edits the name in the
* UI language, as the room panel shows it, and stores what an edit there
* stores: a catalog name stays English ("Salon 3" on a Lounge stores "Lounge 3").
*/
export const RoomRenameInput = memo(function RoomRenameInput({
nodeId,
...props
}: Omit<InlineRenameInputProps, 'editableName' | 'displayName'>) {
const i18n = usePascalI18n()
const stored = useScene((s) => s.nodes[nodeId]?.name)
const handleRename = useCallback(
(next: string | undefined) => commitTreeRoomRename(nodeId, next, i18n),
[nodeId, i18n],
)
return (
<InlineRenameField
{...props}
name={stored ? displayRoomName(stored, i18n) : stored}
onRename={handleRename}
/>
)
})

export const InlineRenameField = memo(function InlineRenameField({
name,
onRename,
Expand Down Expand Up @@ -108,6 +134,7 @@ export const InlineRenameField = memo(function InlineRenameField({

return (
<input
aria-label={i18n._({ id: 'editor.layers.rename', message: 'Rename' })}
className={cn(
'm-0 h-5 min-w-[1ch] max-w-full flex-none rounded-none border-primary/50 border-b bg-transparent px-0 py-0 text-foreground text-sm outline-none focus:border-primary',
className,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import {
useZoneSelected,
} from './../../../../../lib/room-zone-routing'
import { toggleZoneMembership } from './../../../../../lib/units'
import { InlineRenameInput } from './inline-rename-input'
import { RoomRenameInput } from './inline-rename-input'
import { focusTreeNode, TreeNodeWrapper } from './tree-node'
import { TreeNodeActions } from './tree-node-actions'
import { ZoneMembershipCheckbox } from './zone-membership-checkbox'
import { zoneDefaultName } from './default-names'
import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
import { displayRoomName } from '../../../../../lib/room-name-messages'

interface ZoneTreeNodeProps {
nodeId: ZoneNode['id']
Expand All @@ -31,7 +30,6 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
const [isEditing, setIsEditing] = useState(false)
const updateNode = useScene((state) => state.updateNode)
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
const storedName = useScene((s) => s.nodes[nodeId]?.name)
const color = useScene((s) => (s.nodes[nodeId] as ZoneNode | undefined)?.color)
const polygon = useScene((s) => (s.nodes[nodeId] as ZoneNode | undefined)?.polygon ?? [])
const isSelected = useZoneSelected(nodeId)
Expand Down Expand Up @@ -85,9 +83,8 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
unitName={focusedUnit.name || i18n._(nodeLabelDescriptor('unit', 'Unit'))}
/>
)}
<InlineRenameInput
<RoomRenameInput
defaultName={defaultName}
displayName={storedName ? displayRoomName(storedName, i18n) : undefined}
isEditing={isEditing}
nodeId={nodeId}
onStartEditing={handleStartEditing}
Expand Down
Loading
Loading