chrome-devtools-frontend
Version:
Chrome DevTools UI
242 lines (209 loc) • 8.94 kB
text/typescript
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import '../../ui/legacy/components/data_grid/data_grid.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, render} from '../../ui/lit/lit.js';
import editFileSystemViewStyles from './editFileSystemView.css.js';
import type {PlatformFileSystem} from './PlatformFileSystem.js';
const {styleMap} = Directives;
const UIStrings = {
/**
*@description Text in Edit File System View of the Workspace settings in Settings to indicate that the following string is a folder URL
*/
url: 'URL',
/**
*@description Text in Edit File System View of the Workspace settings in Settings
*/
excludedFolders: 'Excluded sub-folders',
/**
*@description Error message when a file system path is an empty string.
*/
enterAPath: 'Enter a path',
/**
*@description Error message when a file system path is identical to an existing path.
*/
enterAUniquePath: 'Enter a unique path',
} as const;
const str_ = i18n.i18n.registerUIStrings('models/persistence/EditFileSystemView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export const enum ExcludedFolderStatus {
VALID = 1,
ERROR_NOT_A_PATH = 2,
ERROR_NOT_UNIQUE = 3,
}
function statusString(status: ExcludedFolderStatus): Platform.UIString.LocalizedString {
switch (status) {
case ExcludedFolderStatus.ERROR_NOT_A_PATH:
return i18nString(UIStrings.enterAPath);
case ExcludedFolderStatus.ERROR_NOT_UNIQUE:
return i18nString(UIStrings.enterAUniquePath);
case ExcludedFolderStatus.VALID:
throw new Error('unreachable');
}
}
export interface PathWithStatus {
path: Platform.DevToolsPath.EncodedPathString;
status: ExcludedFolderStatus;
}
export interface EditFileSystemViewInput {
fileSystemPath: Platform.DevToolsPath.UrlString;
excludedFolderPaths: PathWithStatus[];
onCreate: (event: CustomEvent<{url?: string}>) => void;
onEdit:
(event: CustomEvent<{node: HTMLElement, columnId: string, valueBeforeEditing: string, newText: string}>) => void;
onDelete: (event: CustomEvent<HTMLElement>) => void;
}
export type View = (input: EditFileSystemViewInput, output: object, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, _output, target) => {
// clang-format off
render(html`
<style>${editFileSystemViewStyles}</style>
<div class="excluded-folder-header">
<span>${i18nString(UIStrings.url)}</span>
<span class="excluded-folder-url">${input.fileSystemPath}</span>
<devtools-data-grid
@create=${input.onCreate}
@edit=${input.onEdit}
@delete=${input.onDelete}
class="exclude-subfolders-table"
parts="excluded-folder-row-with-error"
inline striped>
<table>
<thead>
<tr>
<th id="url" editable>${i18nString(UIStrings.excludedFolders)}</th>
</tr>
</thead>
<tbody>
${input.excludedFolderPaths.map((path, index) => html`
<tr data-url=${path.path} data-index=${index}>
<td style=${styleMap({backgroundColor: path.status !== ExcludedFolderStatus.VALID ? 'var(--sys-color-error-container)' : undefined})}>${path.path}</td>
</tr>
`)}
<tr placeholder></tr>
</tbody>
</table>
</devtools-data-grid>
${input.excludedFolderPaths.filter(({status}) => status !== ExcludedFolderStatus.VALID).map(({status}) =>
html`<span class="excluded-folder-error">${statusString(status)}</span>`)}
</div>`, target, {host: input});
// clang-format on
};
export class EditFileSystemView extends UI.Widget.VBox {
#fileSystem?: PlatformFileSystem;
#excludedFolderPaths: PathWithStatus[] = [];
readonly #view: View;
constructor(element: HTMLElement|undefined, view: View = DEFAULT_VIEW) {
super(undefined, undefined, element);
this.#view = view;
}
set fileSystem(fileSystem: PlatformFileSystem) {
this.#fileSystem = fileSystem;
this.#resyncExcludedFolderPaths();
this.requestUpdate();
}
override wasShown(): void {
this.#resyncExcludedFolderPaths();
this.requestUpdate();
}
#resyncExcludedFolderPaths(): void {
this.#excludedFolderPaths = this.#fileSystem?.excludedFolders()
.values()
.map(path => ({path, status: ExcludedFolderStatus.VALID}))
.toArray() ??
[];
}
override performUpdate(): void {
const input: EditFileSystemViewInput = {
fileSystemPath: this.#fileSystem?.path() ?? Platform.DevToolsPath.urlString``,
excludedFolderPaths: this.#excludedFolderPaths,
onCreate: e => this.#onCreate(e.detail.url),
onEdit: e => this.#onEdit(e.detail.node.dataset.index ?? '-1', e.detail.valueBeforeEditing, e.detail.newText),
onDelete: e => this.#onDelete(e.detail.dataset.index ?? '-1'),
};
this.#view(input, {}, this.contentElement);
}
#onCreate(url?: string): void {
if (url === undefined) {
// The data grid fires onCreate even when the user just selects and then deselects the
// creation row. Ignore those occurrences.
return;
}
const pathWithStatus = this.#validateFolder(url);
this.#excludedFolderPaths.push(pathWithStatus);
if (pathWithStatus.status === ExcludedFolderStatus.VALID) {
this.#fileSystem?.addExcludedFolder(pathWithStatus.path);
}
this.requestUpdate();
}
#onEdit(idx: string, valueBeforeEditing: string, newText: string): void {
const index = Number.parseInt(idx, 10);
if (index < 0 || index >= this.#excludedFolderPaths.length) {
return;
}
const pathWithStatus = this.#validateFolder(newText);
const oldPathWithStatus = this.#excludedFolderPaths[index];
this.#excludedFolderPaths[index] = pathWithStatus;
if (oldPathWithStatus.status === ExcludedFolderStatus.VALID) {
this.#fileSystem?.removeExcludedFolder(valueBeforeEditing as Platform.DevToolsPath.EncodedPathString);
}
if (pathWithStatus.status === ExcludedFolderStatus.VALID) {
this.#fileSystem?.addExcludedFolder(pathWithStatus.path);
}
this.requestUpdate();
}
#onDelete(idx: string): void {
const index = Number.parseInt(idx, 10);
if (index < 0 || index >= this.#excludedFolderPaths.length) {
return;
}
this.#fileSystem?.removeExcludedFolder(this.#excludedFolderPaths[index].path);
this.#excludedFolderPaths.splice(index, 1);
this.requestUpdate();
}
#validateFolder(rawInput: string): PathWithStatus {
const path = EditFileSystemView.#normalizePrefix(rawInput.trim()) as Platform.DevToolsPath.EncodedPathString;
if (!path) {
return {path, status: ExcludedFolderStatus.ERROR_NOT_A_PATH};
}
if (this.#excludedFolderPaths.findIndex(({path: p}) => p === path) !== -1) {
return {path, status: ExcludedFolderStatus.ERROR_NOT_UNIQUE};
}
return {path, status: ExcludedFolderStatus.VALID};
}
static #normalizePrefix(prefix: string): string {
if (!prefix) {
return '';
}
return prefix + (prefix[prefix.length - 1] === '/' ? '' : '/');
}
}