mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-23 22:24:24 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59666304c8 | ||
|
|
9287002e45 | ||
|
|
dcf64f43bd | ||
|
|
f934a72f41 | ||
|
|
71acef882c | ||
|
|
cef7461935 | ||
|
|
25231b3353 | ||
|
|
09ea28a8b0 |
No files matched your search
@@ -124,7 +124,25 @@ 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.
|
||||
@@ -209,6 +227,9 @@ 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;
|
||||
|
||||
@@ -720,25 +741,14 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar ${classMap(
|
||||
{ grouped: Boolean(this.groupColumn) && !this.autoHeight }
|
||||
)}"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
@visibilityChanged=${this._handleVisibilityChanged}
|
||||
.items=${items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
${
|
||||
// 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"
|
||||
aria-hidden="true"
|
||||
.group=${this._stickyGroup}
|
||||
@click=${this._collapseStickyGroup}
|
||||
>
|
||||
@@ -751,6 +761,18 @@ export class HaDataTable extends LitElement {
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar ${classMap(
|
||||
{ grouped: Boolean(this.groupColumn) && !this.autoHeight }
|
||||
)}"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
@visibilityChanged=${this._handleVisibilityChanged}
|
||||
.items=${items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
@@ -1025,12 +1047,10 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
if (groupColumn) {
|
||||
const isGroupSortColumn = sortColumn === 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 grouped = groupBy(items, (item) =>
|
||||
dataTableGroupKey(item[groupColumn])
|
||||
);
|
||||
const sortedEntries: [string, DataTableRowData[]][] = Object.keys(
|
||||
grouped
|
||||
)
|
||||
@@ -1107,12 +1127,14 @@ export class HaDataTable extends LitElement {
|
||||
localize: LocalizeFunc | undefined,
|
||||
sticky = false
|
||||
) {
|
||||
// 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.
|
||||
// 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.
|
||||
return html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role="cell"
|
||||
role=${ifDefined(sticky ? undefined : "cell")}
|
||||
.group=${groupName}
|
||||
@click=${sticky ? nothing : this._collapseGroup}
|
||||
>
|
||||
@@ -1127,14 +1149,27 @@ export class HaDataTable extends LitElement {
|
||||
?inert=${sticky}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${
|
||||
groupName === UNDEFINED_GROUP_KEY
|
||||
? localize?.("ui.components.data-table.ungrouped") || "Ungrouped"
|
||||
: groupName || ""
|
||||
}
|
||||
<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>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Actions don't toggle the group their header belongs to
|
||||
private _stopGroupToggle(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _memFilterData = memoizeOne(
|
||||
(
|
||||
data: DataTableRowData[],
|
||||
@@ -1436,6 +1471,11 @@ 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 })
|
||||
@@ -1503,18 +1543,15 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
public collapseAllGroups() {
|
||||
if (
|
||||
!this.groupColumn ||
|
||||
!this.data.some((item) => item[this.groupColumn!])
|
||||
) {
|
||||
const groupColumn = this.groupColumn;
|
||||
|
||||
if (!groupColumn || !this.data.some((item) => item[groupColumn])) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
const grouped = groupBy(this.data, (item) =>
|
||||
dataTableGroupKey(item[groupColumn])
|
||||
);
|
||||
this._collapsedGroups = Object.keys(grouped);
|
||||
this._lastSelectedRowId = null;
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
@@ -1873,6 +1910,16 @@ 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 {
|
||||
@@ -1883,6 +1930,34 @@ 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;
|
||||
|
||||
@@ -17,6 +17,7 @@ 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";
|
||||
@@ -27,7 +28,10 @@ import {
|
||||
dataTableQueryStateFromUrl,
|
||||
} from "../common/url/data-table-query-params";
|
||||
import "../components/chips/ha-assist-chip";
|
||||
import "../components/data-table/ha-data-table";
|
||||
import {
|
||||
dataTableGroupKey,
|
||||
groupActionsSlot,
|
||||
} from "../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
@@ -631,6 +635,13 @@ 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`
|
||||
@@ -846,6 +857,23 @@ 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 });
|
||||
|
||||
@@ -122,6 +122,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { renderCategoryGroupActions } from "../category/render-category-group-actions";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import {
|
||||
getAreaTableColumn,
|
||||
@@ -564,6 +565,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiHelpCircleOutline}
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
${renderCategoryGroupActions(this.hass, "automation", this._categories)}
|
||||
<ha-filter-floor-areas
|
||||
.type=${"automation"}
|
||||
.value=${this._filters["ha-filter-floor-areas"]?.value}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import { html } from "lit";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { groupActionsSlot } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-icon-button";
|
||||
import type { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import { updateCategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
|
||||
|
||||
type CategoryEditButton = HTMLElement & {
|
||||
category: CategoryRegistryEntry;
|
||||
scope: string;
|
||||
hass: HomeAssistant;
|
||||
};
|
||||
|
||||
const editCategory = (ev: HASSDomCurrentTargetEvent<CategoryEditButton>) => {
|
||||
const { category, scope, hass } = ev.currentTarget;
|
||||
showCategoryRegistryDetailDialog(ev.currentTarget, {
|
||||
scope,
|
||||
entry: category,
|
||||
updateEntry: (updates) =>
|
||||
updateCategoryRegistryEntry(hass, scope, category.category_id, updates),
|
||||
});
|
||||
};
|
||||
|
||||
// Edit buttons for the category group headers of a table grouped by category,
|
||||
// which names its groups after the categories
|
||||
export const renderCategoryGroupActions = (
|
||||
hass: HomeAssistant,
|
||||
scope: string,
|
||||
categories: CategoryRegistryEntry[] | undefined
|
||||
) =>
|
||||
categories?.map(
|
||||
(category) =>
|
||||
html`<ha-icon-button
|
||||
slot=${groupActionsSlot(category.name)}
|
||||
.label=${hass.localize("ui.panel.config.category.editor.edit")}
|
||||
.path=${mdiPencil}
|
||||
.category=${category}
|
||||
.scope=${scope}
|
||||
.hass=${hass}
|
||||
@click=${editCategory}
|
||||
></ha-icon-button>`
|
||||
);
|
||||
@@ -121,6 +121,7 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { renderCategoryGroupActions } from "../category/render-category-group-actions";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import {
|
||||
getAreaTableColumn,
|
||||
@@ -869,6 +870,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
slot="toolbar-icon"
|
||||
></ha-integration-overflow-menu>
|
||||
${renderCategoryGroupActions(this.hass, "helpers", this._categories)}
|
||||
<ha-button slot="fab" size="l" @click=${this._createHelper}>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.helpers.picker.create_helper")}
|
||||
|
||||
@@ -109,6 +109,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { renderCategoryGroupActions } from "../category/render-category-group-actions";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import {
|
||||
getAreaTableColumn,
|
||||
@@ -562,6 +563,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircleOutline}
|
||||
></ha-icon-button>
|
||||
${renderCategoryGroupActions(this.hass, "scene", this._categories)}
|
||||
|
||||
<ha-filter-floor-areas
|
||||
.type=${"scene"}
|
||||
|
||||
@@ -113,6 +113,7 @@ import { showToast } from "../../../util/toast";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { renderCategoryGroupActions } from "../category/render-category-group-actions";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import {
|
||||
getAreaTableColumn,
|
||||
@@ -496,6 +497,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiHelpCircleOutline}
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
${renderCategoryGroupActions(this.hass, "script", this._categories)}
|
||||
<ha-filter-floor-areas
|
||||
.type=${"script"}
|
||||
.value=${this._filters["ha-filter-floor-areas"]?.value}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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,
|
||||
@@ -34,6 +35,7 @@ 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,
|
||||
@@ -405,6 +407,14 @@ 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._dismissNew}
|
||||
></ha-icon-button>
|
||||
${
|
||||
this.tab === "installed"
|
||||
? html`<ha-assist-chip
|
||||
@@ -942,18 +952,43 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
showMarketplaceAddFromLink(this, this._i18n.localize, this.marketplace);
|
||||
}
|
||||
|
||||
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 _dismissNew() {
|
||||
showConfirmationDialog(this, {
|
||||
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 _statusStates = memoize(
|
||||
|
||||
@@ -12950,6 +12950,11 @@
|
||||
"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?"
|
||||
|
||||
Reference in new issue
Block a user