Edit sidebar in a dialog (#25532)

This commit is contained in:
Wendelin 2025-05-21 13:42:43 +02:00 committed by GitHub
parent f0beef22d2
commit 32b3c83337
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 264 additions and 280 deletions

View File

@ -25,6 +25,7 @@ export interface DisplayItem {
value: string; value: string;
label: string; label: string;
description?: string; description?: string;
disableSorting?: boolean;
} }
export interface DisplayValue { export interface DisplayValue {
@ -50,6 +51,9 @@ export class HaItemDisplayEditor extends LitElement {
@property({ type: Boolean, attribute: "show-navigation-button" }) @property({ type: Boolean, attribute: "show-navigation-button" })
public showNavigationButton = false; public showNavigationButton = false;
@property({ type: Boolean, attribute: "dont-sort-visible" })
public dontSortVisible = false;
@property({ attribute: false }) @property({ attribute: false })
public value: DisplayValue = { public value: DisplayValue = {
order: [], order: [],
@ -122,9 +126,15 @@ export class HaItemDisplayEditor extends LitElement {
private _visibleItems = memoizeOne( private _visibleItems = memoizeOne(
(items: DisplayItem[], hidden: string[], order: string[]) => { (items: DisplayItem[], hidden: string[], order: string[]) => {
const compare = orderCompare(order); const compare = orderCompare(order);
return items
.filter((item) => !hidden.includes(item.value)) const visibleItems = items.filter((item) => !hidden.includes(item.value));
.sort((a, b) => compare(a.value, b.value)); if (this.dontSortVisible) {
return visibleItems;
}
return items.sort((a, b) =>
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
);
} }
); );
@ -160,7 +170,14 @@ export class HaItemDisplayEditor extends LitElement {
(item) => item.value, (item) => item.value,
(item: DisplayItem, _idx) => { (item: DisplayItem, _idx) => {
const isVisible = !this.value.hidden.includes(item.value); const isVisible = !this.value.hidden.includes(item.value);
const { label, value, description, icon, iconPath } = item; const {
label,
value,
description,
icon,
iconPath,
disableSorting,
} = item;
return html` return html`
<ha-md-list-item <ha-md-list-item
type=${ifDefined( type=${ifDefined(
@ -172,14 +189,14 @@ export class HaItemDisplayEditor extends LitElement {
.value=${value} .value=${value}
class=${classMap({ class=${classMap({
hidden: !isVisible, hidden: !isVisible,
draggable: isVisible, draggable: isVisible && !disableSorting,
})} })}
> >
<span slot="headline">${label}</span> <span slot="headline">${label}</span>
${description ${description
? html`<span slot="supporting-text">${description}</span>` ? html`<span slot="supporting-text">${description}</span>`
: nothing} : nothing}
${isVisible ${isVisible && !disableSorting
? html` ? html`
<ha-svg-icon <ha-svg-icon
class="handle" class="handle"

View File

@ -1,11 +1,9 @@
import "@material/mwc-button/mwc-button";
import { import {
mdiBell, mdiBell,
mdiCalendar, mdiCalendar,
mdiCellphoneCog, mdiCellphoneCog,
mdiChartBox, mdiChartBox,
mdiClipboardList, mdiClipboardList,
mdiClose,
mdiCog, mdiCog,
mdiFormatListBulletedType, mdiFormatListBulletedType,
mdiHammer, mdiHammer,
@ -13,12 +11,11 @@ import {
mdiMenu, mdiMenu,
mdiMenuOpen, mdiMenuOpen,
mdiPlayBoxMultiple, mdiPlayBoxMultiple,
mdiPlus,
mdiTooltipAccount, mdiTooltipAccount,
mdiViewDashboard, mdiViewDashboard,
} from "@mdi/js"; } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResult, CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { import {
customElement, customElement,
@ -29,7 +26,6 @@ import {
} from "lit/decorators"; } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute"; import { toggleAttribute } from "../common/dom/toggle_attribute";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
@ -40,6 +36,7 @@ import { subscribeNotifications } from "../data/persistent_notification";
import { subscribeRepairsIssueRegistry } from "../data/repairs"; import { subscribeRepairsIssueRegistry } from "../data/repairs";
import type { UpdateEntity } from "../data/update"; import type { UpdateEntity } from "../data/update";
import { updateCanInstall } from "../data/update"; import { updateCanInstall } from "../data/update";
import { showEditSidebarDialog } from "../dialogs/sidebar/show-dialog-edit-sidebar";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
@ -49,8 +46,6 @@ import "./ha-icon-button";
import "./ha-md-list"; import "./ha-md-list";
import "./ha-md-list-item"; import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item"; import type { HaMdListItem } from "./ha-md-list-item";
import "./ha-menu-button";
import "./ha-sortable";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
@ -67,7 +62,7 @@ const SORT_VALUE_URL_PATHS = {
config: 11, config: 11,
}; };
const PANEL_ICONS = { export const PANEL_ICONS = {
calendar: mdiCalendar, calendar: mdiCalendar,
"developer-tools": mdiHammer, "developer-tools": mdiHammer,
energy: mdiLightningBolt, energy: mdiLightningBolt,
@ -140,7 +135,7 @@ const defaultPanelSorter = (
return stringCompare(a.title!, b.title!, language); return stringCompare(a.title!, b.title!, language);
}; };
const computePanels = memoizeOne( export const computePanels = memoizeOne(
( (
panels: HomeAssistant["panels"], panels: HomeAssistant["panels"],
defaultPanel: HomeAssistant["defaultPanel"], defaultPanel: HomeAssistant["defaultPanel"],
@ -192,8 +187,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@property({ attribute: "always-expand", type: Boolean }) @property({ attribute: "always-expand", type: Boolean })
public alwaysExpand = false; public alwaysExpand = false;
@property({ attribute: "edit-mode", type: Boolean }) @property({ attribute: false })
public editMode = false; public panelOrder!: string[];
@property({ attribute: false })
public hiddenPanels!: string[];
@state() private _notifications?: PersistentNotification[]; @state() private _notifications?: PersistentNotification[];
@ -207,26 +205,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _recentKeydownActiveUntil = 0; private _recentKeydownActiveUntil = 0;
private _editStyleLoaded = false;
private _unsubPersistentNotifications: UnsubscribeFunc | undefined; private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@state()
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _panelOrder: string[] = [];
@state()
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _hiddenPanels: string[] = [];
@query(".tooltip") private _tooltip!: HTMLDivElement; @query(".tooltip") private _tooltip!: HTMLDivElement;
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
@ -270,13 +250,12 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("expanded") || changedProps.has("expanded") ||
changedProps.has("narrow") || changedProps.has("narrow") ||
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("editMode") ||
changedProps.has("_externalConfig") || changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") || changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") || changedProps.has("_issuesCount") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("_hiddenPanels") || changedProps.has("hiddenPanels") ||
changedProps.has("_panelOrder") changedProps.has("panelOrder")
) { ) {
return true; return true;
} }
@ -322,9 +301,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (changedProps.has("alwaysExpand")) { if (changedProps.has("alwaysExpand")) {
toggleAttribute(this, "expanded", this.alwaysExpand); toggleAttribute(this, "expanded", this.alwaysExpand);
} }
if (changedProps.has("editMode") && this.editMode) {
this._editModeActivated();
}
if (!changedProps.has("hass")) { if (!changedProps.has("hass")) {
return; return;
} }
@ -374,8 +350,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
class="menu" class="menu"
@action=${this._handleAction} @action=${this._handleAction}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
hasHold: !this.editMode, hasHold: true,
disabled: this.editMode,
})} })}
> >
${!this.narrow ${!this.narrow
@ -389,11 +364,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
></ha-icon-button> ></ha-icon-button>
` `
: ""} : ""}
${this.editMode <div class="title">Home Assistant</div>
? html`<mwc-button outlined @click=${this._closeEditMode}>
${this.hass.localize("ui.sidebar.done")}
</mwc-button>`
: html`<div class="title">Home Assistant</div>`}
</div>`; </div>`;
} }
@ -401,14 +372,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
const [beforeSpacer, afterSpacer] = computePanels( const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels, this.hass.panels,
this.hass.defaultPanel, this.hass.defaultPanel,
this._panelOrder, this.panelOrder,
this._hiddenPanels, this.hiddenPanels,
this.hass.locale this.hass.locale
); );
// prettier-ignore // prettier-ignore
return html` return html`
<ha-sortable .disabled=${!this.editMode} draggable-selector=".draggable" @item-moved=${this._panelMoved}>
<ha-md-list <ha-md-list
class="ha-scrollbar" class="ha-scrollbar"
@focusin=${this._listboxFocusIn} @focusin=${this._listboxFocusIn}
@ -416,22 +386,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@scroll=${this._listboxScroll} @scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown} @keydown=${this._listboxKeydown}
> >
${this.editMode ${this._renderPanels(beforeSpacer, selectedPanel)}
? this._renderPanelsEdit(beforeSpacer, selectedPanel)
: this._renderPanels(beforeSpacer, selectedPanel)}
${this._renderSpacer()} ${this._renderSpacer()}
${this._renderPanels(afterSpacer, selectedPanel)} ${this._renderPanels(afterSpacer, selectedPanel)}
${this._renderExternalConfiguration()} ${this._renderExternalConfiguration()}
</ha-md-list> </ha-md-list>
</ha-sortable>
`; `;
} }
private _renderPanels( private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
panels: PanelInfo[],
selectedPanel: string,
sortable = false
) {
return panels.map((panel) => return panels.map((panel) =>
this._renderPanel( this._renderPanel(
panel.url_path, panel.url_path,
@ -444,36 +407,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
: panel.url_path in PANEL_ICONS : panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path] ? PANEL_ICONS[panel.url_path]
: undefined, : undefined,
selectedPanel, selectedPanel
sortable
) )
); );
} }
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
return html`
${this._renderPanels(beforeSpacer, selectedPanel, true)}
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
}
private _renderPanel( private _renderPanel(
urlPath: string, urlPath: string,
title: string | null, title: string | null,
icon: string | null | undefined, icon: string | null | undefined,
iconPath: string | null | undefined, iconPath: string | null | undefined,
selectedPanel: string, selectedPanel: string
sortable = false
) { ) {
return urlPath === "config" return urlPath === "config"
? this._renderConfiguration(title, selectedPanel) ? this._renderConfiguration(title, selectedPanel)
: html` : html`
<ha-md-list-item <ha-md-list-item
.href=${this.editMode ? undefined : `/${urlPath}`} .href=${`/${urlPath}`}
type="link" type="link"
class=${classMap({ class=${classMap({
selected: selectedPanel === urlPath, selected: selectedPanel === urlPath,
draggable: this.editMode && sortable,
})} })}
@mouseenter=${this._itemMouseEnter} @mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave} @mouseleave=${this._itemMouseLeave}
@ -482,81 +435,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>` ? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`} : html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
<span class="item-text" slot="headline">${title}</span> <span class="item-text" slot="headline">${title}</span>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
slot="end"
></ha-icon-button>`
: nothing}
</ha-md-list-item> </ha-md-list-item>
`; `;
} }
private _panelMoved(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const [beforeSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
this.hass.locale
);
const panelOrder = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrder.splice(oldIndex, 1)[0];
panelOrder.splice(newIndex, 0, panel);
this._panelOrder = panelOrder;
}
private _renderHiddenPanels() {
return html`${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
return "";
}
return html`<ha-md-list-item
@click=${this._unhidePanel}
class="hidden-panel"
.panel=${url}
type="button"
>
${panel.url_path === this.hass.defaultPanel && !panel.icon
? html`<ha-svg-icon
slot="start"
.path=${PANEL_ICONS.lovelace}
></ha-svg-icon>`
: panel.url_path in PANEL_ICONS
? html`<ha-svg-icon
slot="start"
.path=${PANEL_ICONS[panel.url_path]}
></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${panel.icon}></ha-icon>`}
<span class="item-text" slot="headline"
>${panel.url_path === this.hass.defaultPanel
? this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) ||
panel.title}</span
>
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.show_panel")}
.path=${mdiPlus}
class="show-panel"
slot="end"
></ha-icon-button>
</ha-md-list-item>`;
})}
${this._renderSpacer()}`
: ""}`;
}
private _renderDivider() { private _renderDivider() {
return html`<div class="divider"></div>`; return html`<div class="divider"></div>`;
} }
@ -677,48 +559,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return; return;
} }
fireEvent(this, "hass-edit-sidebar", { editMode: true }); showEditSidebarDialog(this, {
saveCallback: this._saveSidebar,
});
} }
private async _editModeActivated() { private _saveSidebar = (order: string[], hidden: string[]) => {
await this._loadEditStyle(); fireEvent(this, "hass-edit-sidebar", {
} order,
hidden,
private async _loadEditStyle() { });
if (this._editStyleLoaded) return; };
const editStylesImport = await import("../resources/ha-sidebar-edit-style");
const style = document.createElement("style");
style.innerHTML = (editStylesImport.sidebarEditStyle as CSSResult).cssText;
this.shadowRoot!.appendChild(style);
await this.updateComplete;
}
private _closeEditMode() {
fireEvent(this, "hass-edit-sidebar", { editMode: false });
}
private async _hidePanel(ev: Event) {
ev.preventDefault();
const panel = (ev.currentTarget as any).panel;
if (this._hiddenPanels.includes(panel)) {
return;
}
// Make a copy for Memoize
this._hiddenPanels = [...this._hiddenPanels, panel];
// Remove it from the panel order
this._panelOrder = this._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
);
}
private _itemMouseEnter(ev: MouseEvent) { private _itemMouseEnter(ev: MouseEvent) {
// On keypresses on the listbox, we're going to ignore mouse enter events // On keypresses on the listbox, we're going to ignore mouse enter events
@ -875,12 +726,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
:host([expanded]) .title { :host([expanded]) .title {
display: initial; display: initial;
} }
:host([expanded]) .menu mwc-button {
margin: 0 8px;
}
.menu mwc-button {
width: 100%;
}
.hidden-panel { .hidden-panel {
display: none; display: none;
} }

View File

@ -0,0 +1,159 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog-header";
import "../../components/ha-icon-button";
import "../../components/ha-items-display-editor";
import type { DisplayValue } from "../../components/ha-items-display-editor";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
import type { HomeAssistant } from "../../types";
import type { EditSidebarDialogParams } from "./show-dialog-edit-sidebar";
@customElement("dialog-edit-sidebar")
class DialogEditSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
@state() private _order: string[] = [];
@state() private _hidden: string[] = [];
private _saveCallback?: (order: string[], hidden: string[]) => void;
public async showDialog(params: EditSidebarDialogParams): Promise<void> {
this._open = true;
const storedOrder = localStorage.getItem("sidebarPanelOrder");
const storedHidden = localStorage.getItem("sidebarHiddenPanels");
this._order = storedOrder ? JSON.parse(storedOrder) : this._order;
this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden;
this._saveCallback = params.saveCallback;
}
private _dialogClosed(): void {
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeDialog(): void {
this._dialog?.close();
}
private _panels = memoizeOne((panels: HomeAssistant["panels"]) =>
panels ? Object.values(panels) : []
);
protected render() {
if (!this._open) {
return nothing;
}
const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar");
const panels = this._panels(this.hass.panels);
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._order,
this._hidden,
this.hass.locale
);
const items = [
...beforeSpacer,
...panels.filter((panel) => this._hidden.includes(panel.url_path)),
...afterSpacer.filter((panel) => panel.url_path !== "config"),
].map((panel) => ({
value: panel.url_path,
label:
panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
icon: panel.icon || undefined,
iconPath:
panel.url_path === this.hass.defaultPanel && !panel.icon
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
disableSorting: panel.url_path === "developer-tools",
}));
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
</ha-dialog-header>
<div slot="content" class="content">
<ha-items-display-editor
.hass=${this.hass}
.value=${{
order: this._order,
hidden: this._hidden,
}}
.items=${items}
@value-changed=${this._changed}
dont-sort-visible
>
</ha-items-display-editor>
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save}>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _changed(ev: CustomEvent<{ value: DisplayValue }>): void {
const { order = [], hidden = [] } = ev.detail.value;
this._order = [...order];
this._hidden = [...hidden];
}
private _save(): void {
this._saveCallback?.(this._order ?? [], this._hidden ?? []);
this.closeDialog();
}
static styles = css`
ha-md-dialog {
min-width: 600px;
max-height: 90%;
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
min-width: 100%;
min-height: 100%;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-edit-sidebar": DialogEditSidebar;
}
}

View File

@ -0,0 +1,18 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface EditSidebarDialogParams {
saveCallback: (order: string[], hidden: string[]) => void;
}
export const loadEditSidebarDialog = () => import("./dialog-edit-sidebar");
export const showEditSidebarDialog = (
element: HTMLElement,
dialogParams: EditSidebarDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-edit-sidebar",
dialogImport: loadEditSidebarDialog,
dialogParams,
});
};

View File

@ -10,6 +10,7 @@ import { showNotificationDrawer } from "../dialogs/notifications/show-notificati
import type { HomeAssistant, Route } from "../types"; import type { HomeAssistant, Route } from "../types";
import "./partial-panel-resolver"; import "./partial-panel-resolver";
import { computeRTLDirection } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
import { storage } from "../common/decorators/storage";
declare global { declare global {
// for fire event // for fire event
@ -25,7 +26,8 @@ declare global {
} }
interface EditSideBarEvent { interface EditSideBarEvent {
editMode: boolean; order: string[];
hidden: string[];
} }
@customElement("home-assistant-main") @customElement("home-assistant-main")
@ -42,6 +44,22 @@ export class HomeAssistantMain extends LitElement {
@state() private _drawerOpen = false; @state() private _drawerOpen = false;
@state()
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _panelOrder: string[] = [];
@state()
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _hiddenPanels: string[] = [];
constructor() { constructor() {
super(); super();
listenMediaQuery("(max-width: 870px)", (matches) => { listenMediaQuery("(max-width: 870px)", (matches) => {
@ -63,7 +81,8 @@ export class HomeAssistantMain extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${sidebarNarrow} .narrow=${sidebarNarrow}
.route=${this.route} .route=${this.route}
.editMode=${this._sidebarEditMode} .panelOrder=${this._panelOrder}
.hiddenPanels=${this._hiddenPanels}
.alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"} .alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"}
></ha-sidebar> ></ha-sidebar>
<partial-panel-resolver <partial-panel-resolver
@ -90,17 +109,8 @@ export class HomeAssistantMain extends LitElement {
this.addEventListener( this.addEventListener(
"hass-edit-sidebar", "hass-edit-sidebar",
(ev: HASSDomEvent<EditSideBarEvent>) => { (ev: HASSDomEvent<EditSideBarEvent>) => {
this._sidebarEditMode = ev.detail.editMode; this._panelOrder = ev.detail.order;
this._hiddenPanels = ev.detail.hidden;
if (this._sidebarEditMode) {
if (this._sidebarNarrow) {
this._drawerOpen = true;
} else {
fireEvent(this, "hass-dock-sidebar", {
dock: "docked",
});
}
}
} }
); );

View File

@ -10,6 +10,7 @@ import { isExternal } from "../../data/external";
import type { CoreFrontendUserData } from "../../data/frontend"; import type { CoreFrontendUserData } from "../../data/frontend";
import { subscribeFrontendUserData } from "../../data/frontend"; import { subscribeFrontendUserData } from "../../data/frontend";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showEditSidebarDialog } from "../../dialogs/sidebar/show-dialog-edit-sidebar";
import "../../layouts/hass-tabs-subpage"; import "../../layouts/hass-tabs-subpage";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types"; import type { HomeAssistant, Route } from "../../types";
@ -247,9 +248,18 @@ class HaProfileSectionGeneral extends LitElement {
} }
private _customizeSidebar() { private _customizeSidebar() {
fireEvent(this, "hass-edit-sidebar", { editMode: true }); showEditSidebarDialog(this, {
saveCallback: this._saveSidebar,
});
} }
private _saveSidebar = (order: string[], hidden: string[]) => {
fireEvent(this, "hass-edit-sidebar", {
order,
hidden,
});
};
private _handleLogOut() { private _handleLogOut() {
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.profile.logout_title"), title: this.hass.localize("ui.panel.profile.logout_title"),

View File

@ -1,73 +0,0 @@
import { css } from "lit";
export const sidebarEditStyle = css`
ha-sortable ha-md-list-item.draggable:nth-child(2n) {
animation-name: keyframes1;
animation-iteration-count: infinite;
transform-origin: 50% 10%;
animation-delay: -0.75s;
animation-duration: 0.25s;
}
ha-sortable ha-md-list-item.draggable:nth-child(2n-1) {
animation-name: keyframes2;
animation-iteration-count: infinite;
animation-direction: alternate;
transform-origin: 30% 5%;
animation-delay: -0.5s;
animation-duration: 0.33s;
}
ha-sortable ha-md-list-item.draggable {
cursor: grab;
}
.hidden-panel {
display: flex !important;
}
@keyframes keyframes1 {
0% {
transform: rotate(-1deg);
animation-timing-function: ease-in;
}
50% {
transform: rotate(1.5deg);
animation-timing-function: ease-out;
}
}
@keyframes keyframes2 {
0% {
transform: rotate(1deg);
animation-timing-function: ease-in;
}
50% {
transform: rotate(-1.5deg);
animation-timing-function: ease-out;
}
}
.show-panel,
.hide-panel {
display: none;
--mdc-icon-button-size: 24px;
}
:host([expanded]) .hide-panel {
display: block;
}
:host([expanded]) .show-panel {
display: block;
}
ha-md-list-item.hidden-panel,
ha-md-list-item.hidden-panel span,
ha-md-list-item.hidden-panel ha-icon[slot="start"] {
color: var(--secondary-text-color);
cursor: pointer;
}
`;

View File

@ -2021,9 +2021,7 @@
"sidebar": { "sidebar": {
"external_app_configuration": "App settings", "external_app_configuration": "App settings",
"sidebar_toggle": "Sidebar toggle", "sidebar_toggle": "Sidebar toggle",
"done": "Done", "edit_sidebar": "Edit sidebar"
"hide_panel": "Hide panel",
"show_panel": "Show panel"
}, },
"panel": { "panel": {
"my": { "my": {