Compare commits

...

8 Commits

Author SHA1 Message Date
Wendelin
188e82fa02 Merge branch 'dev' of github.com:home-assistant/frontend into user-siderbar 2025-02-24 09:58:13 +01:00
Wendelin
2fba41b8ca Update translations 2025-02-19 14:06:34 +01:00
Wendelin
25a14c87a5 Delay skeleton loading to prevent flickering 2025-02-18 15:18:53 +01:00
Wendelin
18b2360e46 Fix memoize function 2025-02-18 06:29:17 +01:00
Wendelin
c1a214d1af Update code format 2025-02-17 15:56:23 +01:00
Wendelin
6123d932e1 Add loading skeleton 2025-02-17 15:49:05 +01:00
Wendelin
a3dcf77f2a Add profile settings for device sidebar 2025-02-17 15:16:34 +01:00
Wendelin
7d7f8a9bc2 Use user preferences for sidebar 2025-02-17 12:56:20 +01:00
6 changed files with 817 additions and 493 deletions

View File

@@ -90,6 +90,7 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@shoelace-style/shoelace": "2.20.0",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.6.5",
"@vaadin/vaadin-themable-mixin": "24.6.5",

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@shoelace-style/shoelace/dist/components/skeleton/skeleton";
import {
mdiBell,
mdiCalendar,
@@ -49,6 +50,10 @@ import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
import { preventDefault } from "../common/dom/prevent_default";
import {
saveSidebarPreferences,
subscribeSidebarPreferences,
} from "../data/sidebar";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@@ -207,30 +212,40 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _panelOrder: string[] = [];
@storage({ key: "sidebarPanelOrder", state: true, subscribe: true })
private _devicePanelOrder?: string[];
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _hiddenPanels: string[] = [];
@storage({ key: "sidebarHiddenPanels", state: true, subscribe: true })
private _deviceHiddenPanels?: string[];
@state()
private _userPanelOrder: string[] = [];
@state()
private _userHiddenPanels: string[] = [];
@state()
private _loadingUserPreferences = true;
public hassSubscribe(): UnsubscribeFunc[] {
return this.hass.user?.is_admin
? [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
}),
]
: [];
const subscribeFunctions = [
subscribeSidebarPreferences(this.hass, (sidebar) => {
this._userPanelOrder = sidebar?.panelOrder || [];
this._userHiddenPanels = sidebar?.hiddenPanels || [];
this._loadingUserPreferences = false;
}),
];
if (this.hass.user?.is_admin) {
subscribeFunctions.push(
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
})
);
}
return subscribeFunctions;
}
protected render() {
@@ -260,8 +275,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") ||
changedProps.has("_notifications") ||
changedProps.has("_hiddenPanels") ||
changedProps.has("_panelOrder")
changedProps.has("_devicePanelOrder") ||
changedProps.has("_deviceHiddenPanels") ||
changedProps.has("_userPanelOrder") ||
changedProps.has("_userHiddenPanels")
) {
return true;
}
@@ -381,12 +398,51 @@ class HaSidebar extends SubscribeMixin(LitElement) {
</div>`;
}
private _getPanelPreferencesMemoized = memoizeOne(
(
userPanelOrder: string[],
userHiddenPanels: string[],
userPreferencesLoading: boolean,
devicePanelOrder?: string[],
deviceHiddenPanels?: string[]
): { panelOrder: string[]; hiddenPanels: string[]; loading: boolean } => {
let panelOrder = userPanelOrder ?? [];
let hiddenPanels = userHiddenPanels ?? [];
let loading = userPreferencesLoading;
if (devicePanelOrder || deviceHiddenPanels) {
panelOrder = devicePanelOrder ?? [];
hiddenPanels = deviceHiddenPanels ?? [];
loading = false;
}
return {
panelOrder,
hiddenPanels,
loading,
};
}
);
private _getPanelPreferences() {
return this._getPanelPreferencesMemoized(
this._userPanelOrder,
this._userHiddenPanels,
this._loadingUserPreferences,
this._devicePanelOrder,
this._deviceHiddenPanels
);
}
private _renderAllPanels() {
const { panelOrder, hiddenPanels, loading } = this._getPanelPreferences();
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
panelOrder,
hiddenPanels,
this.hass.locale
);
@@ -407,12 +463,21 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@keydown=${this._listboxKeydown}
@iron-activate=${preventDefault}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
${loading ? html`
<div class="loading">
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
</div>
` : html`
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
`}
</paper-listbox>
`;
}
@@ -474,23 +539,49 @@ class HaSidebar extends SubscribeMixin(LitElement) {
`;
}
private async _setPanelOrder(panelOrder: string[]) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
this._devicePanelOrder = [...panelOrder];
} else {
this._userPanelOrder = [...panelOrder];
await saveSidebarPreferences(this.hass, {
panelOrder: panelOrder,
hiddenPanels: this._userHiddenPanels,
});
}
}
private async _setHiddenPanels(hiddenPanels: string[]) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
this._deviceHiddenPanels = hiddenPanels;
} else {
this._userHiddenPanels = hiddenPanels;
await saveSidebarPreferences(this.hass, {
panelOrder: this._userPanelOrder,
hiddenPanels: hiddenPanels,
});
}
}
private _panelMoved(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
const [beforeSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
panelOrder,
hiddenPanels!,
this.hass.locale
);
const panelOrder = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrder.splice(oldIndex, 1)[0];
panelOrder.splice(newIndex, 0, panel);
const panelOrderNew = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrderNew.splice(oldIndex, 1)[0];
panelOrderNew.splice(newIndex, 0, panel);
this._panelOrder = panelOrder;
this._setPanelOrder(panelOrderNew);
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
@@ -507,8 +598,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private _renderHiddenPanels() {
return html`${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => {
const { hiddenPanels } = this._getPanelPreferences();
return html`${hiddenPanels.length
? html`${hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
return "";
@@ -690,9 +783,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
}
private get _tooltip() {
@@ -730,21 +821,25 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private async _hidePanel(ev: Event) {
ev.preventDefault();
const panel = (ev.currentTarget as any).panel;
if (this._hiddenPanels.includes(panel)) {
if ((this._deviceHiddenPanels || this._userHiddenPanels).includes(panel)) {
return;
}
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
// Make a copy for Memoize
this._hiddenPanels = [...this._hiddenPanels, panel];
this._setHiddenPanels([...hiddenPanels, panel]);
// Remove it from the panel order
this._panelOrder = this._panelOrder.filter((order) => order !== panel);
this._setPanelOrder(panelOrder.filter((order) => order !== panel));
}
private async _unhidePanel(ev: Event) {
ev.preventDefault();
const panel = (ev.currentTarget as any).panel;
this._hiddenPanels = this._hiddenPanels.filter(
(hidden) => hidden !== panel
);
const { hiddenPanels } = this._getPanelPreferences();
this._setHiddenPanels(hiddenPanels.filter((hidden) => hidden !== panel));
}
private _itemMouseEnter(ev: MouseEvent) {
@@ -784,9 +879,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip();
}
@eventOptions({
passive: true,
})
@eventOptions({ passive: true })
private _listboxScroll() {
// On keypresses on the listbox, we're going to ignore scroll events
// for 100ms so that if pressing down arrow scrolls the sidebar, the tooltip
@@ -1117,6 +1210,60 @@ class HaSidebar extends SubscribeMixin(LitElement) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
@keyframes skeletonAnimate {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes contentAnimate {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.loading {
opacity: 0;
animation-name: skeletonAnimate;
animation-duration: 2000ms;
animation-delay: 0;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
sl-skeleton {
--border-radius: 8px;
height: 24px;
--color: var(--outline-color);
--sheen-color: var(--outline-hover-color);
}
sl-skeleton:nth-child(2) {
width: 70%;
}
sl-skeleton:nth-child(3) {
width: 30%;
}
sl-skeleton:nth-child(4) {
width: 90%;
}
`,
];
}

33
src/data/sidebar.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { HomeAssistant } from "../types";
import {
fetchFrontendUserData,
saveFrontendUserData,
subscribeFrontendUserData,
} from "./frontend";
export const SIDEBAR_PREFERENCES_KEY = "sidebar";
export interface SidebarPreferences {
panelOrder?: string[];
hiddenPanels?: string[];
}
declare global {
interface FrontendUserData {
sidebar?: SidebarPreferences;
}
}
export const fetchSidebarPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY);
export const saveSidebarPreferences = (
hass: HomeAssistant,
data: SidebarPreferences
) => saveFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, data);
export const subscribeSidebarPreferences = (
hass: HomeAssistant,
callback: (sidebar?: SidebarPreferences | null) => void
) =>
subscribeFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, callback);

View File

@@ -1,10 +1,11 @@
import "@material/mwc-button";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-card";
import "../../components/ha-button";
import "../../components/ha-expansion-panel";
import "../../layouts/hass-tabs-subpage";
import { profileSections } from "./ha-panel-profile";
import { isExternal } from "../../data/external";
@@ -27,6 +28,9 @@ import "./ha-pick-time-zone-row";
import "./ha-push-notifications-row";
import "./ha-set-suspend-row";
import "./ha-set-vibrate-row";
import { storage } from "../../common/decorators/storage";
import type { HaSwitch } from "../../components/ha-switch";
import { fetchSidebarPreferences } from "../../data/sidebar";
@customElement("ha-profile-section-general")
class HaProfileSectionGeneral extends LitElement {
@@ -38,6 +42,20 @@ class HaProfileSectionGeneral extends LitElement {
@property({ attribute: false }) public route!: Route;
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _devicePanelOrder?: string[];
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _deviceHiddenPanels?: string[];
private _unsubCoreData?: UnsubscribeFunc;
private _getCoreData() {
@@ -71,6 +89,9 @@ class HaProfileSectionGeneral extends LitElement {
}
protected render(): TemplateResult {
const deviceSidebarSettingsEnabled =
!!this._devicePanelOrder || !!this._deviceHiddenPanels;
return html`
<hass-tabs-subpage
main-page
@@ -91,9 +112,9 @@ class HaProfileSectionGeneral extends LitElement {
: ""}
</div>
<div class="card-actions">
<mwc-button class="warning" @click=${this._handleLogOut}>
<ha-button class="warning" @click=${this._handleLogOut}>
${this.hass.localize("ui.panel.profile.logout")}
</mwc-button>
</ha-button>
</div>
</ha-card>
<ha-card
@@ -128,6 +149,29 @@ class HaProfileSectionGeneral extends LitElement {
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-first-weekday-row>
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
)}
</span>
<span
slot="description"
class=${deviceSidebarSettingsEnabled ? "device-info" : ""}
>
${this.hass.localize(
`ui.panel.profile.customize_sidebar.${!deviceSidebarSettingsEnabled ? "description" : "overwritten_by_device"}`
)}
</span>
<ha-button
.disabled=${deviceSidebarSettingsEnabled}
@click=${this._customizeSidebar}
>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</ha-button>
</ha-settings-row>
${this.hass.user!.is_admin
? html`
<ha-advanced-mode-row
@@ -159,20 +203,48 @@ class HaProfileSectionGeneral extends LitElement {
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
"ui.panel.profile.customize_sidebar.device_specific_header"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.description"
"ui.panel.profile.customize_sidebar.device_description"
)}
</span>
<mwc-button @click=${this._customizeSidebar}>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</mwc-button>
<ha-switch
.checked=${deviceSidebarSettingsEnabled}
@change=${this._toggleDeviceSidebarPreferences}
></ha-switch>
</ha-settings-row>
${deviceSidebarSettingsEnabled
? html`
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.profile.customize_sidebar.device_specific_header"
)}
expanded
>
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.description"
)}
</span>
<ha-button @click=${this._customizeSidebar}>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</ha-button>
</ha-settings-row>
</ha-expansion-panel>
`
: nothing}
${this.hass.dockedSidebar !== "auto" || !this.narrow
? html`
<ha-force-narrow-row
@@ -215,6 +287,38 @@ class HaProfileSectionGeneral extends LitElement {
fireEvent(this, "hass-edit-sidebar", { editMode: true });
}
private async _toggleDeviceSidebarPreferences(ev: Event) {
const switchElement = ev.target as HaSwitch;
const enabled = switchElement.checked;
if (!enabled) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.profile.customize_sidebar.delete_device_preferences_header"
),
text: this.hass.localize(
"ui.panel.profile.customize_sidebar.delete_device_preferences_description"
),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
});
if (confirm) {
this._devicePanelOrder = undefined;
this._deviceHiddenPanels = undefined;
} else {
// revert switch
switchElement.click();
}
}
} else {
const sidebarPreferences = await fetchSidebarPreferences(this.hass);
this._devicePanelOrder = sidebarPreferences?.panelOrder ?? [];
this._deviceHiddenPanels = sidebarPreferences?.hiddenPanels ?? [];
}
}
private _handleLogOut() {
showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.profile.logout_title"),
@@ -251,6 +355,14 @@ class HaProfileSectionGeneral extends LitElement {
text-align: center;
color: var(--secondary-text-color);
}
ha-expansion-panel {
margin: 0 8px 8px;
}
.device-info {
color: var(--warning-color);
}
`,
];
}

View File

@@ -7526,9 +7526,14 @@
"description": "This will hide the sidebar by default, similar to the mobile experience."
},
"customize_sidebar": {
"header": "Change the order and hide items from the sidebar",
"header": "Edit sidebar",
"description": "You can also press and hold the header of the sidebar to activate edit mode.",
"button": "Edit"
"overwritten_by_device": "Your user sidebar preferences are overwritten by device-specific preferences.",
"device_description": "Enable specific sidebar preferences for this device.",
"button": "Edit",
"delete_device_preferences_header": "Delete device preferences",
"delete_device_preferences_description": "This will delete sidebar order and hidden items on this device and will show user-defined preferences again.",
"device_specific_header": "Device-specific sidebar"
},
"vibrate": {
"header": "Vibrate",

884
yarn.lock

File diff suppressed because it is too large Load Diff