Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef626c08c3 | ||
|
|
2d3d59b47f | ||
|
|
58fc2b41c9 | ||
|
|
6857dc1aae | ||
|
|
734bc63100 |
No files matched your search
@@ -1,84 +0,0 @@
|
||||
# Anchored popovers for confirmations and simple forms
|
||||
|
||||
Concept branch: `anchored-confirmations`. Not for merging as it is. It exists to try the idea in real screens and to drive a UX design discussion.
|
||||
|
||||
## The idea
|
||||
|
||||
When a confirmation or a small form opens from a specific button, show it as a popover next to that button on larger screens, instead of a centred dialog. On small screens it becomes a bottom sheet. Confirmations opened without a button, such as after a save, keep today's dialog.
|
||||
|
||||
## What is in the branch
|
||||
|
||||
The branch sits on top of the data table group header actions (#55235), which added the first example.
|
||||
|
||||
### Shared pieces
|
||||
|
||||
- `showConfirmationDialog`, `showAlertDialog` and `showPromptDialog` take an optional `anchor`. `dialog-box` renders `ha-adaptive-popover` when it has one, and `ha-dialog` otherwise.
|
||||
- `showFormDialog` takes the same `anchor`, and `dialog-form` follows the same rule.
|
||||
- `ha-adaptive-popover` already switched to a bottom sheet below 870px wide or 500px tall. It now has:
|
||||
- `compact`, with tighter spacing for short content.
|
||||
- `accessibleLabel`, for when the title isn't the popover's own header.
|
||||
- `--ha-adaptive-popover-min-width` and `--ha-adaptive-popover-width`.
|
||||
- `ha-dialog-header` takes `--ha-dialog-header-content-padding` and `--ha-dialog-header-min-height`. Both default to today's values.
|
||||
- Confirmations with only a title show it as the message, without a header, so they don't read as a heading with nothing under it.
|
||||
|
||||
### Where it is used
|
||||
|
||||
| Where | Opened from | Type |
|
||||
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||
| Marketplace | New group header action | Confirmation |
|
||||
| Dashboard edit mode | Section menu, Delete | Confirmation, destructive |
|
||||
| Energy settings | Delete on grid, solar, battery, gas and water sources, individual devices, and the CO2 integration | Confirmation |
|
||||
| Dashboard resources | Delete | Confirmation, destructive |
|
||||
| Apps | Remove repository, remove registry | Confirmation, destructive |
|
||||
| Dropdown helper settings | Remove option | Confirmation over a dialog |
|
||||
| Automation editor | Trigger and action menus, Rename | Prompt |
|
||||
| Object selector (for example, card editor buttons) | Add, Edit | Form |
|
||||
|
||||
Menu items anchor to the menu's button, since the menu has closed by the time the popover opens.
|
||||
|
||||
## Needs deciding
|
||||
|
||||
1. **Whether to do this at all**, and for which kinds: confirmations, prompts (rename), small forms, or all three.
|
||||
2. **Look.** The current popover uses the dialog's surface: large radius, shadow and filled buttons. It reads as Material. A flatter, less rounded popover would suit a small inline control better.
|
||||
3. **Width.**
|
||||
- Messages are sized to their content, up to the screen width, so text only wraps when it has to.
|
||||
- Prompts have a minimum of 400px.
|
||||
- Forms use the 580px dialog width.
|
||||
- Are those the right three rules?
|
||||
4. **Title-only confirmations.** Show the title as plain text (current), or keep a heading?
|
||||
5. **Placement.** On some screens the popover covers the button that opened it (energy devices, dashboard sections). Should it always open beside or below the button?
|
||||
6. **Mobile.** Bottom sheet, the same as adaptive dialogs, or a popover there too?
|
||||
7. **Destructive confirmations.** A popover is lighter than a dialog. Is that too easy to confirm for deletes?
|
||||
8. **Rollout.** There are about 170 confirmation calls. Opt in per call site (current), or anchor automatically when the click event is available?
|
||||
9. **Popovers inside dialogs.** For example, removing a dropdown option from the helper settings dialog. Should popovers open there at all, and if they do, should they be kept inside the dialog's bounds?
|
||||
10. **Forms.** A popover suits a quick edit with one or two fields. The object selector's edit form has more than that and doesn't work well as a popover stacked over the card editor dialog. A longer form should probably be a view of its own instead of a stacked popover or dialog.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- Confirmations block dismissing by clicking outside or pressing Escape, the same as the dialog today. A popover may be expected to dismiss more easily.
|
||||
- Focus lands on the popover rather than the default button.
|
||||
- Renames started from the automation sidebar have no anchor, so they stay dialogs.
|
||||
- Timer presets and other forms opened from custom events can't be anchored yet, because the event doesn't carry the clicked element.
|
||||
|
||||
## Values to turn into variables or tokens
|
||||
|
||||
These were tuned by eye in the concept and are hard-coded:
|
||||
|
||||
| Where | Value | Why |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| `ha-adaptive-popover`, compact footer | `18px` right and bottom padding | Looked even with the 20px around the text |
|
||||
| `ha-adaptive-popover`, compact | `var(--ha-space-5)` (20px) edges, `--ha-space-3` (12px) under the title, `--ha-space-4` (16px) above the footer | Should be `--ha-popover-padding` and gap tokens |
|
||||
| `dialog-box`, prompt popover | `min(400px, 95vw)` minimum width | A text field has no width of its own |
|
||||
| `dialog-box`, message popover | `95vw` maximum width | Lets messages grow before wrapping |
|
||||
| `ha-data-table`, group actions | `margin-top: -12px` | Undoes the group header's `padding-top: 12px` |
|
||||
| `ha-data-table`, group actions | `48px` in the end padding calculation | The rows' icon button size, to line up with overflow menus |
|
||||
|
||||
## Related earlier work
|
||||
|
||||
- [#51641](https://github.com/home-assistant/frontend/issues/51641): card features with actions showing a popover instead of a full dialog. The original idea was a popover mode for more info, similar to the Apple Home controls, which needed a new "mini more info" and more work.
|
||||
- [#51650](https://github.com/home-assistant/frontend/pull/51650): added `ha-adaptive-popover`, used by the set date card feature. This branch builds on it.
|
||||
- [OpenHomeFoundation/ux-design#43](https://github.com/OpenHomeFoundation/ux-design/issues/43): showing contextual actions in popover dialogs.
|
||||
|
||||
## The look
|
||||
|
||||
The popover reuses the dialog's 24px radius (`--ha-border-radius-3xl`), shadow and filled buttons. The look from the new design system in the works would probably suit this idea better.
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -124,25 +124,7 @@ export interface DataTableRowData {
|
||||
|
||||
export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
|
||||
|
||||
// Slot for the actions at the end of a group's header, they don't toggle the
|
||||
// group. Put `ha-icon-button`s with a label in it. They get the rows' icon
|
||||
// colour and a smaller size by default, which `--data-table-group-actions-color`,
|
||||
// `--data-table-group-actions-button-size` and
|
||||
// `--data-table-group-actions-icon-size` change for all of them; style a
|
||||
// button itself for its own colour or size.
|
||||
export const groupActionsSlot = (group: string) => `group-actions-${group}`;
|
||||
|
||||
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
|
||||
// The group a row's value of the group column puts it in. Rows without a value
|
||||
// share a reserved group, sorted to the bottom.
|
||||
export const dataTableGroupKey = (
|
||||
value: string | number | boolean | null | undefined
|
||||
) => {
|
||||
const key = String(value);
|
||||
|
||||
return key === "undefined" ? UNDEFINED_GROUP_KEY : key;
|
||||
};
|
||||
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
|
||||
|
||||
// Default row height, used to fill the viewport with skeleton rows.
|
||||
@@ -227,9 +209,6 @@ export class HaDataTable extends LitElement {
|
||||
@query(".sticky-group-header .group-header")
|
||||
private _stickyGroupHeaderCell?: HTMLElement;
|
||||
|
||||
@query(".sticky-group-header .group-actions")
|
||||
private _stickyGroupActions?: HTMLElement;
|
||||
|
||||
// Group pinned to the top of the scroller while its own header is scrolled out of view
|
||||
@state() private _stickyGroup?: string;
|
||||
|
||||
@@ -741,26 +720,6 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${
|
||||
// Rendered before the rows, so the group's actions slot
|
||||
// shows here and not in its scrolled out header row
|
||||
this.groupColumn &&
|
||||
!this.autoHeight &&
|
||||
this._stickyGroup !== undefined
|
||||
? html`<div
|
||||
class="mdc-data-table__row sticky-group-header"
|
||||
.group=${this._stickyGroup}
|
||||
@click=${this._collapseStickyGroup}
|
||||
>
|
||||
${this._renderGroupHeader(
|
||||
this._stickyGroup,
|
||||
this._collapsedGroups.includes(this._stickyGroup),
|
||||
this._i18n?.localize,
|
||||
true
|
||||
)}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar ${classMap(
|
||||
@@ -773,6 +732,25 @@ export class HaDataTable extends LitElement {
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
${
|
||||
this.groupColumn &&
|
||||
!this.autoHeight &&
|
||||
this._stickyGroup !== undefined
|
||||
? html`<div
|
||||
class="mdc-data-table__row sticky-group-header"
|
||||
aria-hidden="true"
|
||||
.group=${this._stickyGroup}
|
||||
@click=${this._collapseStickyGroup}
|
||||
>
|
||||
${this._renderGroupHeader(
|
||||
this._stickyGroup,
|
||||
this._collapsedGroups.includes(this._stickyGroup),
|
||||
this._i18n?.localize,
|
||||
true
|
||||
)}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
@@ -1047,10 +1025,12 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
if (groupColumn) {
|
||||
const isGroupSortColumn = sortColumn === groupColumn;
|
||||
|
||||
const grouped = groupBy(items, (item) =>
|
||||
dataTableGroupKey(item[groupColumn])
|
||||
);
|
||||
const grouped = groupBy(items, (item) => item[groupColumn]);
|
||||
if (grouped.undefined) {
|
||||
// make sure ungrouped items are at the bottom
|
||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||
delete grouped.undefined;
|
||||
}
|
||||
const sortedEntries: [string, DataTableRowData[]][] = Object.keys(
|
||||
grouped
|
||||
)
|
||||
@@ -1127,14 +1107,12 @@ export class HaDataTable extends LitElement {
|
||||
localize: LocalizeFunc | undefined,
|
||||
sticky = false
|
||||
) {
|
||||
// The sticky copy's button and name are hidden from assistive technology,
|
||||
// and its button is made inert. It handles clicks on its whole row
|
||||
// instead, as its cell shifts with horizontal scrolling. The group's
|
||||
// actions show in the sticky copy while it is pinned, so they stay
|
||||
// available there.
|
||||
// The sticky copy is hidden from assistive technology, so its button is
|
||||
// made inert. It handles clicks on its whole row instead, as its cell
|
||||
// shifts with horizontal scrolling.
|
||||
return html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role=${ifDefined(sticky ? undefined : "cell")}
|
||||
role="cell"
|
||||
.group=${groupName}
|
||||
@click=${sticky ? nothing : this._collapseGroup}
|
||||
>
|
||||
@@ -1149,27 +1127,14 @@ export class HaDataTable extends LitElement {
|
||||
?inert=${sticky}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<span
|
||||
class="group-name"
|
||||
aria-hidden=${ifDefined(sticky ? "true" : undefined)}
|
||||
>
|
||||
${
|
||||
groupName === UNDEFINED_GROUP_KEY
|
||||
? localize?.("ui.components.data-table.ungrouped") || "Ungrouped"
|
||||
: groupName || ""
|
||||
}
|
||||
</span>
|
||||
<div class="group-actions" @click=${this._stopGroupToggle}>
|
||||
<slot name=${groupActionsSlot(groupName)}></slot>
|
||||
</div>
|
||||
${
|
||||
groupName === UNDEFINED_GROUP_KEY
|
||||
? localize?.("ui.components.data-table.ungrouped") || "Ungrouped"
|
||||
: groupName || ""
|
||||
}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Actions don't toggle the group their header belongs to
|
||||
private _stopGroupToggle(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _memFilterData = memoizeOne(
|
||||
(
|
||||
data: DataTableRowData[],
|
||||
@@ -1471,11 +1436,6 @@ export class HaDataTable extends LitElement {
|
||||
if (this._stickyGroupHeaderCell) {
|
||||
this._stickyGroupHeaderCell.style.transform = `translateX(${-scrollLeft}px)`;
|
||||
}
|
||||
|
||||
// Its actions stay at the end of the visible row, like the rows' own
|
||||
if (this._stickyGroupActions) {
|
||||
this._stickyGroupActions.style.transform = `translateX(${scrollLeft}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
@@ -1543,15 +1503,18 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
public collapseAllGroups() {
|
||||
const groupColumn = this.groupColumn;
|
||||
|
||||
if (!groupColumn || !this.data.some((item) => item[groupColumn])) {
|
||||
if (
|
||||
!this.groupColumn ||
|
||||
!this.data.some((item) => item[this.groupColumn!])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = groupBy(this.data, (item) =>
|
||||
dataTableGroupKey(item[groupColumn])
|
||||
);
|
||||
const grouped = groupBy(this.data, (item) => item[this.groupColumn!]);
|
||||
if (grouped.undefined) {
|
||||
// undefined is a reserved group name
|
||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||
delete grouped.undefined;
|
||||
}
|
||||
this._collapsedGroups = Object.keys(grouped);
|
||||
this._lastSelectedRowId = null;
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
@@ -1910,16 +1873,6 @@ export class HaDataTable extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
/* Clipping would make the cell the actions' sticky scroll container,
|
||||
and cut off the pinned header's actions */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button {
|
||||
@@ -1930,34 +1883,6 @@ export class HaDataTable extends LitElement {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
/* Centre in the row, not below the header's top padding */
|
||||
align-self: stretch;
|
||||
margin-top: -12px;
|
||||
margin-inline-start: auto;
|
||||
/* Smaller than the rows' icon buttons, inherited by the slotted ones */
|
||||
--_button-size: var(--data-table-group-actions-button-size, 36px);
|
||||
--ha-icon-button-size: var(--_button-size);
|
||||
--mdc-icon-size: var(--data-table-group-actions-icon-size, 20px);
|
||||
/* Centre the last action over the row's last cell, such as an
|
||||
overflow menu, which is a 48px icon button */
|
||||
padding-inline-end: calc(
|
||||
var(--ha-space-4) + (48px - var(--_button-size)) / 2
|
||||
);
|
||||
/* Stay in view when the rows scroll horizontally */
|
||||
position: sticky;
|
||||
inset-inline-end: 0;
|
||||
cursor: default;
|
||||
/* Inherited by the slotted icon buttons, like the rows' icon cells */
|
||||
color: var(
|
||||
--data-table-group-actions-color,
|
||||
var(--secondary-text-color)
|
||||
);
|
||||
}
|
||||
|
||||
.sticky-group-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -11,24 +11,12 @@ import { HaAdaptiveDialog } from "./ha-adaptive-dialog";
|
||||
*
|
||||
* Uses an anchored desktop popover when an anchor is available and falls back
|
||||
* to `ha-adaptive-dialog` otherwise.
|
||||
*
|
||||
* @cssprop --ha-adaptive-popover-min-width - Minimum popover width. Defaults to its width preset; 0 fits it to its content, up to that width.
|
||||
* @cssprop --ha-adaptive-popover-width - Width of a compact popover. Defaults to the medium dialog width.
|
||||
*/
|
||||
@customElement("ha-adaptive-popover")
|
||||
export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
|
||||
@property({ attribute: false })
|
||||
public dialogAnchor?: Element;
|
||||
|
||||
/** Tighter spacing in the popover, for short content such as confirmations */
|
||||
@property({ type: Boolean, reflect: true }) public compact = false;
|
||||
|
||||
/**
|
||||
* Names the popover when it has no header title to label it, such as when
|
||||
* its title lives in another shadow root
|
||||
*/
|
||||
@property({ attribute: false }) public accessibleLabel?: string;
|
||||
|
||||
private _allowPopoverHide = false;
|
||||
|
||||
private _openPopoverAnimationFrame?: number;
|
||||
@@ -93,7 +81,6 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby=${ifDefined(this._defaultAriaLabelledBy)}
|
||||
aria-label=${ifDefined(this.accessibleLabel)}
|
||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
||||
@pointerdown=${this._handlePopoverPointerDown}
|
||||
@wa-show=${this._handlePopoverShow}
|
||||
@@ -298,11 +285,7 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
|
||||
padding: 0;
|
||||
border-color: transparent;
|
||||
box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l));
|
||||
/* 0 fits the popover to its content, up to its width */
|
||||
min-width: var(
|
||||
--ha-adaptive-popover-min-width,
|
||||
var(--width, var(--full-width))
|
||||
);
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width);
|
||||
max-height: var(
|
||||
--ha-dialog-max-height,
|
||||
@@ -351,36 +334,6 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 20px around the edges, 12px below the title and 16px above the
|
||||
footer. The header bar and its empty navigation icon slot already
|
||||
give the title 16px on the side. */
|
||||
:host([compact]) wa-popover {
|
||||
--ha-dialog-header-content-padding: var(--ha-space-5)
|
||||
var(--ha-space-1) var(--ha-space-3);
|
||||
--ha-dialog-header-min-height: 0;
|
||||
/* Content that sizes itself, such as a short message, can let the
|
||||
popover grow wider so text only wraps when there is no room */
|
||||
--width: var(
|
||||
--ha-adaptive-popover-width,
|
||||
min(var(--ha-dialog-width-md, 580px), var(--full-width))
|
||||
);
|
||||
--ha-dialog-header-white-space: normal;
|
||||
--ha-dialog-header-title-height: auto;
|
||||
--dialog-content-padding: 0 var(--ha-space-5);
|
||||
}
|
||||
|
||||
/* 18px beside and below the buttons looks even with the 20px around
|
||||
the text */
|
||||
/* Without a header the content starts the popover, so it takes the
|
||||
header's place at the top */
|
||||
:host([compact][without-header]) wa-popover {
|
||||
--dialog-content-padding: var(--ha-space-5) var(--ha-space-5) 0;
|
||||
}
|
||||
|
||||
:host([compact]) ::slotted([slot="footer"]) {
|
||||
padding: var(--ha-space-4) 18px 18px var(--ha-space-5);
|
||||
}
|
||||
|
||||
::slotted([slot="footer"]) {
|
||||
display: flex;
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
|
||||
@@ -16,8 +16,6 @@ import { customElement, property } from "lit/decorators";
|
||||
*
|
||||
* @cssprop --ha-dialog-header-white-space - White space of the title and subtitle, set to `normal` to let them wrap. Defaults to `nowrap`.
|
||||
* @cssprop --ha-dialog-header-title-height - Height of the title, set to `auto` when it wraps.
|
||||
* @cssprop --ha-dialog-header-content-padding - Padding around the title and subtitle.
|
||||
* @cssprop --ha-dialog-header-min-height - Minimum height of the title and subtitle area.
|
||||
* @cssprop --ha-dialog-header-title-color - Color of the title.
|
||||
* @cssprop --ha-dialog-header-subtitle-color - Color of the subtitle.
|
||||
*
|
||||
@@ -82,14 +80,11 @@ export class HaDialogHeader extends LitElement {
|
||||
}
|
||||
.header-content {
|
||||
flex: 1;
|
||||
padding: var(
|
||||
--ha-dialog-header-content-padding,
|
||||
10px var(--ha-space-1)
|
||||
);
|
||||
padding: 10px var(--ha-space-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: var(--ha-dialog-header-min-height, var(--ha-space-12));
|
||||
min-height: var(--ha-space-12);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -262,7 +262,6 @@ export class HaObjectSelector extends LitElement {
|
||||
};
|
||||
|
||||
const newItem = await showFormDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.common.add"),
|
||||
schema,
|
||||
data,
|
||||
@@ -291,7 +290,6 @@ export class HaObjectSelector extends LitElement {
|
||||
const index = ev.currentTarget.index;
|
||||
|
||||
const updatedItem = await showFormDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.common.edit"),
|
||||
schema: this._schema(this.selector),
|
||||
data: item,
|
||||
|
||||
@@ -11,7 +11,6 @@ import { deepActiveElement } from "../../common/dom/deep-active-element";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import "../../components/ha-adaptive-popover";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
@@ -40,13 +39,8 @@ export class DialogForm
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
// Set by the dialog manager before each showDialog
|
||||
@property({ attribute: false }) public dialogAnchor?: Element;
|
||||
|
||||
@state() private _params?: FormDialogParams;
|
||||
|
||||
@state() private _anchor?: Element;
|
||||
|
||||
@state() private _data: FormDialogData = {};
|
||||
|
||||
private _initialData: FormDialogData = {};
|
||||
@@ -67,7 +61,6 @@ export class DialogForm
|
||||
|
||||
public async showDialog(params: FormDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._anchor = this.dialogAnchor;
|
||||
this._data = params.data || {};
|
||||
this._initialData = deepClone(this._data);
|
||||
this._open = true;
|
||||
@@ -257,7 +250,12 @@ export class DialogForm
|
||||
): Promise<void> {
|
||||
await this._afterFormRender();
|
||||
|
||||
if (!this.isConnected || !this._open || this._params !== expectedParams) {
|
||||
if (
|
||||
!this.isConnected ||
|
||||
!this._open ||
|
||||
this._params !== expectedParams ||
|
||||
!this._dialog
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,10 +265,7 @@ export class DialogForm
|
||||
this._focusFirstControl();
|
||||
}
|
||||
|
||||
// Only the dialog keeps its own scroll position, a popover fits its form
|
||||
if (this._dialog) {
|
||||
this._dialog.bodyContainer.scrollTop = scrollTop;
|
||||
}
|
||||
this._dialog.bodyContainer.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
@@ -289,7 +284,6 @@ export class DialogForm
|
||||
this._closeState = undefined;
|
||||
this._stack = [];
|
||||
this._params = undefined;
|
||||
this._anchor = undefined;
|
||||
this._initialData = {};
|
||||
this._data = {};
|
||||
this._open = false;
|
||||
@@ -393,23 +387,6 @@ export class DialogForm
|
||||
},
|
||||
];
|
||||
|
||||
if (this._anchor) {
|
||||
// A popover next to what opened it, or a bottom sheet on small screens
|
||||
return html`
|
||||
<ha-adaptive-popover
|
||||
.dialogAnchor=${this._anchor}
|
||||
compact
|
||||
.open=${this._open}
|
||||
.headerTitle=${params.title}
|
||||
.hideCloseButton=${true}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._renderForms(levels)} ${this._renderFooter(params)}
|
||||
</ha-adaptive-popover>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
@@ -417,52 +394,41 @@ export class DialogForm
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._renderForms(levels)} ${this._renderFooter(params)}
|
||||
${levels.map((level, index) => {
|
||||
const isActive = index === levels.length - 1;
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
?hidden=${!isActive}
|
||||
?autofocus=${isActive}
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${level.params.computeLabel}
|
||||
.computeHelper=${level.params.computeHelper}
|
||||
.data=${level.data}
|
||||
.schema=${level.params.schema}
|
||||
.error=${level.error}
|
||||
@value-changed=${this._valueChanged}
|
||||
@show-dialog=${this._handleNestedShowDialog}
|
||||
>
|
||||
</ha-form>
|
||||
`;
|
||||
})}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._cancel}
|
||||
>
|
||||
${params.cancelText || this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${params.submitText || this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderForms(
|
||||
levels: Pick<StackEntry, "params" | "data" | "error">[]
|
||||
) {
|
||||
return levels.map((level, index) => {
|
||||
const isActive = index === levels.length - 1;
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
?hidden=${!isActive}
|
||||
?autofocus=${isActive}
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${level.params.computeLabel}
|
||||
.computeHelper=${level.params.computeHelper}
|
||||
.data=${level.data}
|
||||
.schema=${level.params.schema}
|
||||
.error=${level.error}
|
||||
@value-changed=${this._valueChanged}
|
||||
@show-dialog=${this._handleNestedShowDialog}
|
||||
>
|
||||
</ha-form>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
private _renderFooter(params: FormDialogParams) {
|
||||
return html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._cancel}
|
||||
>
|
||||
${params.cancelText || this.hass?.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${params.submitText || this.hass?.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [haStyleDialog, css``];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,6 @@ export type FormDialogData = Record<string, any>;
|
||||
|
||||
export interface FormDialogParams {
|
||||
title: string;
|
||||
/**
|
||||
* The element that opened the form, such as the button that was clicked.
|
||||
* The form then opens as a popover next to it on larger screens, and as a
|
||||
* bottom sheet on smaller ones.
|
||||
*/
|
||||
anchor?: Element;
|
||||
schema: HaFormSchema[];
|
||||
data?: FormDialogData;
|
||||
// Extra submit-time validation beyond the schema's own checks. Returned
|
||||
@@ -36,7 +30,6 @@ export const showFormDialog = (
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-form",
|
||||
dialogImport: () => import("./dialog-form"),
|
||||
dialogAnchor: dialogParams.anchor,
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
cancel: () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-adaptive-popover";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
@@ -28,13 +27,8 @@ class DialogBox extends DirtyStateProviderMixin<DialogBoxDirtyState>()(
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// Set by the dialog manager before each showDialog
|
||||
@property({ attribute: false }) public dialogAnchor?: Element;
|
||||
|
||||
@state() private _params?: DialogBoxParams;
|
||||
|
||||
@state() private _anchor?: Element;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _closeState?: "canceled" | "confirmed";
|
||||
@@ -54,7 +48,6 @@ class DialogBox extends DirtyStateProviderMixin<DialogBoxDirtyState>()(
|
||||
await this._closePromise;
|
||||
}
|
||||
this._params = params;
|
||||
this._anchor = this.dialogAnchor;
|
||||
this._validInput = true;
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
@@ -91,42 +84,6 @@ class DialogBox extends DirtyStateProviderMixin<DialogBoxDirtyState>()(
|
||||
(this._params.confirmation &&
|
||||
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
|
||||
|
||||
if (this._anchor) {
|
||||
// With only a title, it reads as the message rather than a heading
|
||||
const titleOnly =
|
||||
!!dialogTitle &&
|
||||
!this._params.text &&
|
||||
!this._params.subtitle &&
|
||||
!this._params.prompt;
|
||||
|
||||
// A popover next to what opened it, or a bottom sheet on small screens.
|
||||
// The title is the popover's own, so it can name the dialog.
|
||||
return html`
|
||||
<ha-adaptive-popover
|
||||
.dialogAnchor=${this._anchor}
|
||||
compact
|
||||
class=${this._params.prompt ? "prompt" : ""}
|
||||
?without-header=${titleOnly}
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${!!this._params.confirmation || this.isDirtyState}
|
||||
.headerTitle=${titleOnly ? undefined : dialogTitle || undefined}
|
||||
.headerSubtitle=${this._params.subtitle}
|
||||
.accessibleLabel=${titleOnly ? dialogTitle : undefined}
|
||||
.hideCloseButton=${confirmPrompt && !this._params.showCloseButton}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${
|
||||
titleOnly
|
||||
? html`<div id="dialog-box-description">
|
||||
<p>${dialogTitle}</p>
|
||||
</div>`
|
||||
: this._renderDescription(this._params)
|
||||
}
|
||||
${this._renderFooter(this._params, confirmPrompt)}
|
||||
</ha-adaptive-popover>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
@@ -169,97 +126,95 @@ class DialogBox extends DirtyStateProviderMixin<DialogBoxDirtyState>()(
|
||||
: nothing
|
||||
}
|
||||
</ha-dialog-header>
|
||||
${this._renderDescription(this._params)}
|
||||
${this._renderFooter(this._params, confirmPrompt)}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderDescription(params: DialogBoxParams) {
|
||||
return html`
|
||||
<div id="dialog-box-description">
|
||||
${params.text ? html` <p>${params.text}</p> ` : ""}
|
||||
${
|
||||
params.prompt && !params.multiline
|
||||
? html`
|
||||
<ha-input
|
||||
autofocus
|
||||
value=${ifDefined(params.defaultValue)}
|
||||
.placeholder=${params.placeholder}
|
||||
.label=${params.inputLabel ? params.inputLabel : ""}
|
||||
.type=${params.inputType ? params.inputType : "text"}
|
||||
.min=${params.inputMin}
|
||||
.max=${params.inputMax}
|
||||
.disabled=${this._loading}
|
||||
@input=${this._validateInput}
|
||||
>
|
||||
${
|
||||
params.inputSuffix
|
||||
? html`<span slot="end">${params.inputSuffix}</span>`
|
||||
: nothing
|
||||
}
|
||||
</ha-input>
|
||||
`
|
||||
: params.prompt && params.multiline
|
||||
<div id="dialog-box-description">
|
||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||
${
|
||||
this._params.prompt && !this._params.multiline
|
||||
? html`
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
<ha-input
|
||||
autofocus
|
||||
.value=${params.defaultValue}
|
||||
.placeholder=${params.placeholder}
|
||||
.label=${params.inputLabel}
|
||||
value=${ifDefined(this._params.defaultValue)}
|
||||
.placeholder=${this._params.placeholder}
|
||||
.label=${
|
||||
this._params.inputLabel ? this._params.inputLabel : ""
|
||||
}
|
||||
.type=${
|
||||
this._params.inputType ? this._params.inputType : "text"
|
||||
}
|
||||
.min=${this._params.inputMin}
|
||||
.max=${this._params.inputMax}
|
||||
.disabled=${this._loading}
|
||||
@input=${this._validateInput}
|
||||
></ha-textarea>
|
||||
>
|
||||
${
|
||||
this._params.inputSuffix
|
||||
? html`<span slot="end"
|
||||
>${this._params.inputSuffix}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
</ha-input>
|
||||
`
|
||||
: this._params.prompt && this._params.multiline
|
||||
? html`
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
autofocus
|
||||
.value=${this._params.defaultValue}
|
||||
.placeholder=${this._params.placeholder}
|
||||
.label=${this._params.inputLabel}
|
||||
.disabled=${this._loading}
|
||||
@input=${this._validateInput}
|
||||
></ha-textarea>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._params.prompt && this._params.textBelowInput
|
||||
? html`<div class="after-input">
|
||||
${this._params.textBelowInput}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
${
|
||||
confirmPrompt
|
||||
? html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._dismiss}
|
||||
?autofocus=${!this._params.prompt && this._params.destructive}
|
||||
?disabled=${this._loading}
|
||||
appearance="plain"
|
||||
>
|
||||
${
|
||||
this._params.dismissText
|
||||
? this._params.dismissText
|
||||
: this.hass.localize("ui.common.cancel")
|
||||
}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.prompt && params.textBelowInput
|
||||
? html`<div class="after-input">${params.textBelowInput}</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderFooter(params: DialogBoxParams, confirmPrompt: boolean) {
|
||||
return html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
${
|
||||
confirmPrompt
|
||||
? html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._dismiss}
|
||||
?autofocus=${!params.prompt && params.destructive}
|
||||
?disabled=${this._loading}
|
||||
appearance="plain"
|
||||
>
|
||||
${
|
||||
params.dismissText
|
||||
? params.dismissText
|
||||
: this.hass.localize("ui.common.cancel")
|
||||
}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._confirm}
|
||||
?autofocus=${!params.prompt && !params.destructive}
|
||||
?disabled=${this._loading || (!!params.prompt && !this._validInput)}
|
||||
.loading=${this._loading}
|
||||
variant=${params.destructive ? "danger" : "brand"}
|
||||
>
|
||||
${
|
||||
params.confirmText
|
||||
? params.confirmText
|
||||
: this.hass.localize("ui.common.ok")
|
||||
}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._confirm}
|
||||
?autofocus=${!this._params.prompt && !this._params.destructive}
|
||||
?disabled=${
|
||||
this._loading || (!!this._params.prompt && !this._validInput)
|
||||
}
|
||||
.loading=${this._loading}
|
||||
variant=${this._params.destructive ? "danger" : "brand"}
|
||||
>
|
||||
${
|
||||
this._params.confirmText
|
||||
? this._params.confirmText
|
||||
: this.hass.localize("ui.common.ok")
|
||||
}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -332,7 +287,6 @@ class DialogBox extends DirtyStateProviderMixin<DialogBoxDirtyState>()(
|
||||
}
|
||||
this._closeState = undefined;
|
||||
this._params = undefined;
|
||||
this._anchor = undefined;
|
||||
this._open = false;
|
||||
this._loading = false;
|
||||
this._closeResolve?.();
|
||||
@@ -340,16 +294,6 @@ class DialogBox extends DirtyStateProviderMixin<DialogBoxDirtyState>()(
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-adaptive-popover {
|
||||
/* As wide as the message and buttons need, up to the screen width, so
|
||||
text only wraps when there is no room left */
|
||||
--ha-adaptive-popover-min-width: 0;
|
||||
--ha-adaptive-popover-width: 95vw;
|
||||
}
|
||||
ha-adaptive-popover.prompt {
|
||||
/* A text field has no width of its own to fit to */
|
||||
--ha-adaptive-popover-min-width: min(400px, 95vw);
|
||||
}
|
||||
:host([inert]) {
|
||||
pointer-events: initial !important;
|
||||
cursor: initial !important;
|
||||
|
||||
@@ -4,12 +4,6 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
interface BaseDialogBoxParams {
|
||||
/** Whether back closes the dialog. Off while a navigation is in flight. */
|
||||
addHistory?: boolean;
|
||||
/**
|
||||
* The element that opened the dialog, such as the button that was clicked.
|
||||
* The dialog then opens as a popover next to it on larger screens, and as a
|
||||
* bottom sheet on smaller ones.
|
||||
*/
|
||||
anchor?: Element;
|
||||
confirmText?: string;
|
||||
/** Show the close button for confirmations and prompts, which hide it by default. */
|
||||
showCloseButton?: boolean;
|
||||
@@ -72,7 +66,6 @@ const showDialogHelper = (
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-box",
|
||||
dialogImport: loadGenericDialog,
|
||||
dialogAnchor: dialogParams.anchor,
|
||||
addHistory: dialogParams.addHistory,
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { QueryParamsController } from "../common/controllers/query-params-controller";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
@@ -28,10 +27,7 @@ import {
|
||||
dataTableQueryStateFromUrl,
|
||||
} from "../common/url/data-table-query-params";
|
||||
import "../components/chips/ha-assist-chip";
|
||||
import {
|
||||
dataTableGroupKey,
|
||||
groupActionsSlot,
|
||||
} from "../components/data-table/ha-data-table";
|
||||
import "../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
@@ -635,13 +631,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
.columnOrder=${this.columnOrder}
|
||||
.hiddenColumns=${this.hiddenColumns}
|
||||
>
|
||||
${this._groups(this.data, this._groupColumn).map(
|
||||
(group) =>
|
||||
html`<slot
|
||||
name=${groupActionsSlot(group)}
|
||||
slot=${groupActionsSlot(group)}
|
||||
></slot>`
|
||||
)}
|
||||
${
|
||||
!this.narrow
|
||||
? html`
|
||||
@@ -857,23 +846,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this._setGroupColumn(group);
|
||||
}
|
||||
|
||||
// Groups to pass the actions slots of through to the table
|
||||
private _groups = memoizeOne(
|
||||
(data: DataTableRowData[], groupColumn: string | undefined) => {
|
||||
if (!groupColumn) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groups = new Set<string>();
|
||||
|
||||
for (const row of data) {
|
||||
groups.add(dataTableGroupKey(row[groupColumn]));
|
||||
}
|
||||
|
||||
return [...groups];
|
||||
}
|
||||
);
|
||||
|
||||
private _setGroupColumn(columnId: string, link = false) {
|
||||
this._groupColumn = columnId;
|
||||
fireEvent(this, "grouping-changed", { value: columnId, link });
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button";
|
||||
@@ -131,13 +130,10 @@ export class HaConfigAppsRegistries extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _removeRegistry = async (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { registry: string }>
|
||||
) => {
|
||||
const registry = ev.currentTarget.registry;
|
||||
private _removeRegistry = async (ev: Event) => {
|
||||
const registry = (ev.currentTarget as any).registry;
|
||||
|
||||
showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.apps.registries.remove"),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.apps.registries.confirm_remove",
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
@@ -221,14 +218,11 @@ export class HaConfigAppsRepositories extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _removeRepository = async (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { slug: string }>
|
||||
) => {
|
||||
const slug = ev.currentTarget.slug;
|
||||
private _removeRepository = async (ev: Event) => {
|
||||
const slug = (ev.currentTarget as any).slug;
|
||||
const repo = this._repositories?.find((r) => r.slug === slug);
|
||||
|
||||
showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.apps.repositories.remove"),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.apps.repositories.confirm_remove",
|
||||
|
||||
@@ -225,8 +225,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@query("ha-automation-row")
|
||||
private _automationRowElement?: HaAutomationRow;
|
||||
|
||||
@query(".menu-trigger") private _menuTrigger?: HTMLElement;
|
||||
|
||||
private _runResultTimeout?: number;
|
||||
|
||||
private _sidebarAction?: Action;
|
||||
@@ -487,7 +485,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
class="menu-trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
@@ -1016,9 +1013,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
private _renameAction = async (anchor?: Element): Promise<void> => {
|
||||
private _renameAction = async (): Promise<void> => {
|
||||
const alias = await showPromptDialog(this, {
|
||||
anchor,
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.change_alias"
|
||||
),
|
||||
@@ -1307,8 +1303,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._runAction();
|
||||
break;
|
||||
case "rename":
|
||||
// The menu has closed, so open next to its button
|
||||
this._renameAction(this._menuTrigger);
|
||||
this._renameAction();
|
||||
break;
|
||||
case "edit_note":
|
||||
this._editNoteAction();
|
||||
|
||||
@@ -181,8 +181,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
@query("ha-automation-row")
|
||||
private _automationRowElement?: HaAutomationRow;
|
||||
|
||||
@query(".menu-trigger") private _menuTrigger?: HTMLElement;
|
||||
|
||||
@query(".triggered-chip")
|
||||
private _eventChipElement?: HaAutomationRowEventChip;
|
||||
|
||||
@@ -393,7 +391,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
class="menu-trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
@@ -927,10 +924,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _renameTrigger = async (anchor?: Element): Promise<void> => {
|
||||
private _renameTrigger = async (): Promise<void> => {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const alias = await showPromptDialog(this, {
|
||||
anchor,
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.change_alias"
|
||||
),
|
||||
@@ -1178,8 +1174,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
switch (action) {
|
||||
case "rename":
|
||||
// The menu has closed, so open next to its button
|
||||
this._renameTrigger(this._menuTrigger);
|
||||
this._renameTrigger();
|
||||
break;
|
||||
case "edit_note":
|
||||
this._editNoteTrigger();
|
||||
|
||||
@@ -205,7 +205,6 @@ export class EnergyBatterySettings extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -269,7 +269,6 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -266,7 +266,6 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -197,7 +197,6 @@ export class EnergyGasSettings extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -261,7 +261,6 @@ export class EnergyGridSettings extends LitElement {
|
||||
const entryId = ev.currentTarget.closest(".row").entry.entry_id;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_integration"),
|
||||
}))
|
||||
) {
|
||||
@@ -324,7 +323,6 @@ export class EnergyGridSettings extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -207,7 +207,6 @@ export class EnergySolarSettings extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -186,7 +186,6 @@ export class EnergyWaterSettings extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
@@ -188,13 +185,10 @@ class HaInputSelectForm extends LitElement {
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
private async _removeOption(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { index: number }>
|
||||
) {
|
||||
const index = ev.currentTarget.index;
|
||||
private async _removeOption(ev: Event) {
|
||||
const index = (ev.target as any).index;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: ev.currentTarget,
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_select.confirm_delete.delete"
|
||||
),
|
||||
|
||||
@@ -284,7 +284,6 @@ export class HaConfigLovelaceResources extends LitElement {
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
anchor: event.currentTarget,
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.resources.confirm_delete_title"
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -50,8 +50,6 @@ export class HuiSectionEditMode extends LitElement {
|
||||
@property({ type: Boolean, attribute: "is-strategy", reflect: true })
|
||||
public isStrategy = false;
|
||||
|
||||
@query(".menu-trigger") private _menuTrigger?: HTMLElement;
|
||||
|
||||
// Session storage keeps a copied or cut section while switching views and
|
||||
// dashboards.
|
||||
@state()
|
||||
@@ -97,7 +95,6 @@ export class HuiSectionEditMode extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
class="menu-trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
@@ -295,8 +292,6 @@ export class HuiSectionEditMode extends LitElement {
|
||||
|
||||
if (hasContent) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
// The menu has closed, so open next to its button
|
||||
anchor: this._menuTrigger,
|
||||
title: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.delete_section.title"
|
||||
),
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiAutoFix, mdiContentPaste, mdiViewGridPlusOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { consume } from "../../../../common/decorators/consume";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import "../../../../components/ha-adaptive-dialog";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import { internationalizationContext } from "../../../../data/context";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
|
||||
import { generateDefaultSection } from "../../views/default-section";
|
||||
import { addSection } from "../config-util";
|
||||
import type { CreateSectionDialogParams } from "./show-create-section-dialog";
|
||||
import { showSelectSectionStrategyDialog } from "./show-create-section-dialog";
|
||||
|
||||
@customElement("hui-dialog-create-section")
|
||||
export class HuiDialogCreateSection extends DialogMixin<CreateSectionDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "dashboardSectionClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard?: LovelaceSectionRawConfig;
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const localize = this._i18n.localize;
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
open
|
||||
header-title=${localize("ui.panel.lovelace.editor.create_section.header")}
|
||||
>
|
||||
<ha-list-base>
|
||||
<ha-list-item-button @click=${this._createManual}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiViewGridPlusOutline}
|
||||
></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${localize("ui.panel.lovelace.editor.create_section.manual")}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${localize(
|
||||
"ui.panel.lovelace.editor.create_section.manual_description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button @click=${this._pickStrategy}>
|
||||
<ha-svg-icon slot="start" .path=${mdiAutoFix}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${localize("ui.panel.lovelace.editor.create_section.automatic")}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${localize(
|
||||
"ui.panel.lovelace.editor.create_section.automatic_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
${
|
||||
this._clipboard
|
||||
? html`
|
||||
<ha-list-item-button @click=${this._paste}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${localize("ui.panel.lovelace.editor.create_section.paste")}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${localize(
|
||||
"ui.panel.lovelace.editor.create_section.paste_description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-base>
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createManual() {
|
||||
const { lovelace, containerPath } = this.params!;
|
||||
this.closeDialog();
|
||||
lovelace.saveConfig(
|
||||
addSection(
|
||||
lovelace.config,
|
||||
containerPath,
|
||||
generateDefaultSection(this._i18n.localize, true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _pickStrategy() {
|
||||
showSelectSectionStrategyDialog(this, this.params!);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _paste() {
|
||||
if (!this._clipboard) {
|
||||
return;
|
||||
}
|
||||
const { lovelace, containerPath } = this.params!;
|
||||
const section = deepClone(this._clipboard);
|
||||
this.closeDialog();
|
||||
try {
|
||||
await lovelace.saveConfig(
|
||||
addSection(lovelace.config, containerPath, section)
|
||||
);
|
||||
} catch (_err: unknown) {
|
||||
lovelace.showToast({
|
||||
message: this._i18n.localize(
|
||||
"ui.panel.lovelace.editor.section.paste_error"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-adaptive-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-list-base {
|
||||
--ha-list-padding: 0 0 var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-create-section": HuiDialogCreateSection;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,38 @@
|
||||
import { mdiClose, mdiDotsVertical, mdiFileMoveOutline } from "@mdi/js";
|
||||
import {
|
||||
mdiCellphone,
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiFileMoveOutline,
|
||||
mdiMonitor,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../../data/lovelace/config/section";
|
||||
import {
|
||||
DEFAULT_SECTION_COLUMN_SPAN,
|
||||
isStrategySection,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { saveConfig } from "../../../../data/lovelace/config/types";
|
||||
@@ -32,6 +44,7 @@ import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { cleanLegacyStrategyConfig } from "../../strategies/legacy-strategy";
|
||||
@@ -41,10 +54,13 @@ import {
|
||||
deleteAtPath,
|
||||
getAtPath,
|
||||
getViewPath,
|
||||
insertAtPath,
|
||||
moveAtPath,
|
||||
setAtPath,
|
||||
} from "../lovelace-path";
|
||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||
import "../../sections/hui-section";
|
||||
import "../../sections/hui-section-background";
|
||||
import "./hui-section-settings-editor";
|
||||
import "./hui-section-visibility-editor";
|
||||
import "./hui-section-strategy-element-editor";
|
||||
@@ -53,11 +69,20 @@ import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import type { EditSectionDialogParams } from "./show-edit-section-dialog";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import { getViewType } from "../../views/get-view-type";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "../../views/const";
|
||||
import { DEFAULT_MAX_COLUMNS, SECTIONS_VIEW_LAYOUT } from "../../views/const";
|
||||
import { LOVELACE_EDIT_TOAST_ID } from "../../types";
|
||||
|
||||
const TABS = ["tab-strategy", "tab-appearance", "tab-visibility"] as const;
|
||||
|
||||
type PreviewMode = "desktop" | "mobile";
|
||||
|
||||
const PREVIEW_MODES: { mode: PreviewMode; iconPath: string }[] = [
|
||||
{ mode: "mobile", iconPath: mdiCellphone },
|
||||
{ mode: "desktop", iconPath: mdiMonitor },
|
||||
];
|
||||
|
||||
const TWO_COLUMNS_MIN_WIDTH = 2 * 320 + 3 * 32;
|
||||
|
||||
@customElement("hui-dialog-edit-section")
|
||||
export class HuiDialogEditSection
|
||||
extends DirtyStateProviderMixin<LovelaceSectionRawConfig>()(LitElement)
|
||||
@@ -67,6 +92,8 @@ export class HuiDialogEditSection
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@state() private _params?: EditSectionDialogParams;
|
||||
|
||||
@state() private _config?: LovelaceSectionRawConfig;
|
||||
@@ -83,6 +110,15 @@ export class HuiDialogEditSection
|
||||
|
||||
@state() private _yamlError = false;
|
||||
|
||||
@storage({
|
||||
key: "dashboardSectionPreviewMode",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedPreviewMode?: PreviewMode;
|
||||
|
||||
@state() private _defaultPreviewMode: PreviewMode = "mobile";
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
@query("hui-section-strategy-element-editor")
|
||||
@@ -104,10 +140,9 @@ export class HuiDialogEditSection
|
||||
|
||||
this.lovelace = params.lovelace;
|
||||
|
||||
this._config = getAtPath<LovelaceSectionRawConfig>(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
this._config =
|
||||
params.newSection ??
|
||||
getAtPath<LovelaceSectionRawConfig>(params.lovelaceConfig, params.path);
|
||||
this._currTab =
|
||||
this._config && isStrategySection(this._config)
|
||||
? "tab-strategy"
|
||||
@@ -117,7 +152,18 @@ export class HuiDialogEditSection
|
||||
params.lovelaceConfig,
|
||||
viewPath
|
||||
);
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
if (params.newSection) {
|
||||
this._initDirtyTracking({ type: "deep" }, {});
|
||||
this._updateDirtyState(params.newSection);
|
||||
} else {
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
}
|
||||
this._defaultPreviewMode =
|
||||
window.innerWidth >= TWO_COLUMNS_MIN_WIDTH ? "desktop" : "mobile";
|
||||
}
|
||||
|
||||
private get _previewMode(): PreviewMode {
|
||||
return this._storedPreviewMode ?? this._defaultPreviewMode;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -127,11 +173,13 @@ export class HuiDialogEditSection
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this.large = false;
|
||||
this._yamlMode = false;
|
||||
this._config = undefined;
|
||||
this._currTab = TABS[0];
|
||||
this._strategyError = false;
|
||||
this._yamlError = false;
|
||||
this._discardDirtyStateChanges();
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -140,19 +188,23 @@ export class HuiDialogEditSection
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.header"
|
||||
);
|
||||
|
||||
const strategy = isStrategySection(this._config)
|
||||
? cleanLegacyStrategyConfig(this._config.strategy)
|
||||
: undefined;
|
||||
const heading = this._computeHeading(strategy);
|
||||
const tabs = strategy ? TABS : TABS.filter((tab) => tab !== "tab-strategy");
|
||||
const currentTab =
|
||||
!strategy && this._currTab === "tab-strategy"
|
||||
? "tab-appearance"
|
||||
: this._currTab;
|
||||
|
||||
const columnSpan = Math.min(
|
||||
this._config.column_span ?? DEFAULT_SECTION_COLUMN_SPAN,
|
||||
this._viewConfig?.max_columns || DEFAULT_MAX_COLUMNS
|
||||
);
|
||||
const singleColumn = columnSpan === 1;
|
||||
const previewColumnSpan = this._previewMode === "desktop" ? columnSpan : 1;
|
||||
|
||||
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||
|
||||
if (this._yamlMode) {
|
||||
@@ -202,65 +254,160 @@ export class HuiDialogEditSection
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.width=${this.large ? "full" : "large"}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${classMap({
|
||||
"yaml-mode": this._yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._cancel}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${heading}</span>
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
placement="bottom-end"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="move-to-view">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFileMoveOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_view"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${
|
||||
!this._yamlMode
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${currentTab === tab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this._cancel}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span
|
||||
slot="headerTitle"
|
||||
class="title-enlargeable"
|
||||
@click=${this._enlarge}
|
||||
>${heading}</span
|
||||
>
|
||||
${
|
||||
this._params.newSection
|
||||
? nothing
|
||||
: html`
|
||||
<ha-dropdown
|
||||
slot="headerActionItems"
|
||||
placement="bottom-end"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="move-to-view">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFileMoveOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_view"
|
||||
)}
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-dialog-header>
|
||||
${this._yamlMode ? content : cache(content)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
}
|
||||
<div class="content">
|
||||
<div class="element-editor ha-scrollbar">
|
||||
${
|
||||
!this._yamlMode
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${currentTab === tab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${this._yamlMode ? content : cache(content)}
|
||||
</div>
|
||||
<div class="element-preview ha-scrollbar">
|
||||
<div class="preview-header">
|
||||
<span id="preview-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.preview"
|
||||
)}
|
||||
</span>
|
||||
${
|
||||
singleColumn
|
||||
? nothing
|
||||
: html`
|
||||
<div
|
||||
class="preview-modes"
|
||||
role="group"
|
||||
aria-labelledby="preview-label"
|
||||
>
|
||||
${PREVIEW_MODES.map(
|
||||
({ mode, iconPath }) => html`
|
||||
<button
|
||||
id="preview-mode-${mode}"
|
||||
class=${classMap({
|
||||
"preview-mode": true,
|
||||
active: this._previewMode === mode,
|
||||
})}
|
||||
aria-pressed=${this._previewMode === mode}
|
||||
aria-label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.preview_${mode}`
|
||||
)}
|
||||
.mode=${mode}
|
||||
@click=${this._previewModeClicked}
|
||||
>
|
||||
<ha-svg-icon .path=${iconPath}></ha-svg-icon>
|
||||
</button>
|
||||
<ha-tooltip
|
||||
for="preview-mode-${mode}"
|
||||
placement="bottom"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.preview_${mode}`
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
<div class="preview-frame">
|
||||
<div
|
||||
class=${classMap({
|
||||
"section-preview": true,
|
||||
wide: previewColumnSpan > 1,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--column-span": previewColumnSpan,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
"section-container": true,
|
||||
"has-background": this._config.background !== undefined,
|
||||
blur: this._yamlError || this._strategyError,
|
||||
})}
|
||||
>
|
||||
${
|
||||
this._config.background !== undefined
|
||||
? html`
|
||||
<hui-section-background
|
||||
.hass=${this.hass}
|
||||
.background=${this._config.background}
|
||||
.theme=${this._config.theme}
|
||||
></hui-section-background>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
preview
|
||||
></hui-section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -319,6 +466,37 @@ export class HuiDialogEditSection
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
private _computeHeading(strategy?: LovelaceStrategyConfig) {
|
||||
const header = this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.header"
|
||||
);
|
||||
if (!strategy) {
|
||||
return header;
|
||||
}
|
||||
const strategyName = this.hass.localize(
|
||||
`ui.panel.lovelace.editor.section_strategy.${strategy.type}.name`
|
||||
);
|
||||
if (!strategyName) {
|
||||
return header;
|
||||
}
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.typed_header",
|
||||
{ type: strategyName }
|
||||
);
|
||||
}
|
||||
|
||||
private _previewModeClicked(ev: Event) {
|
||||
this._storedPreviewMode = (
|
||||
ev.currentTarget as HTMLElement & { mode: PreviewMode }
|
||||
).mode;
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
withViewTransition(() => {
|
||||
this.large = !this.large;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.name;
|
||||
if (newTab === this._currTab) {
|
||||
@@ -516,11 +694,13 @@ export class HuiDialogEditSection
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newConfig = setAtPath(
|
||||
this._params.lovelaceConfig,
|
||||
this._params.path,
|
||||
this._config
|
||||
);
|
||||
const newConfig = this._params.newSection
|
||||
? insertAtPath(
|
||||
this._params.lovelaceConfig,
|
||||
this._params.path,
|
||||
this._config
|
||||
)
|
||||
: setAtPath(this._params.lovelaceConfig, this._params.path, this._config);
|
||||
|
||||
this._params.saveConfig(newConfig);
|
||||
this._markDirtyStateClean();
|
||||
@@ -531,15 +711,153 @@ export class HuiDialogEditSection
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 209px);
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-content-padding: var(--ha-space-6);
|
||||
--dialog-content-padding: var(--ha-space-2);
|
||||
}
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-inline-end: auto;
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.content {
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
:host([large]) .content {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content .preview-frame {
|
||||
padding: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.content .section-preview {
|
||||
--preview-column-width: 390px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: var(--preview-column-width);
|
||||
}
|
||||
.content .section-preview.wide {
|
||||
max-width: none;
|
||||
width: calc(
|
||||
var(--column-span) * var(--preview-column-width) +
|
||||
(var(--column-span) - 1) *
|
||||
var(--ha-view-sections-column-gap, 32px)
|
||||
);
|
||||
}
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
min-height: 32px;
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.preview-modes {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
}
|
||||
.preview-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
background: none;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.preview-mode:hover {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.preview-mode.active {
|
||||
color: var(--primary-text-color);
|
||||
background: var(--card-background-color);
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
|
||||
}
|
||||
.preview-mode:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.content .element-editor {
|
||||
padding-inline-start: var(--ha-space-1);
|
||||
padding-inline-end: var(--ha-space-2);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.content {
|
||||
flex-direction: row;
|
||||
max-height: var(--code-mirror-max-height);
|
||||
}
|
||||
.content > .element-editor {
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
max-width: 500px;
|
||||
}
|
||||
.content > .element-editor,
|
||||
.content > .element-preview {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
}
|
||||
.content .preview-frame {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.blur {
|
||||
filter: blur(2px) grayscale(100%);
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
height: max-content;
|
||||
background: var(--primary-background-color);
|
||||
padding: 4px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.section-container {
|
||||
position: relative;
|
||||
}
|
||||
.section-container.has-background {
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
}
|
||||
.section-container hui-section {
|
||||
display: block;
|
||||
}
|
||||
ha-tab-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
@@ -548,6 +866,14 @@ export class HuiDialogEditSection
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.title-enlargeable {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-adaptive-dialog";
|
||||
import "../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../components/ha-ripple";
|
||||
import type { LovelaceStrategySectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { CUSTOM_TYPE_PREFIX } from "../../../../data/lovelace_custom_cards";
|
||||
import { getCustomStrategiesForType } from "../../../../data/lovelace_custom_strategies";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../sections/hui-section";
|
||||
import { getLovelaceStrategy } from "../../strategies/get-strategy";
|
||||
import type { LovelaceSectionStrategy } from "../../strategies/types";
|
||||
import { getAtPath } from "../lovelace-path";
|
||||
import type { CreateSectionDialogParams } from "./show-create-section-dialog";
|
||||
import { showCreateSectionDialog } from "./show-create-section-dialog";
|
||||
import { showEditSectionDialog } from "./show-edit-section-dialog";
|
||||
|
||||
interface BuiltInSectionStrategy {
|
||||
type: "common-controls" | "device";
|
||||
isAvailable?: (hass: HomeAssistant) => boolean;
|
||||
}
|
||||
|
||||
const BUILT_IN_SECTION_STRATEGIES: BuiltInSectionStrategy[] = [
|
||||
{
|
||||
type: "common-controls",
|
||||
isAvailable: (hass) => isComponentLoaded(hass.config, "usage_prediction"),
|
||||
},
|
||||
{
|
||||
type: "device",
|
||||
},
|
||||
];
|
||||
|
||||
interface StrategyOption {
|
||||
type: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@customElement("hui-dialog-select-section-strategy")
|
||||
export class HuiDialogSelectSectionStrategy
|
||||
extends LitElement
|
||||
implements HassDialog<CreateSectionDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: CreateSectionDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _previews: Record<string, LovelaceStrategySectionConfig> =
|
||||
{};
|
||||
|
||||
public showDialog(params: CreateSectionDialogParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
this._previews = {};
|
||||
this._loadPreviews(params);
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._previews = {};
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _availableBuiltInTypes() {
|
||||
return BUILT_IN_SECTION_STRATEGIES.filter(
|
||||
(strategy) => strategy.isAvailable?.(this.hass) ?? true
|
||||
).map((strategy) => strategy.type);
|
||||
}
|
||||
|
||||
private _builtInOptions(): StrategyOption[] {
|
||||
return this._availableBuiltInTypes().map((type) => ({
|
||||
type,
|
||||
name: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.section_strategy.${type}.name`
|
||||
),
|
||||
description: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.section_strategy.${type}.description`
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
private _customOptions(): StrategyOption[] {
|
||||
return getCustomStrategiesForType("section").map((strategy) => ({
|
||||
type: CUSTOM_TYPE_PREFIX + strategy.type,
|
||||
name: strategy.name || strategy.type,
|
||||
description: strategy.description || "",
|
||||
}));
|
||||
}
|
||||
|
||||
private async _loadPreviews(params: CreateSectionDialogParams) {
|
||||
const types = [
|
||||
...this._availableBuiltInTypes(),
|
||||
...getCustomStrategiesForType("section").map(
|
||||
(strategy) => CUSTOM_TYPE_PREFIX + strategy.type
|
||||
),
|
||||
];
|
||||
await Promise.all(
|
||||
types.map(async (type) => {
|
||||
try {
|
||||
const strategy = (await getLovelaceStrategy(
|
||||
"section",
|
||||
type
|
||||
)) as LovelaceSectionStrategy;
|
||||
const stubConfig = await strategy.getStubConfig?.(this.hass);
|
||||
if (!stubConfig || this._params !== params) {
|
||||
return;
|
||||
}
|
||||
this._previews = {
|
||||
...this._previews,
|
||||
[type]: { strategy: stubConfig },
|
||||
};
|
||||
} catch (_err: unknown) {
|
||||
// The tile is shown without a preview
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const customOptions = this._customOptions();
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
width="large"
|
||||
@closed=${this._dialogClosed}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_section_strategy.header"
|
||||
)}
|
||||
>
|
||||
<ha-icon-button-arrow-prev
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this._back}
|
||||
></ha-icon-button-arrow-prev>
|
||||
${
|
||||
customOptions.length
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_section_strategy.built_in"
|
||||
)}
|
||||
</h3>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="options">
|
||||
${this._builtInOptions().map((option) => this._renderOption(option))}
|
||||
</div>
|
||||
${
|
||||
customOptions.length
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.select_section_strategy.custom"
|
||||
)}
|
||||
</h3>
|
||||
<div class="options">
|
||||
${customOptions.map((option) => this._renderOption(option))}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOption(option: StrategyOption) {
|
||||
const preview = this._previews[option.type];
|
||||
return html`
|
||||
<div
|
||||
class="option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label=${option.name}
|
||||
data-type=${option.type}
|
||||
@click=${this._strategyPicked}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
<div class="header">
|
||||
<h4>${option.name}</h4>
|
||||
<p>${option.description}</p>
|
||||
</div>
|
||||
${
|
||||
preview
|
||||
? html`
|
||||
<div class="preview" inert>
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${preview}
|
||||
preview
|
||||
></hui-section>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-ripple></ha-ripple>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
(ev.currentTarget as HTMLElement).click();
|
||||
}
|
||||
}
|
||||
|
||||
private _back() {
|
||||
showCreateSectionDialog(this, this._params!);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _strategyPicked(ev: Event) {
|
||||
const type = (ev.currentTarget as HTMLElement).dataset.type!;
|
||||
const { lovelace, containerPath } = this._params!;
|
||||
const sectionsPath = [...containerPath, "sections"];
|
||||
const sectionCount =
|
||||
getAtPath<unknown[]>(lovelace.config, sectionsPath)?.length ?? 0;
|
||||
|
||||
showEditSectionDialog(this, {
|
||||
lovelace,
|
||||
lovelaceConfig: lovelace.config,
|
||||
saveConfig: (newConfig) => {
|
||||
lovelace.saveConfig(newConfig);
|
||||
},
|
||||
path: [...sectionsPath, sectionCount],
|
||||
newSection: this._previews[type] ?? { strategy: { type } },
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
h3 {
|
||||
margin: 0 0 var(--ha-space-3) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.options + h3 {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
.options {
|
||||
display: grid;
|
||||
gap: var(--ha-space-4);
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
|
||||
}
|
||||
.option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.option:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.header {
|
||||
flex: 1;
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
}
|
||||
h4 {
|
||||
margin: 0 0 var(--ha-space-1) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.preview {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
padding: var(--ha-space-3);
|
||||
background: var(--primary-background-color);
|
||||
pointer-events: none;
|
||||
mask-image: linear-gradient(to bottom, black 75%, transparent);
|
||||
}
|
||||
hui-section {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-select-section-strategy": HuiDialogSelectSectionStrategy;
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,71 @@ import {
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import { DEFAULT_MAX_COLUMNS } from "../../views/const";
|
||||
|
||||
const COLUMN_SPAN_NAMES = ["single", "double", "triple"] as const;
|
||||
|
||||
const MAX_NAMED_COLUMN_SPAN = COLUMN_SPAN_NAMES.length + 1;
|
||||
|
||||
const computeColumnSpanLabel = (
|
||||
span: number,
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
if (span >= maxColumns) {
|
||||
return localize(
|
||||
"ui.panel.lovelace.editor.edit_section.settings.column_span_options.full"
|
||||
);
|
||||
}
|
||||
return localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.column_span_options.${COLUMN_SPAN_NAMES[span - 1]}`
|
||||
);
|
||||
};
|
||||
|
||||
const computeColumnSpanSchemas = (
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
if (maxColumns <= 1) {
|
||||
return [];
|
||||
}
|
||||
if (maxColumns > MAX_NAMED_COLUMN_SPAN) {
|
||||
return [
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: { min: 1, max: maxColumns, slider_ticks: true },
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
const options = Array.from({ length: maxColumns }, (_, i) => {
|
||||
const span = i + 1;
|
||||
return {
|
||||
value: String(span),
|
||||
label: computeColumnSpanLabel(span, maxColumns, localize),
|
||||
image: {
|
||||
src: `/static/images/form/section_column_span_${span}_of_${maxColumns}.svg`,
|
||||
src_dark: `/static/images/form/section_column_span_${span}_of_${maxColumns}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
box_max_columns:
|
||||
maxColumns === MAX_NAMED_COLUMN_SPAN ? 2 : maxColumns,
|
||||
options,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
|
||||
interface SettingsData {
|
||||
column_span?: number;
|
||||
column_span?: string | number;
|
||||
background_enabled?: boolean;
|
||||
background_color?: string;
|
||||
background_opacity?: number;
|
||||
@@ -36,20 +99,30 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
private get _maxColumns() {
|
||||
return this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS;
|
||||
}
|
||||
|
||||
private get _displayedColumnSpan() {
|
||||
return String(
|
||||
Math.min(
|
||||
this.config.column_span ?? DEFAULT_SECTION_COLUMN_SPAN,
|
||||
this._maxColumns
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private get _columnSpanFormValue() {
|
||||
if (this._maxColumns > MAX_NAMED_COLUMN_SPAN) {
|
||||
return Number(this._displayedColumnSpan);
|
||||
}
|
||||
return this._displayedColumnSpan;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number, localize: LocalizeFunc) =>
|
||||
(localize: LocalizeFunc, maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
default: DEFAULT_SECTION_COLUMN_SPAN,
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
...computeColumnSpanSchemas(maxColumns, localize),
|
||||
{
|
||||
name: "background_enabled",
|
||||
selector: { boolean: {} },
|
||||
@@ -109,17 +182,14 @@ export class HuiDialogEditSection extends LitElement {
|
||||
const backgroundEnabled = background !== undefined;
|
||||
|
||||
const data: SettingsData = {
|
||||
column_span: this.config.column_span,
|
||||
column_span: this._columnSpanFormValue,
|
||||
background_enabled: backgroundEnabled,
|
||||
background_color: background?.color,
|
||||
background_opacity: background?.opacity,
|
||||
theme: this.config.theme,
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS,
|
||||
this._localize
|
||||
);
|
||||
const schema = this._schema(this._localize, this._maxColumns);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -154,10 +224,15 @@ export class HuiDialogEditSection extends LitElement {
|
||||
...this.config,
|
||||
};
|
||||
|
||||
if (newData.column_span) {
|
||||
newConfig.column_span = newData.column_span;
|
||||
} else {
|
||||
if (newData.column_span === undefined) {
|
||||
delete newConfig.column_span;
|
||||
} else if (String(newData.column_span) !== this._displayedColumnSpan) {
|
||||
const columnSpan = Number(newData.column_span);
|
||||
if (columnSpan === DEFAULT_SECTION_COLUMN_SPAN) {
|
||||
delete newConfig.column_span;
|
||||
} else {
|
||||
newConfig.column_span = columnSpan;
|
||||
}
|
||||
}
|
||||
|
||||
if (newData.background_enabled) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { Lovelace } from "../../types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface CreateSectionDialogParams {
|
||||
lovelace: Lovelace;
|
||||
containerPath: LovelacePath;
|
||||
}
|
||||
|
||||
export const showCreateSectionDialog = (
|
||||
element: HTMLElement,
|
||||
params: CreateSectionDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-create-section",
|
||||
dialogImport: () => import("./hui-dialog-create-section"),
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
|
||||
export const showSelectSectionStrategyDialog = (
|
||||
element: HTMLElement,
|
||||
params: CreateSectionDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-select-section-strategy",
|
||||
dialogImport: () => import("./hui-dialog-select-section-strategy"),
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { Lovelace } from "../../types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
@@ -8,6 +9,7 @@ export interface EditSectionDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelacePath;
|
||||
newSection?: LovelaceSectionRawConfig;
|
||||
}
|
||||
|
||||
const importEditSectionDialog = () => import("./hui-dialog-edit-section");
|
||||
|
||||
@@ -279,7 +279,8 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
}
|
||||
|
||||
private async _initializeConfig() {
|
||||
let sectionConfig = { ...this.config };
|
||||
const config = this.config;
|
||||
let sectionConfig = { ...config };
|
||||
let isStrategy = false;
|
||||
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
@@ -288,6 +289,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
sectionConfig,
|
||||
this.hass!
|
||||
);
|
||||
if (config !== this.config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sectionConfig = {
|
||||
|
||||
@@ -35,7 +35,11 @@ export type LovelaceDashboardStrategyGetCreateSuggestions = (
|
||||
|
||||
export interface LovelaceViewStrategy extends LovelaceStrategy<LovelaceViewConfig> {}
|
||||
|
||||
export interface LovelaceSectionStrategy extends LovelaceStrategy<LovelaceSectionConfig> {}
|
||||
export interface LovelaceSectionStrategy extends LovelaceStrategy<LovelaceSectionConfig> {
|
||||
getStubConfig?(
|
||||
hass: HomeAssistant
|
||||
): LovelaceStrategyConfig | Promise<LovelaceStrategyConfig>;
|
||||
}
|
||||
|
||||
export interface LovelaceStrategyEditor extends LovelaceGenericElementEditor {
|
||||
setConfig(config: LovelaceStrategyConfig): void;
|
||||
|
||||
@@ -40,6 +40,10 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
);
|
||||
}
|
||||
|
||||
public static getStubConfig(): CommonControlsSectionStrategyConfig {
|
||||
return { type: "common-controls" };
|
||||
}
|
||||
|
||||
static async generate(
|
||||
config: CommonControlsSectionStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -30,6 +30,7 @@ import "../components/hui-section-edit-mode";
|
||||
import { addSection } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { getAtPath, moveAtPath } from "../editor/lovelace-path";
|
||||
import { showCreateSectionDialog } from "../editor/section-editor/show-create-section-dialog";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import {
|
||||
renderSectionContainer,
|
||||
@@ -462,12 +463,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
private _createSection(): void {
|
||||
const newConfig = addSection(
|
||||
this.lovelace!.config,
|
||||
this.path!,
|
||||
generateDefaultSection(this.hass.localize, true)
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
showCreateSectionDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
containerPath: this.path!,
|
||||
});
|
||||
}
|
||||
|
||||
private _sectionMoved(ev: CustomEvent) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import {
|
||||
mdiCheckAll,
|
||||
mdiCheckCircleOutline,
|
||||
mdiCompassOutline,
|
||||
mdiDotsVertical,
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
sortedMarketplaceFilterQueryString,
|
||||
} from "../../../common/url/marketplace-query-params";
|
||||
import { shallowEqual } from "../../../common/util/shallow-equal";
|
||||
import { groupActionsSlot } from "../../../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
SelectionChangedEvent,
|
||||
@@ -407,14 +405,6 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
@sorting-changed=${this._tableSettings.sortingChanged}
|
||||
>
|
||||
${this._renderToolbar()}
|
||||
<ha-icon-button
|
||||
slot=${groupActionsSlot(
|
||||
this._i18n.localize("ui.panel.marketplace.repository_status.new")
|
||||
)}
|
||||
.label=${this._i18n.localize("ui.panel.marketplace.menu.dismiss")}
|
||||
.path=${mdiCheckAll}
|
||||
@click=${this._dismissNewFromGroup}
|
||||
></ha-icon-button>
|
||||
${
|
||||
this.tab === "installed"
|
||||
? html`<ha-assist-chip
|
||||
@@ -952,48 +942,18 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
showMarketplaceAddFromLink(this, this._i18n.localize, this.marketplace);
|
||||
}
|
||||
|
||||
private _dismissNewFromGroup(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
|
||||
this._dismissNew(ev.currentTarget);
|
||||
}
|
||||
|
||||
private _dismissNew(anchor?: Element) {
|
||||
showConfirmationDialog(this, {
|
||||
anchor,
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.marketplace.dialog.dismiss_new.title"
|
||||
),
|
||||
// Dismissing clears all of them, not only the ones the filters show
|
||||
text: this._i18n.localize(
|
||||
"ui.panel.marketplace.dialog.dismiss_new.text",
|
||||
{
|
||||
count: this.marketplace.repositories.filter(
|
||||
(repository) => repository.new
|
||||
).length,
|
||||
}
|
||||
),
|
||||
confirmText: this._i18n.localize(
|
||||
"ui.panel.marketplace.dialog.dismiss_new.confirm"
|
||||
),
|
||||
dismissText: this._i18n.localize("ui.common.cancel"),
|
||||
action: async () => {
|
||||
try {
|
||||
await dismissNewMarketplaceRepositories(
|
||||
this._api,
|
||||
this.marketplace.info.categories
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
showAlertDialog(this, {
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.marketplace.dialog.error.title"
|
||||
),
|
||||
text: marketplaceErrorMessage(err, this._i18n.localize),
|
||||
});
|
||||
// The alert reuses the confirmation's dialog, which only stays open
|
||||
// when its action fails
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
});
|
||||
private async _dismissNew() {
|
||||
try {
|
||||
await dismissNewMarketplaceRepositories(
|
||||
this._api,
|
||||
this.marketplace.info.categories
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
showAlertDialog(this, {
|
||||
title: this._i18n.localize("ui.panel.marketplace.dialog.error.title"),
|
||||
text: marketplaceErrorMessage(err, this._i18n.localize),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _statusStates = memoize(
|
||||
|
||||
@@ -10429,8 +10429,36 @@
|
||||
"error_section": {
|
||||
"title": "Error"
|
||||
},
|
||||
"section_strategy": {
|
||||
"common-controls": {
|
||||
"name": "Common controls",
|
||||
"description": "Shows the entities you use most, based on your activity."
|
||||
},
|
||||
"device": {
|
||||
"name": "Device",
|
||||
"description": "Shows the controls and sensors of a device."
|
||||
}
|
||||
},
|
||||
"create_section": {
|
||||
"header": "Create section",
|
||||
"manual": "Manual section",
|
||||
"manual_description": "Start empty and add your own cards.",
|
||||
"automatic": "[%key:ui::panel::lovelace::editor::section::automatic%]",
|
||||
"automatic_description": "Fills itself with cards and stays up to date.",
|
||||
"paste": "Paste section",
|
||||
"paste_description": "Add the section you copied."
|
||||
},
|
||||
"select_section_strategy": {
|
||||
"header": "Choose an automatic section",
|
||||
"built_in": "Built-in sections",
|
||||
"custom": "Community sections"
|
||||
},
|
||||
"edit_section": {
|
||||
"header": "Edit section",
|
||||
"header": "Section configuration",
|
||||
"typed_header": "{type} section configuration",
|
||||
"preview": "Preview",
|
||||
"preview_desktop": "Desktop preview",
|
||||
"preview_mobile": "Mobile preview",
|
||||
"tab_strategy": "Configuration",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
|
||||
"tab_appearance": "Appearance",
|
||||
@@ -10438,7 +10466,13 @@
|
||||
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||
"settings": {
|
||||
"column_span": "Width",
|
||||
"column_span_helper": "Larger sections will be made smaller to fit the display. (for example, on mobile devices)",
|
||||
"column_span_helper": "Smaller screens may show it narrower, and phones stack sections one under the other.",
|
||||
"column_span_options": {
|
||||
"single": "Single",
|
||||
"double": "Double",
|
||||
"triple": "Triple",
|
||||
"full": "Full width"
|
||||
},
|
||||
"background": "Background options",
|
||||
"background_enabled": "Background",
|
||||
"background_enabled_helper": "Display a colored background behind the section",
|
||||
@@ -12950,11 +12984,6 @@
|
||||
"error": {
|
||||
"title": "Error"
|
||||
},
|
||||
"dismiss_new": {
|
||||
"title": "Dismiss new repositories?",
|
||||
"text": "{count, plural,\n one {The new repository will no longer be marked as new.}\n other {The {count} new repositories will no longer be marked as new.}\n}",
|
||||
"confirm": "Dismiss"
|
||||
},
|
||||
"reload": {
|
||||
"description": "You need to reload your browser for the updated resources to be used.",
|
||||
"confirm": "Do you want to do that now?"
|
||||
|
||||