diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index caa6ad1534d3..98ed39e912fe 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -34,6 +34,7 @@ export interface LovelaceSectionElement extends HTMLElement { index?: number; cards?: HuiCard[]; isStrategy: boolean; + importOnly?: boolean; setConfig(config: LovelaceSectionConfig): void; } diff --git a/src/panels/lovelace/components/hui-card-edit-mode.ts b/src/panels/lovelace/components/hui-card-edit-mode.ts index 3f0faf96ebb7..c599910cce71 100644 --- a/src/panels/lovelace/components/hui-card-edit-mode.ts +++ b/src/panels/lovelace/components/hui-card-edit-mode.ts @@ -1,8 +1,8 @@ import "@material/mwc-button"; -import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { mdiContentCopy, mdiContentCut, + mdiCursorMove, mdiDelete, mdiDotsVertical, mdiPencil, @@ -10,7 +10,7 @@ import { } from "@mdi/js"; import deepClone from "deep-clone-simple"; import type { CSSResultGroup, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { storage } from "../../../common/decorators/storage"; @@ -39,7 +39,14 @@ export class HuiCardEditMode extends LitElement { @property({ type: Array }) public path!: LovelaceCardPath; - @property({ type: Boolean }) public hiddenOverlay = false; + @property({ type: Boolean, attribute: "hidden-overlay" }) + public hiddenOverlay = false; + + @property({ type: Boolean, attribute: "no-edit" }) + public noEdit = false; + + @property({ type: Boolean, attribute: "no-duplicate" }) + public noDuplicate = false; @state() public _menuOpened: boolean = false; @@ -110,15 +117,24 @@ export class HuiCardEditMode extends LitElement { return html`
-
-
- -
+ ${this.noEdit + ? html` +
+
+ +
+ ` + : html` +
+
+ +
+ `} - - - ${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit")} - - - - ${this.hass.localize( - "ui.panel.lovelace.editor.edit_card.duplicate" - )} - - + ${this.noEdit + ? nothing + : html` + + + ${this.hass.localize( + "ui.panel.lovelace.editor.edit_card.edit" + )} + + `} + ${this.noDuplicate + ? nothing + : html` + + + ${this.hass.localize( + "ui.panel.lovelace.editor.edit_card.duplicate" + )} + + `} + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")} - + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")}
  • - + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")} ) { - switch (ev.detail.index) { - case 0: + private _handleAction(ev) { + switch (ev.target.action) { + case "edit": this._editCard(); break; - case 1: + case "duplicate": this._duplicateCard(); break; - case 2: + case "copy": this._copyCard(); break; - case 3: + case "cut": this._cutCard(); break; - case 4: + case "delete": this._deleteCard(); break; } @@ -262,7 +309,7 @@ export class HuiCardEditMode extends LitElement { z-index: 0; } - .edit { + .control { outline: none !important; cursor: pointer; position: absolute; @@ -273,7 +320,7 @@ export class HuiCardEditMode extends LitElement { border-radius: var(--ha-card-border-radius, 12px); z-index: 0; } - .edit-overlay { + .control-overlay { position: absolute; inset: 0; opacity: 0.8; @@ -282,7 +329,7 @@ export class HuiCardEditMode extends LitElement { border-radius: var(--ha-card-border-radius, 12px); z-index: 0; } - .edit ha-svg-icon { + .control ha-svg-icon { display: flex; position: relative; color: var(--primary-text-color); diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 3406dc40c625..851afe9b4018 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -21,22 +21,17 @@ import "../../../components/ha-icon-button"; import "../../../components/ha-list-item"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { saveConfig } from "../../../data/lovelace/config/types"; -import { - isStrategyView, - type LovelaceViewConfig, -} from "../../../data/lovelace/config/view"; +import { isStrategyView } from "../../../data/lovelace/config/view"; import { showAlertDialog, showPromptDialog, } from "../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import { showSaveSuccessToast } from "../../../util/toast-saved-success"; import { computeCardSize } from "../common/compute-card-size"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { addCard, - addSection, deleteCard, moveCardToContainer, moveCardToIndex, @@ -50,8 +45,6 @@ import { } from "../editor/lovelace-path"; import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog"; import type { Lovelace, LovelaceCard } from "../types"; -import { SECTIONS_VIEW_LAYOUT } from "../views/const"; -import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; @customElement("hui-card-options") export class HuiCardOptions extends LitElement { @@ -352,9 +345,13 @@ export class HuiCardOptions extends LitElement { allowDashboardChange: true, header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"), viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => { - const fromView = selectedDashConfig.views[this.path![0]]; - let toView = selectedDashConfig.views[viewIndex]; - let newConfig = selectedDashConfig; + if (!this.lovelace) return; + const toView = selectedDashConfig.views[viewIndex]; + const newConfig = selectedDashConfig; + + const undoAction = async () => { + this.lovelace!.saveConfig(selectedDashConfig); + }; if (isStrategyView(toView)) { showAlertDialog(this, { @@ -369,53 +366,22 @@ export class HuiCardOptions extends LitElement { return; } - const isSectionsView = toView.type === SECTIONS_VIEW_LAYOUT; - - let toPath: LovelaceContainerPath = [viewIndex]; - - // If the view is a section view and has no "imported cards" section, adds a default section. - if (isSectionsView) { - const importedCardHeading = fromView.title - ? this.hass!.localize( - "ui.panel.lovelace.editor.section.imported_card_section_title_view", - { view_title: fromView.title } - ) - : this.hass!.localize( - "ui.panel.lovelace.editor.section.imported_card_section_title_default" - ); - - let sectionIndex = toView.sections - ? toView.sections.findIndex( - (s) => - "cards" in s && - s.cards?.some( - (c) => - c.type === "heading" && c.heading === importedCardHeading - ) - ) - : -1; - if (sectionIndex === -1) { - const newSection: LovelaceSectionConfig = { - type: "grid", - cards: [ - { - type: "heading", - heading: importedCardHeading, - }, - ], - }; - newConfig = addSection(selectedDashConfig, viewIndex, newSection); - toView = newConfig.views[viewIndex] as LovelaceViewConfig; - sectionIndex = toView.sections!.length - 1; - } - toPath = [viewIndex, sectionIndex]; - } + const toPath: LovelaceContainerPath = [viewIndex]; if (urlPath === this.lovelace!.urlPath) { this.lovelace!.saveConfig( moveCardToContainer(newConfig, this.path!, toPath) ); - showSaveSuccessToast(this, this.hass!); + this.lovelace.showToast({ + message: this.hass!.localize( + "ui.panel.lovelace.editor.move_card.success" + ), + duration: 4000, + action: { + action: undoAction, + text: this.hass!.localize("ui.common.undo"), + }, + }); return; } try { @@ -429,10 +395,22 @@ export class HuiCardOptions extends LitElement { this.lovelace!.saveConfig( deleteCard(this.lovelace!.config, this.path!) ); - showSaveSuccessToast(this, this.hass!); + + this.lovelace.showToast({ + message: this.hass!.localize( + "ui.panel.lovelace.editor.move_card.success" + ), + duration: 4000, + action: { + action: undoAction, + text: this.hass!.localize("ui.common.undo"), + }, + }); } catch (err: any) { - showAlertDialog(this, { - text: `Moving failed: ${err.message}`, + this.lovelace.showToast({ + message: this.hass!.localize( + "ui.panel.lovelace.editor.move_card.error" + ), }); } }, diff --git a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts index 2ac013fa7f5d..68f7c4f93bf3 100644 --- a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts @@ -13,6 +13,7 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { navigate } from "../../../../common/navigate"; import { deepEqual } from "../../../../common/util/deep-equal"; import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; import "../../../../components/ha-circular-progress"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; @@ -57,8 +58,10 @@ export class HuiDialogEditView extends LitElement { @query("ha-yaml-editor") private _editor?: HaYamlEditor; + @state() private _currentType = getViewType(); + get _type(): string { - return getViewType(this._config!); + return getViewType(this._config); } protected updated(changedProperties: PropertyValues) { @@ -76,7 +79,6 @@ export class HuiDialogEditView extends LitElement { if (this._params.viewIndex === undefined) { this._config = { type: SECTIONS_VIEW_LAYOUT, - sections: [generateDefaultSection(this.hass!.localize)], }; this._dirty = false; return; @@ -89,6 +91,7 @@ export class HuiDialogEditView extends LitElement { return; } this._config = view; + this._currentType = this._type; } public closeDialog(): void { @@ -159,12 +162,14 @@ export class HuiDialogEditView extends LitElement { } } - const isCompatibleViewType = - this._config?.type === SECTIONS_VIEW_LAYOUT - ? this._config?.type === SECTIONS_VIEW_LAYOUT && - !this._config?.cards?.length - : this._config?.type !== SECTIONS_VIEW_LAYOUT && - !this._config?.sections?.length; + const convertToSection = + this._type === SECTIONS_VIEW_LAYOUT && + this._currentType !== SECTIONS_VIEW_LAYOUT && + this._config?.cards?.length; + const convertNotSupported = + this._type !== SECTIONS_VIEW_LAYOUT && + this._currentType === SECTIONS_VIEW_LAYOUT && + this._config?.sections?.length; return html`
    - ${!isCompatibleViewType + ${convertToSection ? html` - - ${this._config?.type === SECTIONS_VIEW_LAYOUT - ? this.hass!.localize( - "ui.panel.lovelace.editor.edit_view.type_warning_sections" - ) - : this.hass!.localize( - "ui.panel.lovelace.editor.edit_view.type_warning_others" - )} + + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.card_to_section_convert" + )} + + + + ` + : nothing} + ${convertNotSupported + ? html` + + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.section_to_card_not_supported" + )} ` : nothing} @@ -258,7 +276,7 @@ export class HuiDialogEditView extends LitElement { ${content} ${this._params.viewIndex !== undefined ? html` - + ` : nothing} - ${this._saving @@ -284,7 +303,7 @@ export class HuiDialogEditView extends LitElement { aria-label="Saving" >` : nothing} - ${this.hass!.localize("ui.common.save")} `; @@ -303,6 +322,54 @@ export class HuiDialogEditView extends LitElement { } } + private async _convertToSection() { + if (!this._params || !this._config) { + return; + } + const confirm = await showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.convert_view_title" + ), + text: this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.convert_view_text" + ), + confirmText: this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.convert_view_action" + ), + dismissText: this.hass!.localize("ui.common.cancel"), + }); + + if (!confirm) { + return; + } + + const newConfig = { + ...this._config, + }; + newConfig.type = SECTIONS_VIEW_LAYOUT; + newConfig.sections = [generateDefaultSection(this.hass!.localize)]; + newConfig.path = undefined; + const lovelace = this._params!.lovelace!; + + try { + await lovelace.saveConfig( + addView(this.hass!, lovelace.config, newConfig) + ); + if (this._params.saveCallback) { + this._params.saveCallback(lovelace.config.views.length, newConfig); + } + this.closeDialog(); + } catch (err: any) { + showAlertDialog(this, { + text: `${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.saving_failed" + )}: ${err.message}`, + }); + } finally { + this._saving = false; + } + } + private async _delete(): Promise { if (!this._params) { return; @@ -366,7 +433,7 @@ export class HuiDialogEditView extends LitElement { const viewConf = { ...this._config, }; - + // Ensure we have at least one section if we are in sections view if (viewConf.type === SECTIONS_VIEW_LAYOUT && !viewConf.sections?.length) { viewConf.sections = [generateDefaultSection(this.hass!.localize)]; } else if (!viewConf.cards?.length) { @@ -386,7 +453,7 @@ export class HuiDialogEditView extends LitElement { viewConf ) ); - if (this._params.saveCallback) { + if (this._params.saveCallback && this._creatingView) { this._params.saveCallback( this._params.viewIndex || lovelace.config.views.length, viewConf @@ -479,7 +546,7 @@ export class HuiDialogEditView extends LitElement { text-transform: uppercase; padding: 0 20px; } - mwc-button.warning { + ha-button.warning { margin-right: auto; margin-inline-end: auto; margin-inline-start: initial; @@ -494,7 +561,10 @@ export class HuiDialogEditView extends LitElement { color: var(--error-color); border-bottom: 1px solid var(--error-color); } - .incompatible { + ha-alert { + display: block; + } + ha-alert ha-button { display: block; } diff --git a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts index 7e715d1b9929..000b5ca9d30f 100644 --- a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts @@ -40,21 +40,6 @@ export class HuiViewEditor extends LitElement { private _schema = memoizeOne( (localize: LocalizeFunc, viewType: string) => [ - { name: "title", selector: { text: {} } }, - { - name: "icon", - selector: { - icon: {}, - }, - }, - { name: "path", selector: { text: {} } }, - { name: "theme", selector: { theme: {} } }, - { - name: "subview", - selector: { - boolean: {}, - }, - }, { name: "type", selector: { @@ -62,9 +47,9 @@ export class HuiViewEditor extends LitElement { options: ( [ SECTIONS_VIEW_LAYOUT, + MASONRY_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT, PANEL_VIEW_LAYOUT, - MASONRY_VIEW_LAYOUT, ] as const ).map((type) => ({ value: type, @@ -75,6 +60,21 @@ export class HuiViewEditor extends LitElement { }, }, }, + { name: "title", selector: { text: {} } }, + { + name: "icon", + selector: { + icon: {}, + }, + }, + { name: "path", selector: { text: {} } }, + { name: "theme", selector: { theme: {} } }, + { + name: "subview", + selector: { + boolean: {}, + }, + }, ...(viewType === SECTIONS_VIEW_LAYOUT ? ([ { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 202ae6738587..0fe8d4f6de18 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -833,6 +833,10 @@ class HUIRoot extends LitElement { showEditViewDialog(this, { lovelace: this.lovelace!, viewIndex: this._curView as number, + saveCallback: (viewIndex: number, viewConfig: LovelaceViewConfig) => { + const path = viewConfig.path || viewIndex; + this._navigateToView(path); + }, }); } diff --git a/src/panels/lovelace/sections/hui-grid-section.ts b/src/panels/lovelace/sections/hui-grid-section.ts index 2a985ffea896..5c1e5e18f7e2 100644 --- a/src/panels/lovelace/sections/hui-grid-section.ts +++ b/src/panels/lovelace/sections/hui-grid-section.ts @@ -25,8 +25,18 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = { delayOnTouchOnly: true, direction: "vertical", invertedSwapThreshold: 0.7, + group: "card", } as HaSortableOptions; +const IMPORT_MODE_CARD_SORTABLE_OPTIONS: HaSortableOptions = { + ...CARD_SORTABLE_OPTIONS, + sort: false, + group: { + name: "card", + put: false, + }, +}; + export class GridSection extends LitElement implements LovelaceSectionElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -40,6 +50,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement { @property({ attribute: false }) public cards: HuiCard[] = []; + @property({ attribute: false }) public importOnly = false; + @state() _config?: LovelaceSectionConfig; @state() _dragging = false; @@ -67,21 +79,29 @@ export class GridSection extends LitElement implements LovelaceSectionElement { const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy); + const sortableOptions = this.importOnly + ? IMPORT_MODE_CARD_SORTABLE_OPTIONS + : CARD_SORTABLE_OPTIONS; + return html` -
    +
    ${repeat( cardsConfig, (cardConfig) => this._getKey(cardConfig), @@ -117,6 +137,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement { .lovelace=${this.lovelace!} .path=${cardPath} .hiddenOverlay=${this._dragging} + .noEdit=${this.importOnly} + .noDuplicate=${this.importOnly} > ${card} @@ -126,7 +148,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement { `; } )} - ${editMode + ${editMode && !this.importOnly ? html`
    `; } + private _importedCardSectionConfig = memoizeOne( + (cards: LovelaceCardConfig[]) => ({ + type: "grid", + cards, + }) + ); + private _createSection(): void { const newConfig = addSection(this.lovelace!.config, this.index!, { type: "grid", @@ -432,6 +472,33 @@ export class SectionsView extends LitElement implements LovelaceViewElement { --mdc-icon-size: 20px; color: var(--primary-text-color); } + + .imported-cards { + --column-span: var(--column-count); + --row-span: 1; + order: 2; + } + + .imported-card-header { + margin-top: 24px; + padding: 16px 8px; + border-top: 2px dashed var(--divider-color); + } + + .imported-card-header .title { + margin: 0; + color: var(--primary-text-color); + font-size: 16px; + font-weight: 400; + line-height: 24px; + } + .imported-card-header .subtitle { + margin: 0; + color: var(--secondary-text-color); + font-size: 14px; + font-weight: 400; + line-height: 20px; + } `; } } diff --git a/src/translations/en.json b/src/translations/en.json index ea07a3b19397..528743b0b90d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5611,14 +5611,18 @@ "visibility": { "select_users": "Select which users should see this view in the navigation" }, - "type": "View type", - "type_warning_sections": "You can not change your view to use the 'sections' view type because migration is not supported yet. Start from scratch with a new view if you want to experiment with the 'sections' view.", - "type_warning_others": "You can not change your view to an other type because migration is not supported yet. Start from scratch with a new view if you want to use another view type.", + "type": "Layout", + "convert_view": "Convert", + "convert_view_title": "Convert view layout", + "convert_view_text": "It will create a new view using sections. This current view will stay untouched. All your cards will be imported so you can rearrange them freely.", + "convert_view_action": "Create", + "card_to_section_convert": "Convert your view to a section view.", + "section_to_card_not_supported": "You can not change your section view to an other type. Start from scratch with a new view if you want to use another view type.", "types": { + "sections": "Sections (default)", "masonry": "Masonry", "sidebar": "Sidebar", - "panel": "Panel (single card)", - "sections": "Sections" + "panel": "Panel (single card)" }, "subview": "Subview", "max_columns": "Max number of sections wide", @@ -5704,7 +5708,9 @@ "move_card": { "header": "Choose a view to move the card to", "strategy_error_title": "Impossible to move the card", - "strategy_error_text_strategy": "Moving a card to an auto generated view is not supported." + "strategy_error_text_strategy": "Moving a card to an auto generated view is not supported.", + "success": "Card moved successfully", + "error": "Error while moving card" }, "change_position": { "title": "Change card position", @@ -5723,8 +5729,8 @@ "add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]", "create_section": "Create section", "default_section_title": "New section", - "imported_card_section_title_view": "Imported cards from ''{view_title}'' view", - "imported_card_section_title_default": "Imported cards from another view" + "imported_cards_title": "Imported cards", + "imported_cards_description": "These cards are imported from another view. They will only be displayed in edit mode. Move them into sections to display them in your view." }, "delete_section": { "title": "Delete section",