Merge remote-tracking branch 'origin/main' into feature/dropzone-management
This commit is contained in:
@@ -3,13 +3,23 @@ import { format, resolveConfig } from 'prettier';
|
||||
import { createImportMap } from '../importmap/index.js';
|
||||
|
||||
const tsconfigPath = 'tsconfig.json';
|
||||
const tsconfigComment = `// Don't edit this file directly. It is generated by /devops/tsconfig/index.js\n\n`;
|
||||
const tsconfigComment = `
|
||||
/* -------------------------------------------------------------------------
|
||||
|
||||
|
||||
DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
|
||||
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------- */
|
||||
`;
|
||||
|
||||
const tsConfigBase = {
|
||||
compilerOptions: {
|
||||
module: 'esnext',
|
||||
target: 'ES2020',
|
||||
lib: ['es2020', 'dom', 'dom.iterable'],
|
||||
target: 'es2022',
|
||||
lib: ['es2022', 'dom', 'dom.iterable'],
|
||||
outDir: './types',
|
||||
allowSyntheticDefaultImports: true,
|
||||
experimentalDecorators: true,
|
||||
|
||||
@@ -588,7 +588,7 @@ export default {
|
||||
examineManagement: {
|
||||
configuredSearchers: 'Konfigurerede søgere',
|
||||
configuredSearchersDescription:
|
||||
'Viser egenskaber og værktøjer til enhver konfigureret søger (dvs. som en\n multi-indekssøger)\n ',
|
||||
'Viser egenskaber og værktøjer til enhver konfigureret søger (dvs. som en multi-indekssøger)',
|
||||
fieldValues: 'Feltværdier',
|
||||
healthStatus: 'Sundhedstilstand',
|
||||
healthStatusDescription: 'Indeksets sundhedstilstand, og hvis det kan læses',
|
||||
@@ -597,10 +597,10 @@ export default {
|
||||
indexInfoDescription: 'Viser indeksets egenskaber',
|
||||
manageIndexes: 'Administrer Examine indekserne',
|
||||
manageIndexesDescription:
|
||||
'Giver dig mulighed for at se detaljerne for hvert indeks og giver nogle\n værktøjer til styring af indeksørerne\n ',
|
||||
'Giver dig mulighed for at se detaljerne for hvert indeks og giver nogle værktøjer til styring af indeksørerne',
|
||||
rebuildIndex: 'Genopbyg indeks',
|
||||
rebuildIndexWarning:
|
||||
'\n Dette vil medføre, at indekset genopbygges.<br />\n Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.<br />\n Det anbefales ikke at genopbygge et indeks i perioder med høj websitetrafik eller når redaktører redigerer indhold.\n ',
|
||||
'Dette vil medføre, at indekset genopbygges.<br /> Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.<br /> Det anbefales ikke at genopbygge et indeks i perioder med høj websitetrafik eller når redaktører redigerer indhold.',
|
||||
searchers: 'Søgere',
|
||||
searchDescription: 'Søg i indekset og se resultaterne',
|
||||
tools: 'Værktøjer',
|
||||
@@ -608,7 +608,7 @@ export default {
|
||||
fields: 'felter',
|
||||
indexCannotRead: 'Indexet skal bygges igen, for at kunne læses',
|
||||
processIsTakingLonger:
|
||||
'Processen tager længere tid end forventet. Kontrollér Umbraco loggen for at se om\n der er sket fejl under operationen\n ',
|
||||
'Processen tager længere tid end forventet. Kontrollér Umbraco loggen for at se om der er sket fejl under operationen',
|
||||
indexCannotRebuild: 'Dette index kan ikke genbygges for det ikke har nogen',
|
||||
iIndexPopulator: 'IIndexPopulator',
|
||||
contentInIndex: 'Content in index',
|
||||
|
||||
@@ -597,7 +597,7 @@ export default {
|
||||
examineManagement: {
|
||||
configuredSearchers: 'Configured Searchers',
|
||||
configuredSearchersDescription:
|
||||
'Shows properties and tools for any configured Searcher (i.e. such as a\n multi-index searcher)\n ',
|
||||
'Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)',
|
||||
fieldValues: 'Field values',
|
||||
healthStatus: 'Health status',
|
||||
healthStatusDescription: 'The health status of the index and if it can be read',
|
||||
@@ -607,10 +607,10 @@ export default {
|
||||
indexInfoDescription: 'Lists the properties of the index',
|
||||
manageIndexes: "Manage Examine's indexes",
|
||||
manageIndexesDescription:
|
||||
'Allows you to view the details of each index and provides some tools for\n managing the indexes\n ',
|
||||
'Allows you to view the details of each index and provides some tools for managing the indexes',
|
||||
rebuildIndex: 'Rebuild index',
|
||||
rebuildIndexWarning:
|
||||
'\n This will cause the index to be rebuilt.<br />\n Depending on how much content there is in your site this could take a while.<br />\n It is not recommended to rebuild an index during times of high website traffic or when editors are editing content.\n ',
|
||||
'This will cause the index to be rebuilt.<br /> Depending on how much content there is in your site this could take a while.<br /> It is not recommended to rebuild an index during times of high website traffic or when editors are editing content.',
|
||||
searchers: 'Searchers',
|
||||
searchDescription: 'Search the index and view the results',
|
||||
tools: 'Tools',
|
||||
@@ -618,7 +618,7 @@ export default {
|
||||
fields: 'fields',
|
||||
indexCannotRead: 'The index cannot be read and will need to be rebuilt',
|
||||
processIsTakingLonger:
|
||||
'The process is taking longer than expected, check the Umbraco log to see if there\n have been any errors during this operation\n ',
|
||||
'The process is taking longer than expected, check the Umbraco log to see if there have been any errors during this operation',
|
||||
indexCannotRebuild: 'This index cannot be rebuilt because it has no assigned',
|
||||
iIndexPopulator: 'IIndexPopulator',
|
||||
},
|
||||
|
||||
@@ -979,6 +979,11 @@ export enum HealthStatusModel {
|
||||
REBUILDING = 'Rebuilding'
|
||||
}
|
||||
|
||||
export type HealthStatusResponseModel = {
|
||||
status: HealthStatusModel
|
||||
message?: string | null
|
||||
};
|
||||
|
||||
export type HelpPageResponseModel = {
|
||||
name?: string | null
|
||||
description?: string | null
|
||||
@@ -993,7 +998,7 @@ parent?: ReferenceByIdModel | null
|
||||
|
||||
export type IndexResponseModel = {
|
||||
name: string
|
||||
healthStatus: HealthStatusModel
|
||||
healthStatus: HealthStatusResponseModel
|
||||
canRebuild: boolean
|
||||
searcherName: string
|
||||
documentCount: number
|
||||
@@ -5236,15 +5241,15 @@ PostWebhook: {
|
||||
GetWebhookById: {
|
||||
id: string
|
||||
|
||||
};
|
||||
DeleteWebhookById: {
|
||||
id: string
|
||||
|
||||
};
|
||||
PutWebhookById: {
|
||||
id: string
|
||||
requestBody?: UpdateWebhookRequestModel
|
||||
|
||||
};
|
||||
DeleteWebhookById: {
|
||||
id: string
|
||||
|
||||
};
|
||||
GetWebhookEvents: {
|
||||
skip?: number
|
||||
@@ -5259,8 +5264,8 @@ take?: number
|
||||
,GetWebhook: PagedWebhookResponseModel
|
||||
,PostWebhook: string
|
||||
,GetWebhookById: WebhookResponseModel
|
||||
,PutWebhookById: string
|
||||
,DeleteWebhookById: string
|
||||
,PutWebhookById: string
|
||||
,GetWebhookEvents: PagedWebhookEventModel
|
||||
|
||||
}
|
||||
|
||||
@@ -9022,20 +9022,17 @@ take
|
||||
* @returns string Success
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static putWebhookById(data: WebhookData['payloads']['PutWebhookById']): CancelablePromise<WebhookData['responses']['PutWebhookById']> {
|
||||
public static deleteWebhookById(data: WebhookData['payloads']['DeleteWebhookById']): CancelablePromise<WebhookData['responses']['DeleteWebhookById']> {
|
||||
const {
|
||||
|
||||
id,
|
||||
requestBody
|
||||
id
|
||||
} = data;
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
method: 'DELETE',
|
||||
url: '/umbraco/management/api/v1/webhook/{id}',
|
||||
path: {
|
||||
id
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
responseHeader: 'Umb-Notifications',
|
||||
errors: {
|
||||
400: `Bad Request`,
|
||||
@@ -9050,17 +9047,20 @@ requestBody
|
||||
* @returns string Success
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteWebhookById(data: WebhookData['payloads']['DeleteWebhookById']): CancelablePromise<WebhookData['responses']['DeleteWebhookById']> {
|
||||
public static putWebhookById(data: WebhookData['payloads']['PutWebhookById']): CancelablePromise<WebhookData['responses']['PutWebhookById']> {
|
||||
const {
|
||||
|
||||
id
|
||||
id,
|
||||
requestBody
|
||||
} = data;
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
method: 'PUT',
|
||||
url: '/umbraco/management/api/v1/webhook/{id}',
|
||||
path: {
|
||||
id
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
responseHeader: 'Umb-Notifications',
|
||||
errors: {
|
||||
400: `Bad Request`,
|
||||
|
||||
@@ -20,7 +20,7 @@ export const Indexers: IndexResponseModel[] = [
|
||||
{
|
||||
name: 'ExternalIndex',
|
||||
canRebuild: true,
|
||||
healthStatus: HealthStatusModel.HEALTHY,
|
||||
healthStatus: { status: HealthStatusModel.HEALTHY },
|
||||
documentCount: 0,
|
||||
fieldCount: 0,
|
||||
searcherName: '',
|
||||
@@ -40,7 +40,7 @@ export const Indexers: IndexResponseModel[] = [
|
||||
{
|
||||
name: 'InternalIndex',
|
||||
canRebuild: true,
|
||||
healthStatus: HealthStatusModel.HEALTHY,
|
||||
healthStatus: { status: HealthStatusModel.HEALTHY },
|
||||
documentCount: 0,
|
||||
fieldCount: 0,
|
||||
searcherName: '',
|
||||
@@ -60,7 +60,7 @@ export const Indexers: IndexResponseModel[] = [
|
||||
{
|
||||
name: 'MemberIndex',
|
||||
canRebuild: true,
|
||||
healthStatus: HealthStatusModel.HEALTHY,
|
||||
healthStatus: { status: HealthStatusModel.HEALTHY },
|
||||
fieldCount: 0,
|
||||
documentCount: 0,
|
||||
searcherName: '',
|
||||
|
||||
@@ -371,8 +371,10 @@ export class UmbAuthFlow {
|
||||
async #performTokenRequest(request: TokenRequest): Promise<void> {
|
||||
try {
|
||||
this.#tokenResponse = await this.#tokenHandler.performTokenRequest(this.#configuration, request);
|
||||
this.#saveTokenState();
|
||||
} catch (error) {
|
||||
// If the token request fails, it means the refresh token is invalid
|
||||
// If the token request fails, it means the code or refresh token is invalid
|
||||
this.clearTokenStorage();
|
||||
console.error('Token request error', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,5 @@ export * from './multiple-text-string-input/index.js';
|
||||
export * from './popover-layout/index.js';
|
||||
export * from './ref-item/index.js';
|
||||
export * from './stack/index.js';
|
||||
export * from './split-panel/index.js';
|
||||
export * from './table/index.js';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import './split-panel.element.js';
|
||||
|
||||
export * from './split-panel.element.js';
|
||||
@@ -0,0 +1,296 @@
|
||||
import {
|
||||
type PropertyValueMap,
|
||||
LitElement,
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
/**
|
||||
* Custom element for a split panel with adjustable divider.
|
||||
* @element umb-split-panel
|
||||
* @slot start - Content for the start panel.
|
||||
* @slot end - Content for the end panel.
|
||||
* @cssprop --umb-split-panel-initial-position - Initial position of the divider.
|
||||
* @cssprop --umb-split-panel-start-min-width - Minimum width of the start panel.
|
||||
* @cssprop --umb-split-panel-end-min-width - Minimum width of the end panel.
|
||||
* @cssprop --umb-split-panel-divider-touch-area-width - Width of the divider touch area.
|
||||
* @cssprop --umb-split-panel-divider-width - Width of the divider.
|
||||
* @cssprop --umb-split-panel-divider-color - Color of the divider.
|
||||
*/
|
||||
@customElement('umb-split-panel')
|
||||
export class UmbSplitPanelElement extends LitElement {
|
||||
@query('#main') mainElement!: HTMLElement;
|
||||
@query('#divider-touch-area') dividerTouchAreaElement!: HTMLElement;
|
||||
@query('#divider') dividerElement!: HTMLElement;
|
||||
|
||||
/**
|
||||
* Snap points for the divider position.
|
||||
* Pixel or percent space-separated values: e.g., "100px 50% -75% -200px".
|
||||
* Negative values are relative to the end of the container.
|
||||
*/
|
||||
@property({ type: String }) snap?: string; //TODO: Consider using css variables for snap points.
|
||||
|
||||
/**
|
||||
* Locking mode for the split panel.
|
||||
* Possible values: "start", "end", "none" (default).
|
||||
*/
|
||||
@property({ type: String }) lock: 'start' | 'end' | 'none' = 'none';
|
||||
|
||||
/**
|
||||
* Initial position of the divider.
|
||||
* Pixel or percent value: e.g., "100px" or "25%".
|
||||
* Defaults to a CSS variable if not set: "var(--umb-split-panel-initial-position) which defaults to 50%".
|
||||
*/
|
||||
@property({ type: String, reflect: true }) position = 'var(--umb-split-panel-initial-position)';
|
||||
//TODO: Add support for negative values (relative to end of container) similar to snap points.
|
||||
|
||||
/** Width of the locked panel when in "start" or "end" lock mode */
|
||||
#lockedPanelWidth: number = 0;
|
||||
/** Pixel value for the snap threshold. Determines how close the divider needs to be to a snap point to snap to it. */
|
||||
readonly #SNAP_THRESHOLD = 25 as const;
|
||||
|
||||
@state() _hasStartPanel = false;
|
||||
@state() _hasEndPanel = false;
|
||||
get #hasBothPanels() {
|
||||
return this._hasStartPanel && this._hasEndPanel;
|
||||
}
|
||||
|
||||
#hasInitialized = false;
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect();
|
||||
}
|
||||
|
||||
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.updated(_changedProperties);
|
||||
|
||||
if (!this.#hasInitialized) return;
|
||||
|
||||
if (_changedProperties.has('position')) {
|
||||
if (this.lock !== 'none') {
|
||||
const { width } = this.mainElement.getBoundingClientRect();
|
||||
|
||||
let pos = parseFloat(this.position);
|
||||
|
||||
if (this.position.endsWith('%')) {
|
||||
pos = (pos / 100) * width;
|
||||
}
|
||||
|
||||
const lockedPanelWidth = this.lock === 'start' ? pos : width - pos;
|
||||
this.#lockedPanelWidth = lockedPanelWidth;
|
||||
}
|
||||
|
||||
this.#updateSplit();
|
||||
}
|
||||
}
|
||||
|
||||
#clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
#setPosition(pos: number) {
|
||||
const { width } = this.mainElement.getBoundingClientRect();
|
||||
const localPos = this.#clamp(pos, 0, width);
|
||||
const percentagePos = (localPos / width) * 100;
|
||||
this.position = percentagePos + '%';
|
||||
}
|
||||
|
||||
#updateSplit() {
|
||||
// If lock is none
|
||||
let maxStartWidth = this.position;
|
||||
let maxEndWidth = '1fr';
|
||||
|
||||
if (this.lock === 'start') {
|
||||
maxStartWidth = this.#lockedPanelWidth + 'px';
|
||||
maxEndWidth = `1fr`;
|
||||
}
|
||||
if (this.lock === 'end') {
|
||||
maxStartWidth = `1fr`;
|
||||
maxEndWidth = this.#lockedPanelWidth + 'px';
|
||||
}
|
||||
|
||||
this.mainElement.style.gridTemplateColumns = `
|
||||
minmax(var(--umb-split-panel-start-min-width), ${maxStartWidth})
|
||||
0px
|
||||
minmax(var(--umb-split-panel-end-min-width), ${maxEndWidth})
|
||||
`;
|
||||
}
|
||||
|
||||
#onDragStart = (event: PointerEvent | TouchEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const move = (event: PointerEvent) => {
|
||||
const { clientX } = event;
|
||||
const { left, width } = this.mainElement.getBoundingClientRect();
|
||||
const localPos = this.#clamp(clientX - left, 0, width);
|
||||
const mappedPos = mapXAxisToSnap(localPos, width);
|
||||
|
||||
this.#lockedPanelWidth = this.lock === 'start' ? mappedPos : width - mappedPos;
|
||||
this.#setPosition(mappedPos);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
document.removeEventListener('pointermove', move);
|
||||
document.removeEventListener('pointerup', stop);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('position-changed', { detail: { position: this.position } }));
|
||||
};
|
||||
|
||||
const mapXAxisToSnap = (xPos: number, containerWidth: number) => {
|
||||
const snaps = this.snap?.split(' ');
|
||||
if (!snaps) return xPos;
|
||||
|
||||
const snapsInPixels = snaps.map((snap) => {
|
||||
let snapPx = parseFloat(snap);
|
||||
|
||||
if (snap.endsWith('%')) {
|
||||
snapPx = (snapPx / 100) * containerWidth;
|
||||
}
|
||||
|
||||
if (snap.startsWith('-')) {
|
||||
snapPx = containerWidth + snapPx;
|
||||
}
|
||||
|
||||
return snapPx;
|
||||
});
|
||||
|
||||
const closestSnap = snapsInPixels.reduce((prev, curr) => {
|
||||
return Math.abs(curr - xPos) < Math.abs(prev - xPos) ? curr : prev;
|
||||
});
|
||||
|
||||
if (closestSnap < xPos + this.#SNAP_THRESHOLD && closestSnap > xPos - this.#SNAP_THRESHOLD) {
|
||||
xPos = closestSnap;
|
||||
}
|
||||
|
||||
return xPos;
|
||||
};
|
||||
|
||||
document.addEventListener('pointermove', move, { passive: true });
|
||||
document.addEventListener('pointerup', stop);
|
||||
};
|
||||
|
||||
#disconnect() {
|
||||
this.dividerTouchAreaElement.removeEventListener('pointerdown', this.#onDragStart);
|
||||
this.dividerTouchAreaElement.removeEventListener('touchstart', this.#onDragStart);
|
||||
this.dividerElement.style.display = 'none';
|
||||
this.mainElement.style.display = 'flex';
|
||||
this.#hasInitialized = false;
|
||||
}
|
||||
|
||||
async #connect() {
|
||||
this.#hasInitialized = true;
|
||||
|
||||
this.mainElement.style.display = 'grid';
|
||||
this.mainElement.style.gridTemplateColumns = `${this.position} 0px 1fr`;
|
||||
this.dividerElement.style.display = 'unset';
|
||||
|
||||
this.dividerTouchAreaElement.addEventListener('pointerdown', this.#onDragStart);
|
||||
this.dividerTouchAreaElement.addEventListener('touchstart', this.#onDragStart, { passive: false });
|
||||
|
||||
// Wait for the next frame to get the correct position of the divider.
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
|
||||
const { left: dividerLeft } = this.shadowRoot!.querySelector('#divider')!.getBoundingClientRect();
|
||||
const { left: mainLeft, width: mainWidth } = this.mainElement.getBoundingClientRect();
|
||||
const percentagePos = ((dividerLeft - mainLeft) / mainWidth) * 100;
|
||||
this.position = `${percentagePos}%`;
|
||||
}
|
||||
|
||||
#onSlotChanged(event: Event) {
|
||||
const slot = event.target as HTMLSlotElement;
|
||||
const name = slot.name;
|
||||
|
||||
if (name === 'start') {
|
||||
this._hasStartPanel = slot.assignedElements().length > 0;
|
||||
}
|
||||
if (name === 'end') {
|
||||
this._hasEndPanel = slot.assignedElements().length > 0;
|
||||
}
|
||||
|
||||
if (!this.#hasBothPanels) {
|
||||
if (this.#hasInitialized) {
|
||||
this.#disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.#connect();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="main">
|
||||
<slot
|
||||
name="start"
|
||||
@slotchange=${this.#onSlotChanged}
|
||||
style="width: ${this._hasStartPanel ? '100%' : '0'}"></slot>
|
||||
<div id="divider">
|
||||
<div id="divider-touch-area" tabindex="0"></div>
|
||||
</div>
|
||||
<slot name="end" @slotchange=${this.#onSlotChanged} style="width: ${this._hasEndPanel ? '100%' : '0'}"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
static styles = css`
|
||||
:host {
|
||||
display: contents;
|
||||
--umb-split-panel-initial-position: 50%;
|
||||
--umb-split-panel-start-min-width: 0;
|
||||
--umb-split-panel-end-min-width: 0;
|
||||
--umb-split-panel-divider-touch-area-width: 20px;
|
||||
--umb-split-panel-divider-width: 1px;
|
||||
--umb-split-panel-divider-color: transparent;
|
||||
--umb-split-panel-slot-overflow: hidden;
|
||||
}
|
||||
slot {
|
||||
overflow: var(--umb-split-panel-slot-overflow);
|
||||
display: block;
|
||||
}
|
||||
#main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#divider {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
display: none;
|
||||
}
|
||||
#divider-touch-area {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: var(--umb-split-panel-divider-touch-area-width);
|
||||
transform: translateX(-50%);
|
||||
cursor: col-resize;
|
||||
}
|
||||
/* Do we want a line that shows the divider? */
|
||||
#divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: var(--umb-split-panel-divider-width);
|
||||
height: 100%;
|
||||
transform: round(translateX(-50%));
|
||||
background-color: var(--umb-split-panel-divider-color);
|
||||
z-index: -1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-split-panel': UmbSplitPanelElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Meta, StoryObj } from '@storybook/web-components';
|
||||
import './split-panel.element.js';
|
||||
import type { UmbSplitPanelElement } from './split-panel.element.js';
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
const meta: Meta<UmbSplitPanelElement> = {
|
||||
title: 'Components/Split Panel',
|
||||
component: 'umb-split-panel',
|
||||
argTypes: {
|
||||
lock: { options: ['none', 'start', 'end'] },
|
||||
snap: { control: 'text' },
|
||||
position: { control: 'text' },
|
||||
},
|
||||
args: {
|
||||
lock: 'start',
|
||||
snap: '',
|
||||
position: '50%',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<UmbSplitPanelElement>;
|
||||
|
||||
export const Overview: Story = {
|
||||
render: (props) => html`
|
||||
<umb-split-panel .lock=${props.lock} .snap=${props.snap} .position=${props.position}>
|
||||
<div id="start" slot="start">Start</div>
|
||||
<div id="end" slot="end">End</div>
|
||||
</umb-split-panel>
|
||||
<style>
|
||||
#start,
|
||||
#end {
|
||||
background-color: #ffffff;
|
||||
color: #383838;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
#start {
|
||||
border-right: 2px solid #e5e5e5;
|
||||
min-height: 300px;
|
||||
}
|
||||
#end {
|
||||
border-left: 2px solid #e5e5e5;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
};
|
||||
@@ -71,7 +71,7 @@ export class UmbInputCultureSelectElement extends UUIFormControlMixin(UmbLitElem
|
||||
}
|
||||
|
||||
get #fromAvailableCultures() {
|
||||
return this._cultures.find((culture) => culture.name.toLowerCase() === (this.value as string).toLowerCase());
|
||||
return this._cultures.find((culture) => culture.name.toLowerCase() === (this.value as string)?.toLowerCase());
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from './confirm-modal.token.js';
|
||||
export * from './debug-modal.token.js';
|
||||
export * from './embedded-media-modal.token.js';
|
||||
export * from './entity-user-permission-settings-modal.token.js';
|
||||
export * from './examine-fields-settings-modal.token.js';
|
||||
export * from './icon-picker-modal.token.js';
|
||||
export * from './item-picker-modal.token.js';
|
||||
export * from './link-picker-modal.token.js';
|
||||
|
||||
@@ -44,6 +44,9 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
|
||||
UmbExtensionElementInitializer<ManifestSectionSidebarApp | ManifestSectionSidebarAppMenuKind>
|
||||
>;
|
||||
|
||||
@state()
|
||||
_splitPanelPosition = '300px';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -54,6 +57,11 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
|
||||
});
|
||||
|
||||
this.#createRoutes();
|
||||
|
||||
const splitPanelPosition = localStorage.getItem('umb-split-panel-position');
|
||||
if (splitPanelPosition) {
|
||||
this._splitPanelPosition = splitPanelPosition;
|
||||
}
|
||||
}
|
||||
|
||||
#createRoutes() {
|
||||
@@ -75,26 +83,37 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
|
||||
];
|
||||
}
|
||||
|
||||
#onSplitPanelChange(event: CustomEvent) {
|
||||
const position = event.detail.position;
|
||||
localStorage.setItem('umb-split-panel-position', position.toString());
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this._sidebarApps && this._sidebarApps.length > 0
|
||||
? html`
|
||||
<!-- TODO: these extensions should be combined into one type: sectionSidebarApp with a "subtype" -->
|
||||
<umb-section-sidebar>
|
||||
${repeat(
|
||||
this._sidebarApps,
|
||||
(app) => app.alias,
|
||||
(app) => app.component,
|
||||
)}
|
||||
</umb-section-sidebar>
|
||||
`
|
||||
: nothing}
|
||||
<umb-section-main>
|
||||
${this._routes && this._routes.length > 0
|
||||
? html`<umb-router-slot id="router-slot" .routes=${this._routes}></umb-router-slot>`
|
||||
<umb-split-panel
|
||||
lock="start"
|
||||
snap="300px"
|
||||
@position-changed=${this.#onSplitPanelChange}
|
||||
.position=${this._splitPanelPosition}>
|
||||
${this._sidebarApps && this._sidebarApps.length > 0
|
||||
? html`
|
||||
<!-- TODO: these extensions should be combined into one type: sectionSidebarApp with a "subtype" -->
|
||||
<umb-section-sidebar slot="start">
|
||||
${repeat(
|
||||
this._sidebarApps,
|
||||
(app) => app.alias,
|
||||
(app) => app.component,
|
||||
)}
|
||||
</umb-section-sidebar>
|
||||
`
|
||||
: nothing}
|
||||
<slot></slot>
|
||||
</umb-section-main>
|
||||
<umb-section-main slot="end">
|
||||
${this._routes && this._routes.length > 0
|
||||
? html`<umb-router-slot id="router-slot" .routes=${this._routes}></umb-router-slot>`
|
||||
: nothing}
|
||||
<slot></slot>
|
||||
</umb-section-main>
|
||||
</umb-split-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -106,6 +125,18 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
umb-split-panel {
|
||||
--umb-split-panel-start-min-width: 200px;
|
||||
--umb-split-panel-start-max-width: 400px;
|
||||
--umb-split-panel-end-min-width: 600px;
|
||||
--umb-split-panel-slot-overflow: visible;
|
||||
}
|
||||
@media only screen and (min-width: 800px) {
|
||||
umb-split-panel {
|
||||
--umb-split-panel-initial-position: 300px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ export class UmbSectionSidebarContextMenuElement extends UmbLitElement {
|
||||
}
|
||||
#action-modal {
|
||||
position: absolute;
|
||||
left: var(--umb-section-sidebar-width);
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: calc(var(--umb-section-sidebar-width) * -1);
|
||||
width: var(--umb-section-sidebar-width);
|
||||
border: none;
|
||||
border-left: 1px solid var(--uui-color-border);
|
||||
|
||||
@@ -29,6 +29,7 @@ export class UmbSectionSidebarElement extends UmbLitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#scroll-container {
|
||||
|
||||
@@ -14,11 +14,7 @@ const entityActions: Array<ManifestTypes> = [
|
||||
name: 'Create Document Type Entity Action',
|
||||
weight: 1200,
|
||||
api: UmbCreateDocumentTypeEntityAction,
|
||||
forEntityTypes: [
|
||||
UMB_DOCUMENT_TYPE_ENTITY_TYPE,
|
||||
UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE,
|
||||
UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE,
|
||||
],
|
||||
forEntityTypes: [UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE],
|
||||
meta: {
|
||||
icon: 'icon-add',
|
||||
label: '#actions_create',
|
||||
|
||||
@@ -115,7 +115,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
|
||||
|
||||
#toggle {
|
||||
color: var(--uui-color-text);
|
||||
width: var(--umb-section-sidebar-width);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -112,10 +112,11 @@ export class UmbLanguageDetailsWorkspaceViewElement extends UmbLitElement implem
|
||||
<umb-property-layout label="Language">
|
||||
<div slot="editor">
|
||||
<!-- TODO: disable already created cultures in the select -->
|
||||
<umb-input-culture-select
|
||||
value=${ifDefined(this._language?.unique)}
|
||||
@change=${this.#handleCultureChange}
|
||||
?readonly=${this._isNew === false}></umb-input-culture-select>
|
||||
${this._isNew
|
||||
? html` <umb-input-culture-select
|
||||
value=${ifDefined(this._language?.unique)}
|
||||
@change=${this.#handleCultureChange}></umb-input-culture-select>`
|
||||
: this._language?.name}
|
||||
</div>
|
||||
</umb-property-layout>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './modal/index.js';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { manifests as modalManifests } from './modal/manifests.js';
|
||||
|
||||
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes> = [...modalManifests];
|
||||
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
UmbExamineFieldsSettingsModalData,
|
||||
UmbExamineFieldsSettingsModalValue,
|
||||
UmbExamineFieldSettingsType,
|
||||
} from './examine-fields-settings-modal.token.js';
|
||||
import { html, css, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
@customElement('umb-examine-fields-settings-modal')
|
||||
export class UmbExamineFieldsSettingsModalElement extends UmbModalBaseElement<
|
||||
UmbExamineFieldsSettingsModalData,
|
||||
UmbExamineFieldsSettingsModalValue
|
||||
> {
|
||||
render() {
|
||||
return html`<umb-body-layout headline=${this.localize.term('examineManagement_fields')}>
|
||||
<uui-scroll-container id="field-settings"> ${this.#renderFields()} </uui-scroll-container>
|
||||
<div slot="actions">
|
||||
<uui-button
|
||||
look="primary"
|
||||
label=${this.localize.term('general_close')}
|
||||
@click="${this._submitModal}"></uui-button>
|
||||
</div>
|
||||
</umb-body-layout>`;
|
||||
}
|
||||
|
||||
#setExposed(fieldSetting: UmbExamineFieldSettingsType) {
|
||||
const newField: UmbExamineFieldSettingsType = { ...fieldSetting, exposed: !fieldSetting.exposed };
|
||||
|
||||
const updatedFields =
|
||||
this.modalContext?.getValue().fields.map((field) => {
|
||||
if (field.name === fieldSetting.name) return newField;
|
||||
else return field;
|
||||
}) ?? [];
|
||||
|
||||
this.modalContext?.updateValue({ fields: updatedFields });
|
||||
}
|
||||
|
||||
#renderFields() {
|
||||
if (!this.value.fields.length) return;
|
||||
return html`<span>
|
||||
${Object.values(this.value.fields).map((field) => {
|
||||
return html`<uui-toggle
|
||||
name="${field.name}"
|
||||
label="${field.name}"
|
||||
.checked="${field.exposed}"
|
||||
@change="${() => this.#setExposed(field)}"></uui-toggle>
|
||||
<br />`;
|
||||
})}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: relative;
|
||||
}
|
||||
|
||||
uui-scroll-container {
|
||||
overflow-y: scroll;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbExamineFieldsSettingsModalElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-examine-fields-settings-modal': UmbExamineFieldsSettingsModalElement;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import { UmbModalToken } from './modal-token.js';
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export type UmbExamineFieldsSettingsModalData = never;
|
||||
|
||||
type FieldSettingsType = {
|
||||
export type UmbExamineFieldSettingsType = {
|
||||
name: string;
|
||||
exposed: boolean;
|
||||
};
|
||||
|
||||
export type UmbExamineFieldsSettingsModalValue = {
|
||||
fields: Array<FieldSettingsType>;
|
||||
fields: Array<UmbExamineFieldSettingsType>;
|
||||
};
|
||||
|
||||
export const UMB_EXAMINE_FIELDS_SETTINGS_MODAL = new UmbModalToken<
|
||||
UmbExamineFieldsSettingsModalData,
|
||||
UmbExamineFieldsSettingsModalValue
|
||||
>('Umb.Modal.ExamineFieldsSettings', {
|
||||
>('Umb.Modal.Examine.FieldsSettings', {
|
||||
modal: {
|
||||
type: 'sidebar',
|
||||
size: 'small',
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './examine-fields-settings-modal.element.js';
|
||||
export * from './examine-fields-settings-modal.token.js';
|
||||
@@ -1,11 +1,15 @@
|
||||
import type {
|
||||
UmbExamineFieldsViewerModalData,
|
||||
UmbExamineFieldsViewerModalValue,
|
||||
} from './examine-fields-viewer-modal.token.js';
|
||||
import { html, css, nothing, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
import type { SearchResultResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
@customElement('umb-modal-element-fields-viewer')
|
||||
export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
|
||||
SearchResultResponseModel & { name: string }
|
||||
@customElement('umb-examine-fields-viewer-modal')
|
||||
export class UmbExamineFieldsViewerModalElement extends UmbModalBaseElement<
|
||||
UmbExamineFieldsViewerModalData,
|
||||
UmbExamineFieldsViewerModalValue
|
||||
> {
|
||||
private _handleClose() {
|
||||
this.modalContext?.reject();
|
||||
@@ -15,7 +19,7 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
|
||||
if (!this.data) return nothing;
|
||||
|
||||
return html`
|
||||
<uui-dialog-layout class="uui-text" headline="${this.data.name}">
|
||||
<umb-body-layout headline="${this.data?.name}">
|
||||
<uui-scroll-container id="field-viewer">
|
||||
<span>
|
||||
<uui-table>
|
||||
@@ -23,7 +27,7 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
|
||||
<uui-table-head-cell> Field </uui-table-head-cell>
|
||||
<uui-table-head-cell> Value </uui-table-head-cell>
|
||||
</uui-table-head>
|
||||
${Object.values(this.data.fields ?? []).map((cell) => {
|
||||
${Object.values(this.data.searchResult.fields ?? []).map((cell) => {
|
||||
return html`<uui-table-row>
|
||||
<uui-table-cell> ${cell.name} </uui-table-cell>
|
||||
<uui-table-cell> ${cell.values?.join(', ')} </uui-table-cell>
|
||||
@@ -32,10 +36,13 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
|
||||
</uui-table>
|
||||
</span>
|
||||
</uui-scroll-container>
|
||||
<div>
|
||||
<uui-button look="primary" @click="${this._handleClose}">Close</uui-button>
|
||||
<div slot="actions">
|
||||
<uui-button
|
||||
look="primary"
|
||||
label=${this.localize.term('general_close')}
|
||||
@click=${this._rejectModal}></uui-button>
|
||||
</div>
|
||||
</uui-dialog-layout>
|
||||
</umb-body-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -45,11 +52,6 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
|
||||
:host {
|
||||
display: relative;
|
||||
}
|
||||
uui-dialog-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
@@ -57,22 +59,18 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
|
||||
}
|
||||
|
||||
uui-scroll-container {
|
||||
line-height: 0;
|
||||
overflow-y: scroll;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
div {
|
||||
margin-top: var(--uui-size-space-5);
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbExamineFieldsViewerModalElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-modal-element-fields-viewer': UmbModalElementFieldsViewerElement;
|
||||
'umb-examine-fields-viewer-modal': UmbExamineFieldsViewerModalElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { SearchResultResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export type UmbExamineFieldsViewerModalData = {
|
||||
name: string;
|
||||
searchResult: SearchResultResponseModel;
|
||||
};
|
||||
|
||||
export type UmbExamineFieldsViewerModalValue = never;
|
||||
|
||||
export const UMB_EXAMINE_FIELDS_VIEWER_MODAL = new UmbModalToken<
|
||||
UmbExamineFieldsViewerModalData,
|
||||
UmbExamineFieldsViewerModalValue
|
||||
>('Umb.Modal.Examine.FieldsViewer', {
|
||||
modal: {
|
||||
type: 'sidebar',
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './examine-fields-viewer-modal.element.js';
|
||||
export * from './examine-fields-viewer-modal.token.js';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './fields-settings/index.js';
|
||||
export * from './fields-viewer/index.js';
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
const modals: Array<ManifestModal> = [
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.Examine.FieldsSettings',
|
||||
name: 'Examine Field Settings Modal',
|
||||
js: () => import('./fields-settings/examine-fields-settings-modal.element.js'),
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.Examine.FieldsViewer',
|
||||
name: 'Examine Field Viewer Modal',
|
||||
js: () => import('./fields-viewer/examine-fields-viewer-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests: Array<ManifestTypes> = [...modals];
|
||||
@@ -1,81 +0,0 @@
|
||||
import { html, css, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import type {
|
||||
UmbExamineFieldsSettingsModalValue,
|
||||
UmbExamineFieldsSettingsModalData} from '@umbraco-cms/backoffice/modal';
|
||||
import {
|
||||
UmbModalBaseElement,
|
||||
} from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
@customElement('umb-examine-fields-settings-modal')
|
||||
export default class UmbExamineFieldsSettingsModalElement extends UmbModalBaseElement<
|
||||
UmbExamineFieldsSettingsModalData,
|
||||
UmbExamineFieldsSettingsModalValue
|
||||
> {
|
||||
render() {
|
||||
if (this.value.fields) {
|
||||
return html`
|
||||
<uui-dialog-layout headline="Show fields">
|
||||
<uui-scroll-container id="field-settings">
|
||||
<span>
|
||||
${Object.values(this.value.fields).map((field, index) => {
|
||||
return html`<uui-toggle
|
||||
name="${field.name}"
|
||||
label="${field.name}"
|
||||
.checked="${field.exposed}"
|
||||
@change="${() => {
|
||||
this.value.fields ? (this.value.fields[index].exposed = !field.exposed) : '';
|
||||
}}"></uui-toggle>
|
||||
<br />`;
|
||||
})}
|
||||
</span>
|
||||
</uui-scroll-container>
|
||||
<div>
|
||||
<uui-button look="primary" label="Close sidebar" @click="${this._submitModal}">Close</uui-button>
|
||||
</div>
|
||||
</uui-dialog-layout>
|
||||
`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: relative;
|
||||
}
|
||||
|
||||
uui-dialog-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--uui-color-surface);
|
||||
box-shadow: var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24));
|
||||
border-radius: var(--uui-border-radius);
|
||||
padding: var(--uui-size-space-5);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
uui-scroll-container {
|
||||
overflow-y: scroll;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-top: var(--uui-size-space-5);
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-examine-fields-settings-modal': UmbExamineFieldsSettingsModalElement;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
|
||||
import type { IndexResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { HealthStatusResponseModel, IndexResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { HealthStatusModel, IndexerService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
@@ -25,35 +25,51 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._getIndexData();
|
||||
this.#loadData();
|
||||
}
|
||||
|
||||
private async _getIndexData() {
|
||||
async #loadData() {
|
||||
this._indexData = await this.#getIndexData();
|
||||
|
||||
if (this._indexData?.healthStatus.status === HealthStatusModel.REBUILDING) {
|
||||
this._buttonState = 'waiting';
|
||||
this._continuousPolling();
|
||||
} else {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async #getIndexData() {
|
||||
const { data } = await tryExecuteAndNotify(
|
||||
this,
|
||||
IndexerService.getIndexerByIndexName({ indexName: this.indexName }),
|
||||
);
|
||||
this._indexData = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO: Add continuous polling to update the status
|
||||
if (this._indexData?.healthStatus === HealthStatusModel.REBUILDING) {
|
||||
this._buttonState = 'waiting';
|
||||
private async _continuousPolling() {
|
||||
//Checking the server every 5 seconds to see if the index is still rebuilding.
|
||||
while (this._buttonState === 'waiting') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
this._indexData = await this.#getIndexData();
|
||||
if (this._indexData?.healthStatus.status !== HealthStatusModel.REBUILDING) {
|
||||
this._buttonState = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
private async _onRebuildHandler() {
|
||||
await umbConfirmModal(this, {
|
||||
headline: `Rebuild ${this.indexName}`,
|
||||
content: html`
|
||||
This will cause the index to be rebuilt.<br />
|
||||
headline: `${this.localize.term('examineManagement_rebuildIndex')} ${this.indexName}`,
|
||||
content: html`<umb-localize key="examineManagement_rebuildIndexWarning"
|
||||
>This will cause the index to be rebuilt.<br />
|
||||
Depending on how much content there is in your site this could take a while.<br />
|
||||
It is not recommended to rebuild an index during times of high website traffic or when editors are editing
|
||||
content.
|
||||
`,
|
||||
content.</umb-localize
|
||||
> `,
|
||||
color: 'danger',
|
||||
confirmLabel: 'Rebuild',
|
||||
confirmLabel: this.localize.term('examineManagement_rebuildIndex'),
|
||||
});
|
||||
|
||||
this._rebuild();
|
||||
@@ -68,9 +84,22 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
|
||||
this._buttonState = 'failed';
|
||||
return;
|
||||
}
|
||||
|
||||
this._buttonState = 'success';
|
||||
await this._getIndexData();
|
||||
await this.#loadData();
|
||||
}
|
||||
|
||||
#renderHealthStatus(healthStatus: HealthStatusResponseModel) {
|
||||
const msg = healthStatus.message ? healthStatus.message : healthStatus.status;
|
||||
switch (healthStatus.status) {
|
||||
case HealthStatusModel.HEALTHY:
|
||||
return html`<umb-icon name="icon-check color-green"></umb-icon>${msg}`;
|
||||
case HealthStatusModel.UNHEALTHY:
|
||||
return html`<umb-icon name="icon-error color-red"></umb-icon>${msg}`;
|
||||
case HealthStatusModel.REBUILDING:
|
||||
return html`<umb-icon name="icon-time color-yellow"></umb-icon>${msg}`;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -79,43 +108,41 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
|
||||
return html`
|
||||
<uui-box headline="${this.indexName}">
|
||||
<p>
|
||||
<strong>Health Status</strong><br />
|
||||
The health status of the ${this.indexName} and if it can be read
|
||||
<strong><umb-localize key="examineManagement_healthStatus">Health Status</umb-localize></strong
|
||||
><br />
|
||||
<umb-localize key="examineManagement_healthStatusDescription"
|
||||
>The health status of the ${this.indexName} and if it can be read</umb-localize
|
||||
>
|
||||
</p>
|
||||
<div>
|
||||
<uui-icon-essentials>
|
||||
${
|
||||
this._indexData.healthStatus === HealthStatusModel.UNHEALTHY
|
||||
? html`<uui-icon name="wrong" class="danger"></uui-icon>`
|
||||
: html`<uui-icon name="check" class="positive"></uui-icon>`
|
||||
}
|
||||
</uui-icon>
|
||||
</uui-icon-essentials>
|
||||
${this._indexData.healthStatus}
|
||||
</div>
|
||||
<div id="health-status">${this.#renderHealthStatus(this._indexData.healthStatus)}</div>
|
||||
</uui-box>
|
||||
${this.renderIndexSearch()} ${this.renderPropertyList()} ${this.renderTools()}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIndexSearch() {
|
||||
if (!this._indexData || this._indexData.healthStatus !== HealthStatusModel.HEALTHY) return nothing;
|
||||
// Do we want to show the search while rebuilding?
|
||||
if (!this._indexData || this._indexData.healthStatus.status === HealthStatusModel.REBUILDING) return nothing;
|
||||
return html`<umb-dashboard-examine-searcher .searcherName="${this.indexName}"></umb-dashboard-examine-searcher>`;
|
||||
}
|
||||
|
||||
private renderPropertyList() {
|
||||
if (!this._indexData) return nothing;
|
||||
|
||||
return html`<uui-box headline="Index info">
|
||||
<p>Lists the properties of the ${this.indexName}</p>
|
||||
return html`<uui-box headline=${this.localize.term('examineManagement_indexInfo')}>
|
||||
<p>
|
||||
<umb-localize key="examineManagement_indexInfoDescription"
|
||||
>Lists the properties of the ${this.indexName}</umb-localize
|
||||
>
|
||||
</p>
|
||||
<uui-table class="info">
|
||||
<uui-table-row>
|
||||
<uui-table-cell style="width:0px; font-weight: bold;"> documentCount </uui-table-cell>
|
||||
<uui-table-cell>${this._indexData.documentCount} </uui-table-cell>
|
||||
<uui-table-cell style="width:0px; font-weight: bold;">DocumentCount</uui-table-cell>
|
||||
<uui-table-cell>${this._indexData.documentCount}</uui-table-cell>
|
||||
</uui-table-row>
|
||||
<uui-table-row>
|
||||
<uui-table-cell style="width:0px; font-weight: bold;"> fieldCount </uui-table-cell>
|
||||
<uui-table-cell>${this._indexData.fieldCount} </uui-table-cell>
|
||||
<uui-table-cell style="width:0px; font-weight: bold;">FieldCount</uui-table-cell>
|
||||
<uui-table-cell>${this._indexData.fieldCount}</uui-table-cell>
|
||||
</uui-table-row>
|
||||
${this._indexData.providerProperties
|
||||
? Object.entries(this._indexData.providerProperties).map((entry) => {
|
||||
@@ -130,23 +157,26 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
private renderTools() {
|
||||
return html` <uui-box headline="Tools">
|
||||
<p>Tools to manage the ${this.indexName}</p>
|
||||
return html` <uui-box headline=${this.localize.term('examineManagement_tools')}>
|
||||
<p><umb-localize key="examineManagement_toolsDescription">Tools to manage the ${this.indexName}</umb-localize></p>
|
||||
<uui-button
|
||||
color="danger"
|
||||
look="primary"
|
||||
.state="${this._buttonState}"
|
||||
@click="${this._onRebuildHandler}"
|
||||
.disabled="${this._indexData?.canRebuild ? false : true}"
|
||||
label="Rebuild index">
|
||||
Rebuild
|
||||
</uui-button>
|
||||
label=${this.localize.term('examineManagement_rebuildIndex')}></uui-button>
|
||||
</uui-box>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
#health-status {
|
||||
display: flex;
|
||||
gap: var(--uui-size-6);
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -190,13 +220,6 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
|
||||
padding-right: var(--uui-size-space-5);
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: var(--uui-color-positive);
|
||||
}
|
||||
.danger {
|
||||
color: var(--uui-color-danger);
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -40,19 +40,38 @@ export class UmbDashboardExamineOverviewElement extends UmbLitElement {
|
||||
this._loadingSearchers = false;
|
||||
}
|
||||
|
||||
#renderStatus(status: HealthStatusModel) {
|
||||
switch (status) {
|
||||
case HealthStatusModel.HEALTHY:
|
||||
return html`<umb-icon name="icon-check color-green"></umb-icon>`;
|
||||
case HealthStatusModel.UNHEALTHY:
|
||||
return html`<umb-icon name="icon-error color-red"></umb-icon>`;
|
||||
case HealthStatusModel.REBUILDING:
|
||||
return html`<umb-icon name="icon-time color-yellow"></umb-icon>`;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box headline="Indexers" class="overview">
|
||||
<uui-box headline=${this.localize.term('examineManagement_indexers')} class="overview">
|
||||
<p>
|
||||
<strong>Manage Examine's indexes</strong><br />
|
||||
Allows you to view the details of each index and provides some tools for managing the indexes
|
||||
<strong><umb-localize key="examineManagement_manageIndexes">Manage Examine's indexes</umb-localize></strong
|
||||
><br />
|
||||
<umb-localize key="examineManagement_manageIndexesDescription"
|
||||
>Allows you to view the details of each index and provides some tools for managing the indexes</umb-localize
|
||||
>
|
||||
</p>
|
||||
${this.renderIndexersList()}
|
||||
</uui-box>
|
||||
<uui-box headline="Searchers">
|
||||
<uui-box headline=${this.localize.term('examineManagement_searchers')}>
|
||||
<p>
|
||||
<strong>Configured Searchers</strong><br />
|
||||
Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)
|
||||
<strong><umb-localize key="examineManagement_configuredSearchers">Configured Searchers</umb-localize></strong
|
||||
><br />
|
||||
<umb-localize key="examineManagement_configuredSearchersDescription"
|
||||
>Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)</umb-localize
|
||||
>
|
||||
</p>
|
||||
${this.renderSearchersList()}
|
||||
</uui-box>
|
||||
@@ -66,16 +85,7 @@ export class UmbDashboardExamineOverviewElement extends UmbLitElement {
|
||||
${this._indexers.map((index) => {
|
||||
return html`
|
||||
<uui-table-row>
|
||||
<uui-table-cell style="width:0px">
|
||||
<uui-icon-essentials>
|
||||
${
|
||||
index.healthStatus === HealthStatusModel.UNHEALTHY
|
||||
? html`<uui-icon name="wrong" class="danger"></uui-icon>`
|
||||
: html`<uui-icon name="check" class="positive"></uui-icon>`
|
||||
}
|
||||
</uui-icon>
|
||||
</uui-icon-essentials>
|
||||
</uui-table-cell>
|
||||
<uui-table-cell style="width:0px"> ${this.#renderStatus(index.healthStatus.status)} </uui-table-cell>
|
||||
<uui-table-cell>
|
||||
<a href="${window.location.href.replace(/\/+$/, '')}/index/${index.name}">${index.name}</a>
|
||||
</uui-table-cell>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { UMB_EXAMINE_FIELDS_SETTINGS_MODAL, UMB_EXAMINE_FIELDS_VIEWER_MODAL } from '../modal/index.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, nothing, customElement, state, query, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT, UMB_EXAMINE_FIELDS_SETTINGS_MODAL } from '@umbraco-cms/backoffice/modal';
|
||||
import {
|
||||
UMB_MODAL_MANAGER_CONTEXT,
|
||||
UMB_WORKSPACE_MODAL,
|
||||
UmbModalRouteRegistrationController,
|
||||
} from '@umbraco-cms/backoffice/modal';
|
||||
import type { SearchResultResponseModel, FieldPresentationModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { SearcherService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
import './modal-views/fields-viewer.element.js';
|
||||
import './modal-views/fields-settings-modal.element.js';
|
||||
|
||||
interface ExposedSearchResultField {
|
||||
name: string;
|
||||
exposed: boolean;
|
||||
@@ -31,15 +33,27 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
|
||||
@query('#search-input')
|
||||
private _searchInput!: HTMLInputElement;
|
||||
|
||||
private _onNameClick() {
|
||||
// TODO:
|
||||
alert('TODO: Open workspace for ' + this.searcherName);
|
||||
}
|
||||
@state()
|
||||
private _workspacePath = 'aa';
|
||||
|
||||
private _onKeyPress(e: KeyboardEvent) {
|
||||
e.key == 'Enter' ? this._onSearch() : undefined;
|
||||
}
|
||||
|
||||
#entityType = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
|
||||
.addAdditionalPath(':entityType')
|
||||
.onSetup((routingInfo) => {
|
||||
return { data: { entityType: routingInfo.entityType, preset: {} } };
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
this._workspacePath = routeBuilder({ entityType: this.#entityType });
|
||||
});
|
||||
}
|
||||
|
||||
private async _onSearch() {
|
||||
if (!this._searchInput.value.length) return;
|
||||
this._searchLoading = true;
|
||||
@@ -86,36 +100,48 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
|
||||
const modalContext = modalManager.open(this, UMB_EXAMINE_FIELDS_SETTINGS_MODAL, {
|
||||
value: { fields: this._exposedFields ?? [] },
|
||||
});
|
||||
modalContext?.onSubmit().then((value) => {
|
||||
this._exposedFields = value.fields;
|
||||
});
|
||||
await modalContext.onSubmit().catch(() => undefined);
|
||||
|
||||
const value = modalContext.getValue();
|
||||
|
||||
this._exposedFields = value?.fields;
|
||||
}
|
||||
|
||||
async #onFieldViewClick(rowData: SearchResultResponseModel) {
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
modalManager.open(this, 'umb-modal-element-fields-viewer', {
|
||||
|
||||
const modalContext = modalManager.open(this, UMB_EXAMINE_FIELDS_VIEWER_MODAL, {
|
||||
modal: {
|
||||
type: 'sidebar',
|
||||
size: 'medium',
|
||||
},
|
||||
data: { ...rowData, name: this.getSearchResultNodeName(rowData) },
|
||||
data: { searchResult: rowData, name: this.getSearchResultNodeName(rowData) },
|
||||
});
|
||||
await modalContext.onSubmit().catch(() => undefined);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box headline="Search">
|
||||
<p>Search the ${this.searcherName} and view the results</p>
|
||||
<uui-box headline=${this.localize.term('general_search')}>
|
||||
<p>
|
||||
<umb-localize key="examineManagement_searchDescription"
|
||||
>Search the ${this.searcherName} and view the results</umb-localize
|
||||
>
|
||||
</p>
|
||||
<div class="flex">
|
||||
<uui-input
|
||||
type="search"
|
||||
id="search-input"
|
||||
placeholder="Type to filter..."
|
||||
label="Type to filter"
|
||||
placeholder=${this.localize.term('placeholders_filter')}
|
||||
label=${this.localize.term('placeholders_filter')}
|
||||
@keypress=${this._onKeyPress}
|
||||
${umbFocus()}>
|
||||
</uui-input>
|
||||
<uui-button color="positive" look="primary" label="Search" @click="${this._onSearch}"> Search </uui-button>
|
||||
<uui-button
|
||||
color="positive"
|
||||
look="primary"
|
||||
label=${this.localize.term('general_search')}
|
||||
@click="${this._onSearch}"></uui-button>
|
||||
</div>
|
||||
${this.renderSearchResults()}
|
||||
</uui-box>
|
||||
@@ -128,28 +154,44 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
|
||||
return nodeNameField?.values?.join(', ') ?? '';
|
||||
}
|
||||
|
||||
#getEntityTypeFromIndexType(indexType: string) {
|
||||
switch (indexType) {
|
||||
case 'content':
|
||||
return 'document';
|
||||
default:
|
||||
return indexType;
|
||||
}
|
||||
}
|
||||
|
||||
private renderSearchResults() {
|
||||
if (this._searchLoading) return html`<uui-loader></uui-loader>`;
|
||||
if (!this._searchResults) return nothing;
|
||||
if (!this._searchResults.length) {
|
||||
return html`<p>No results found</p>`;
|
||||
return html`<p>${this.localize.term('examineManagement_noResults')}</p>`;
|
||||
}
|
||||
return html`<div class="table-container">
|
||||
<uui-scroll-container>
|
||||
<uui-table class="search">
|
||||
<uui-table-head>
|
||||
<uui-table-head-cell style="width:0">Score</uui-table-head-cell>
|
||||
<uui-table-head-cell style="width:0">Id</uui-table-head-cell>
|
||||
<uui-table-head-cell>Name</uui-table-head-cell>
|
||||
<uui-table-head-cell>Fields</uui-table-head-cell>
|
||||
<uui-table-head-cell style="width:0">${this.localize.term('general_id')}</uui-table-head-cell>
|
||||
<uui-table-head-cell>${this.localize.term('general_name')}</uui-table-head-cell>
|
||||
<uui-table-head-cell>${this.localize.term('examineManagement_fields')}</uui-table-head-cell>
|
||||
${this.renderHeadCells()}
|
||||
</uui-table-head>
|
||||
${this._searchResults?.map((rowData) => {
|
||||
const indexType = rowData.fields?.find((field) => field.name === '__IndexType')?.values?.join(', ') ?? '';
|
||||
this.#entityType = this.#getEntityTypeFromIndexType(indexType);
|
||||
const unique = rowData.fields?.find((field) => field.name === '__Key')?.values?.join(', ') ?? '';
|
||||
|
||||
return html`<uui-table-row>
|
||||
<uui-table-cell> ${rowData.score} </uui-table-cell>
|
||||
<uui-table-cell> ${rowData.id} </uui-table-cell>
|
||||
<uui-table-cell>
|
||||
<uui-button look="secondary" label="Open workspace for this document" @click="${this._onNameClick}">
|
||||
<uui-button
|
||||
look="secondary"
|
||||
label=${this.localize.term('actions_editContent')}
|
||||
href=${this._workspacePath + this.#entityType + '/edit/' + unique}>
|
||||
${this.getSearchResultNodeName(rowData)}
|
||||
</uui-button>
|
||||
</uui-table-cell>
|
||||
@@ -157,9 +199,10 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
|
||||
<uui-button
|
||||
class="bright"
|
||||
look="secondary"
|
||||
label="Open sidebar to see all fields"
|
||||
label=${this.localize.term('examineManagement_fieldValues')}
|
||||
@click=${() => this.#onFieldViewClick(rowData)}>
|
||||
${rowData.fields ? Object.keys(rowData.fields).length : ''} fields
|
||||
${rowData.fields ? Object.keys(rowData.fields).length : ''}
|
||||
${this.localize.term('examineManagement_fields')}
|
||||
</uui-button>
|
||||
</uui-table-cell>
|
||||
${rowData.fields ? this.renderBodyCells(rowData.fields) : ''}
|
||||
@@ -185,7 +228,7 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
|
||||
<span>${field.name}</span>
|
||||
<uui-button
|
||||
look="secondary"
|
||||
label="Close field ${field.name}"
|
||||
label="${this.localize.term('actions_remove')} ${field.name}"
|
||||
compact
|
||||
@click="${() => {
|
||||
this._exposedFields = this._exposedFields?.map((f) => {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { manifests as examineManifests } from './examine-management-dashboard/manifests.js';
|
||||
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
import './examine-management-dashboard/index.js';
|
||||
|
||||
export const manifests: Array<ManifestTypes> = [
|
||||
{
|
||||
type: 'headerApp',
|
||||
@@ -37,10 +40,5 @@ export const manifests: Array<ManifestTypes> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.ExamineFieldsSettings',
|
||||
name: 'Examine Field Settings Modal',
|
||||
js: () => import('./examine-management-dashboard/views/modal-views/fields-settings-modal.element.js'),
|
||||
},
|
||||
...examineManifests,
|
||||
];
|
||||
|
||||
@@ -52,7 +52,7 @@ export class UmbInputWebhookEventsElement extends UmbLitElement {
|
||||
(item) => item.alias,
|
||||
(item) => html`
|
||||
<span>${item.eventName}</span>
|
||||
<uui-button @click=${() => this.#removeEvent(item.alias)} label="remove"></uui-button>
|
||||
<uui-button label=${this.localize.term('general_remove')} @click=${() => this.#removeEvent(item.alias)}></uui-button>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
@@ -60,7 +60,7 @@ export class UmbInputWebhookEventsElement extends UmbLitElement {
|
||||
|
||||
render() {
|
||||
return html`${this.#renderEvents()}
|
||||
<uui-button id="add" look="placeholder" label="Add" @click=${this.#openModal}></uui-button>`;
|
||||
<uui-button id="choose" look="placeholder" label=${this.localize.term('general_choose')} @click=${this.#openModal}></uui-button>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
@@ -73,7 +73,7 @@ export class UmbInputWebhookEventsElement extends UmbLitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#add {
|
||||
#choose {
|
||||
grid-column: -1 / 1;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
// Don't edit this file directly. It is generated by /devops/tsconfig/index.js
|
||||
/* -------------------------------------------------------------------------
|
||||
|
||||
|
||||
DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
|
||||
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------- */
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
|
||||
Reference in New Issue
Block a user