Skip to content
Open
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
11 changes: 11 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
wb-mqtt-homeui (2.250.4) stable; urgency=medium

* DALI: live device controls on device, group and bus pages — a featured strip
of the controls one reaches for (level, colour, temperature, on/off), each
named by its quantity and showing the value read back from the device
beside it, with the rest behind "All controls"; the page toolbar names what
is open, which the hidden tree no longer does on mobile, and its buttons
wrap on phones

-- Wiren Board Robot <info@wirenboard.com> Mon, 31 Aug 2026 20:40:00 +0300

wb-mqtt-homeui (2.249.3) stable; urgency=medium

* Fix autologin being switched off when the autologin user is edited
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen } from '@testing-library/react';
import { CollapsiblePanel } from './collapsible-panel';

describe('CollapsiblePanel: the whole title row toggles, exactly once per click', () => {
it('clicking the title text collapses and expands the body', () => {
render(<CollapsiblePanel title="Broadcast settings"><div>body</div></CollapsiblePanel>);
expect(screen.getByText('body')).toBeTruthy();
fireEvent.click(screen.getByText('Broadcast settings'));
expect(screen.queryByText('body')).toBeNull();
fireEvent.click(screen.getByText('Broadcast settings'));
expect(screen.getByText('body')).toBeTruthy();
});

it('the toggle row is not a <label> — Chrome forwards label clicks into the button and toggles twice', () => {
const { container } = render(<CollapsiblePanel title="T">x</CollapsiblePanel>);
expect(container.querySelector('label.collapsiblePanel-label')).toBeNull();
expect(container.querySelector('div.collapsiblePanel-label')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ export const CollapsiblePanel = ({ title, isCollapsed = false, children }) => {

return (
<section className="collapsiblePanel-container">
<label className="collapsiblePanel-label">
{/* Not a <label>: a label forwards clicks on its text to the button, but
Chrome also forwards a click that lands on the button's own icon, so a
click on the chevron toggled twice — i.e. did nothing. One handler on
the row covers text, empty space, and the button (keyboard included:
Enter/Space on the button fire a click that bubbles here). */}
<div className="collapsiblePanel-label" onClick={() => setCollapsed(!collapsed)}>
<Button
className="collapsiblePanel-button"
variant="secondary"
Expand All @@ -21,10 +26,9 @@ export const CollapsiblePanel = ({ title, isCollapsed = false, children }) => {
aria-labelledby={titleId}
aria-label={collapsed ? t('common.buttons.expand') : t('common.buttons.collapse')}
icon={collapsed ? <ChevronRightIcon /> : <ChevronDownIcon />}
onClick={() => setCollapsed(!collapsed)}
/>
<span id={titleId}>{title}</span>
</label>
</div>
{!collapsed && <div>{children}</div>}
</section>
);
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,19 @@
"monitor-col-command": "Command",
"monitor-col-response": "Response",
"monitor-foreign": "foreign",
"setpoint": {
"wanted_level": "Brightness",
"set_rgb": "Colour",
"set_white": "White",
"set_colour_temperature": "Colour temperature",
"set_x_coordinate": "X coordinate",
"set_y_coordinate": "Y coordinate",
"set_primary_n": "Primary N{{n}}"
},
"readback-tooltip": "Value read back from the device",
"all-controls": "All controls",
"state-on": "on",
"state-off": "off",
"monitor-broadcast": "Broadcast"
},
"buttons": {
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,19 @@
"monitor-col-command": "Команда",
"monitor-col-response": "Ответ",
"monitor-foreign": "сторонний",
"setpoint": {
"wanted_level": "Яркость",
"set_rgb": "Цвет",
"set_white": "Белый",
"set_colour_temperature": "Цветовая температура",
"set_x_coordinate": "Координата X",
"set_y_coordinate": "Координата Y",
"set_primary_n": "Основной N{{n}}"
},
"readback-tooltip": "Значение, прочитанное из устройства",
"all-controls": "Все элементы управления",
"state-on": "вкл",
"state-off": "выкл",
"monitor-broadcast": "Широковещательные"
},
"buttons": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,13 @@
height: 180px;
}

.dali-busCommands-catalogButton .button-text {
display: none;
}

.dali-busCommands-results .wb-tableWrapper {
overflow-x: visible;
}
}

/* At phone widths the flex row squeezed the catalog button into a label-less
pill; buttons here keep their intrinsic width and wrap instead. */
.dali-busCommands-toolbar .button {
flex-shrink: 0;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { observer } from 'mobx-react-lite';
import { type CSSProperties } from 'react';
import { type CSSProperties, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/button';
import { Card } from '@/components/card';
Expand All @@ -12,6 +12,8 @@ import type { ObjectParamStore } from '@/stores/json-schema-editor/object-store'
import { useAsyncAction } from '@/utils/async-action';
import { BusCommands } from '../bus-commands';
import { BusToggle } from '../bus-toggle';
import { DeviceControls } from '../device-controls';
import { TabToolbar } from '../tab-toolbar';
import { CommissioningErrorBanner } from './commissioning-error-banner';
import { CommissioningProgress } from './commissioning-progress';
import { PollingIntervalField } from './polling-interval-field';
Expand Down Expand Up @@ -118,7 +120,7 @@ const BusParamsTabContent = observer(({ store }: { store: BusStore }) => {
);
});

export const BusTabContent = observer(({ store }: { store: BusStore }) => {
export const BusTabContent = observer(({ store, title }: { store: BusStore; title?: ReactNode }) => {
const { t } = useTranslation();

if (store.isLoading) {
Expand All @@ -136,12 +138,18 @@ export const BusTabContent = observer(({ store }: { store: BusStore }) => {
) : (
<>
<CommissioningErrorBanner store={store} />
<FormButtonGroup>
<Button
label={t('dali.buttons.rescan')}
onClick={() => store.scan()}
/>
</FormButtonGroup>
<TabToolbar title={title}>
<FormButtonGroup>
<Button
label={t('dali.buttons.rescan')}
onClick={() => store.scan()}
/>
</FormButtonGroup>
</TabToolbar>
{/* The daemon publishes the bus's broadcast commands as a virtual
device of their own — every lamp at once, which is what one
wants to try first after a scan. */}
<DeviceControls mqttId={`${store.id}_broadcast`} />
<PollingIntervalField store={store} />
<BusParamsTabContent store={store} />
<BusCommands store={store.commands} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @vitest-environment happy-dom
import type Cell from '@/stores/devices/cell';
import { render, screen } from '@/test/render';
import { ReadbackSuffix } from './device-controls';

vi.mock('@/services', () => import('@/test/mocks/services'));

/** The slice of Cell the readback suffix reads. */
const cell = (controlId: string, name: string, type: string, value: unknown, units?: string): Cell =>
({ id: `dev/${controlId}`, controlId, name, type, value, units, readOnly: true } as unknown as Cell);

describe('ReadbackSuffix', () => {
it('explains the bracketed reading in a tooltip, since the slot is now named for the quantity', () => {
render(<ReadbackSuffix cell={cell('actual_level', 'Actual Level', 'value', 42, '%')} />);
const readback = screen.getByTitle('dali.labels.readback-tooltip');
expect(readback).toHaveTextContent('(42 %)');
});

it('keeps the colour value in the swatch tooltip, which the swatch itself cannot show', () => {
render(<ReadbackSuffix cell={cell('current_rgb', 'Current RGB', 'rgb', '#ff8800')} />);
expect(screen.getByTitle('dali.labels.readback-tooltip: #ff8800')).toBeInTheDocument();
});
});
Loading