Skip to content
Closed
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
9 changes: 9 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
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) 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.250.0) stable; urgency=medium

* DALI: host capabilities for embedding hosts — the syslog toggle and the MQTT id
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
3 changes: 3 additions & 0 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,9 @@
"monitor-col-command": "Command",
"monitor-col-response": "Response",
"monitor-foreign": "foreign",
"all-controls": "All controls",
"state-on": "on",
"state-off": "off",
"monitor-broadcast": "Broadcast"
},
"buttons": {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,9 @@
"monitor-col-command": "Команда",
"monitor-col-response": "Ответ",
"monitor-foreign": "сторонний",
"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 @@ -13,6 +13,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 @@ -119,7 +121,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 @@ -137,12 +139,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
Loading