Skip to content

Commit 8abd75c

Browse files
committed
Refine the Dock and desktop files
1 parent 51cd35e commit 8abd75c

7 files changed

Lines changed: 546 additions & 95 deletions

File tree

‎src/App.test.tsx‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ interface MockDesktopShellProps {
5656
readonly settingsSlot?: ReactNode
5757
readonly notificationSlot?: ReactNode
5858
readonly focusRequest?: { readonly appId: string }
59+
readonly desktopItems?: readonly {
60+
readonly id: string
61+
readonly title: string
62+
readonly previewUrl?: string
63+
}[]
64+
readonly onOpenDesktopItem?: (itemId: string) => void
5965
}
6066

6167
vi.mock('./shell', async () => {
@@ -68,11 +74,33 @@ vi.mock('./shell', async () => {
6874
save: () => undefined,
6975
clear: () => undefined,
7076
}),
71-
DesktopShell: ({ apps, settingsSlot, notificationSlot, focusRequest }: MockDesktopShellProps) => createElement(
77+
DesktopShell: ({
78+
apps,
79+
settingsSlot,
80+
notificationSlot,
81+
focusRequest,
82+
desktopItems = [],
83+
onOpenDesktopItem,
84+
}: MockDesktopShellProps) => createElement(
7285
'main',
7386
{ 'data-testid': 'desktop-shell', 'data-focus-app': focusRequest?.appId },
7487
createElement('aside', { 'data-testid': 'settings-slot' }, settingsSlot),
7588
notificationSlot,
89+
createElement(
90+
'nav',
91+
{ 'data-testid': 'desktop-items' },
92+
...desktopItems.map((item) => createElement(
93+
'button',
94+
{
95+
key: item.id,
96+
type: 'button',
97+
'data-desktop-item-id': item.id,
98+
onDoubleClick: () => onOpenDesktopItem?.(item.id),
99+
},
100+
item.previewUrl ? createElement('img', { src: item.previewUrl, alt: '' }) : null,
101+
item.title,
102+
)),
103+
),
76104
...apps.map((app) => createElement('section', { key: app.id, 'data-app-id': app.id }, app.content)),
77105
),
78106
}
@@ -615,6 +643,30 @@ describe('App forensics handoff', () => {
615643
expect(viewer?.textContent).toContain('Lobi kamera kaydı')
616644
expect(viewer?.textContent).not.toContain('Görsel 1')
617645
})
646+
647+
it('puts only projected evidence files on the desktop and opens their authorized asset', async () => {
648+
await act(async () => {
649+
root.render(<App />)
650+
await flushMicrotasks()
651+
})
652+
653+
const items = host.querySelector<HTMLElement>('[data-testid="desktop-items"]')!
654+
const file = items.querySelector<HTMLButtonElement>('[data-desktop-item-id="camera-record"]')!
655+
expect(items.querySelectorAll('button')).toHaveLength(1)
656+
expect(file.textContent).toContain('Lobi kamera kaydı')
657+
expect(file.querySelector('img')?.getAttribute('src')).toBe(
658+
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==',
659+
)
660+
661+
await act(async () => {
662+
file.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
663+
await flushMicrotasks()
664+
})
665+
666+
const viewer = host.querySelector<HTMLElement>('[data-modal-kind="asset"]')
667+
expect(viewer?.textContent).toContain('Lobi kamera kaydı')
668+
expect(viewer?.textContent).not.toContain('Görsel 1')
669+
})
618670
})
619671

620672
describe('App contact discovery handoff', () => {

‎src/App.tsx‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
createLocalStorageLayoutPersistence,
3232
DesktopShell,
3333
kebabCaseChannelName,
34+
type DesktopItemDefinition,
3435
type ShellAppDefinition,
3536
} from './shell'
3637
import {
@@ -1655,11 +1656,51 @@ function CaseDesktop({
16551656
if (boardAsset) return boardAsset
16561657
for (const record of models.files.records) {
16571658
const asset = record.assets.find(({ id }) => id === openAssetId)
1658-
if (asset) return asset
1659+
if (asset) {
1660+
return {
1661+
...asset,
1662+
label: record.assets.length === 1
1663+
? record.title
1664+
: `${record.title} · ${asset.label}`,
1665+
description: asset.description ?? record.summary,
1666+
}
1667+
}
16591668
}
16601669
return undefined
16611670
}, [caseBoardModel.pins, inboxModel.messages, models.files.records, openAssetId])
16621671

1672+
const desktopItems = useMemo<readonly DesktopItemDefinition[]>(() => (
1673+
models.files.records.map((record) => {
1674+
const asset = record.assets[0]
1675+
const previewUrl = asset?.thumbnailUrl ?? (
1676+
asset?.kind === 'image' ? asset.deliveryUrl : undefined
1677+
)
1678+
return {
1679+
id: record.id,
1680+
title: record.title,
1681+
kind: asset?.kind ?? 'file',
1682+
...(previewUrl ? { previewUrl } : {}),
1683+
status: record.status === 'new' ? 'new' : 'reviewed',
1684+
}
1685+
})
1686+
), [models.files.records])
1687+
1688+
const openDesktopItem = useCallback((recordId: string) => {
1689+
const record = models.files.records.find(({ id }) => id === recordId)
1690+
if (!record) return
1691+
setSelection((current) => ({
1692+
...current,
1693+
selectedEvidenceId: record.id,
1694+
selectedRecordId: record.id,
1695+
}))
1696+
const firstAsset = record.assets[0]
1697+
if (firstAsset?.deliveryUrl) {
1698+
setOpenAssetId(firstAsset.id)
1699+
return
1700+
}
1701+
focusApp('files')
1702+
}, [focusApp, models.files.records])
1703+
16631704
const apps = useMemo<readonly ShellAppDefinition[]>(() => [
16641705
{
16651706
id: 'casebook',
@@ -1700,7 +1741,6 @@ function CaseDesktop({
17001741
defaultActive: false,
17011742
minSize: { width: 650, height: 430 },
17021743
defaultOpen: true,
1703-
desktopShortcut: true,
17041744
startMenu: true,
17051745
taskbarPinned: true,
17061746
windowClassName: 'detective-window--casebook',
@@ -1720,7 +1760,6 @@ function CaseDesktop({
17201760
initialBounds: { x: 145, y: 52, width: 940, height: 650 },
17211761
minSize: { width: 760, height: 520 },
17221762
defaultOpen: false,
1723-
desktopShortcut: true,
17241763
startMenu: true,
17251764
taskbarPinned: true,
17261765
windowClassName: 'detective-window--case-board',
@@ -1740,7 +1779,6 @@ function CaseDesktop({
17401779
initialBounds: { x: 355, y: 58, width: 820, height: 610 },
17411780
minSize: { width: 680, height: 480 },
17421781
defaultOpen: false,
1743-
desktopShortcut: true,
17441782
startMenu: true,
17451783
taskbarPinned: true,
17461784
windowClassName: 'detective-window--case-dispatch',
@@ -1763,7 +1801,6 @@ function CaseDesktop({
17631801
initialBounds: { x: 105, y: 35, width: 900, height: 620 },
17641802
minSize: { width: 720, height: 450 },
17651803
defaultOpen: false,
1766-
desktopShortcut: true,
17671804
startMenu: true,
17681805
taskbarPinned: true,
17691806
windowClassName: 'detective-window--inbox',
@@ -1822,7 +1859,6 @@ function CaseDesktop({
18221859
closable: true,
18231860
defaultOpen: true,
18241861
defaultActive: true,
1825-
desktopShortcut: true,
18261862
startMenu: true,
18271863
taskbarPinned: true,
18281864
badge: newContactIds.length || models.phone.affordances?.length || undefined,
@@ -1851,7 +1887,6 @@ function CaseDesktop({
18511887
initialBounds: { x: 72, y: 28, width: 920, height: 620 },
18521888
minSize: { width: 720, height: 500 },
18531889
defaultOpen: false,
1854-
desktopShortcut: true,
18551890
startMenu: true,
18561891
taskbarPinned: true,
18571892
},
@@ -1885,7 +1920,6 @@ function CaseDesktop({
18851920
initialBounds: { x: 180, y: 54, width: 960, height: 640 },
18861921
minSize: { width: 450, height: 280 },
18871922
defaultOpen: false,
1888-
desktopShortcut: true,
18891923
startMenu: true,
18901924
taskbarPinned: true,
18911925
badge: snapshot.affordances.filter(({ surface }) => surface === 'web').length || undefined,
@@ -1911,7 +1945,6 @@ function CaseDesktop({
19111945
initialBounds: { x: 1130, y: 115, width: 310, height: 594 },
19121946
minSize: { width: 280, height: 380 },
19131947
defaultOpen: false,
1914-
desktopShortcut: false,
19151948
startMenu: true,
19161949
taskbarPinned: false,
19171950
},
@@ -1948,6 +1981,8 @@ function CaseDesktop({
19481981
<DesktopShell
19491982
key={`${manifest.case.id}:${runEpoch}`}
19501983
apps={apps}
1984+
desktopItems={desktopItems}
1985+
onOpenDesktopItem={openDesktopItem}
19511986
brand="opencase"
19521987
subtitle={manifest.case.title}
19531988
ariaLabel={copy.desktopAria(manifest.case.title)}

‎src/shell/DesktopShell.test.tsx‎

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { renderToStaticMarkup } from 'react-dom/server'
66
import { describe, expect, it, vi } from 'vitest'
77

88
import { DesktopShell } from './DesktopShell'
9-
import type { DesktopLayoutSnapshot, ShellAppDefinition } from './types'
9+
import type { DesktopItemDefinition, DesktopLayoutSnapshot, ShellAppDefinition } from './types'
1010

1111
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
1212

@@ -20,6 +20,144 @@ const dockedApp: ShellAppDefinition = {
2020

2121
const turkishShellProps = { locale: 'tr' as const }
2222

23+
const floatingDockApps: ShellAppDefinition[] = ['notes', 'files', 'browser'].map((id, index) => ({
24+
id,
25+
title: id,
26+
icon: { type: 'glyph', value: `${index + 1}` },
27+
content: <p>{id}</p>,
28+
placement: 'floating',
29+
}))
30+
31+
const desktopFiles: DesktopItemDefinition[] = [
32+
{
33+
id: 'lobby-camera',
34+
title: 'Lobby camera',
35+
kind: 'image',
36+
previewUrl: '/assets/lobby-camera.jpg',
37+
status: 'new',
38+
},
39+
{
40+
id: 'witness-statement',
41+
title: 'Witness statement',
42+
kind: 'document',
43+
status: 'reviewed',
44+
},
45+
]
46+
47+
describe('DesktopShell dock presentation', () => {
48+
it('keeps applications in the dock and renders supplied files on the desktop', () => {
49+
const html = renderToStaticMarkup(
50+
<DesktopShell {...turkishShellProps} apps={floatingDockApps} desktopItems={desktopFiles} />,
51+
)
52+
53+
expect(html.match(/detective-dock__icon/g)).toHaveLength(floatingDockApps.length)
54+
expect(html.match(/data-desktop-item-id=/g)).toHaveLength(desktopFiles.length)
55+
expect(html).toContain('/assets/lobby-camera.jpg')
56+
expect(html).toContain('detective-desktop__file-icon--document')
57+
expect(html).not.toContain('detective-desktop__alias-badge')
58+
})
59+
60+
it('selects desktop files on one click and opens them on double click or Enter', async () => {
61+
const onOpenDesktopItem = vi.fn()
62+
const host = document.createElement('div')
63+
document.body.append(host)
64+
const root = createRoot(host)
65+
66+
try {
67+
await act(async () => {
68+
root.render(
69+
<DesktopShell
70+
{...turkishShellProps}
71+
apps={floatingDockApps}
72+
desktopItems={desktopFiles}
73+
onOpenDesktopItem={onOpenDesktopItem}
74+
/>,
75+
)
76+
})
77+
const file = host.querySelector<HTMLButtonElement>('[data-desktop-item-id="lobby-camera"]')!
78+
79+
await act(async () => file.click())
80+
expect(file.classList.contains('is-selected')).toBe(true)
81+
expect(onOpenDesktopItem).not.toHaveBeenCalled()
82+
83+
await act(async () => {
84+
file.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
85+
})
86+
expect(onOpenDesktopItem).toHaveBeenLastCalledWith('lobby-camera')
87+
88+
await act(async () => {
89+
file.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
90+
})
91+
expect(onOpenDesktopItem).toHaveBeenCalledTimes(2)
92+
} finally {
93+
await act(async () => root.unmount())
94+
host.remove()
95+
}
96+
})
97+
98+
it('magnifies and spreads nearby dock icons continuously, then resets on leave', async () => {
99+
const originalRect = HTMLElement.prototype.getBoundingClientRect
100+
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
101+
if (this.matches('.detective-dock button')) {
102+
const buttons = [...this.parentElement!.querySelectorAll('button')]
103+
const index = buttons.indexOf(this as HTMLButtonElement)
104+
return new DOMRect(100 + index * 54, 700, 48, 53)
105+
}
106+
return originalRect.call(this)
107+
}
108+
const pointer = (
109+
type: string,
110+
clientX: number,
111+
pointerType = 'mouse',
112+
relatedTarget?: EventTarget,
113+
) => {
114+
const event = new MouseEvent(type, {
115+
bubbles: true,
116+
clientX,
117+
relatedTarget,
118+
})
119+
Object.defineProperty(event, 'pointerType', { value: pointerType })
120+
return event
121+
}
122+
const host = document.createElement('div')
123+
document.body.append(host)
124+
const root = createRoot(host)
125+
126+
try {
127+
await act(async () => {
128+
root.render(<DesktopShell {...turkishShellProps} apps={floatingDockApps} />)
129+
})
130+
const dock = host.querySelector<HTMLElement>('.detective-dock')!
131+
const shelf = host.querySelector<HTMLElement>('.detective-dock__apps')!
132+
const buttons = [...host.querySelectorAll<HTMLButtonElement>('.detective-dock button')]
133+
134+
await act(async () => dock.dispatchEvent(pointer('pointermove', 178)))
135+
136+
const scales = buttons.map((button) => Number(button.style.getPropertyValue('--dock-scale')))
137+
expect(scales[1]).toBeCloseTo(1.52, 2)
138+
expect(scales[0]).toBeGreaterThan(1)
139+
expect(scales[0]).toBeLessThan(scales[1]!)
140+
expect(scales[2]).toBeCloseTo(scales[0]!, 2)
141+
expect(Number.parseFloat(buttons[0]!.style.getPropertyValue('--dock-shift'))).toBeLessThan(0)
142+
expect(Number.parseFloat(buttons[2]!.style.getPropertyValue('--dock-shift'))).toBeGreaterThan(0)
143+
expect(Number.parseFloat(shelf.style.getPropertyValue('--dock-side-expansion'))).toBeGreaterThan(0)
144+
expect(shelf.classList.contains('is-magnifying')).toBe(true)
145+
146+
await act(async () => dock.dispatchEvent(pointer('pointerout', 400, 'mouse', document.body)))
147+
expect(buttons[1]!.style.getPropertyValue('--dock-scale')).toBe('')
148+
expect(shelf.style.getPropertyValue('--dock-side-expansion')).toBe('')
149+
expect(shelf.classList.contains('is-magnifying')).toBe(false)
150+
151+
await act(async () => dock.dispatchEvent(pointer('pointermove', 178, 'touch')))
152+
expect(buttons[1]!.style.getPropertyValue('--dock-scale')).toBe('')
153+
} finally {
154+
await act(async () => root.unmount())
155+
host.remove()
156+
HTMLElement.prototype.getBoundingClientRect = originalRect
157+
}
158+
})
159+
})
160+
23161
describe('DesktopShell fixed docks', () => {
24162
it('keeps a non-closable right dock open and omits all window controls', () => {
25163
const persistedClosedLayout: DesktopLayoutSnapshot = {
@@ -320,6 +458,7 @@ describe('DesktopShell fixed docks', () => {
320458
{ ...dockedApp, id: 'floating-app', placement: 'floating', defaultOpen: true },
321459
{ ...dockedApp, defaultOpen: true },
322460
]}
461+
desktopItems={desktopFiles}
323462
layoutPersistence={{ load: () => persistedOverlayLayout }}
324463
settingsSlot={<button type="button">Setting action</button>}
325464
/>,
@@ -370,8 +509,8 @@ describe('DesktopShell fixed docks', () => {
370509
})
371510
expect(panel?.style.left).toBe('558px')
372511

373-
const desktopShortcut = host.querySelector<HTMLButtonElement>('.detective-desktop__shortcut')
374-
await act(async () => desktopShortcut?.click())
512+
const desktopItem = host.querySelector<HTMLButtonElement>('.detective-desktop__item')
513+
await act(async () => desktopItem?.click())
375514
expect(host.querySelector('.detective-settings-panel')).not.toBeNull()
376515
expect(panel?.style.left).toBe('558px')
377516

0 commit comments

Comments
 (0)