// Copyright 2013 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 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 type {PlatformFileSystem} from '../../models/persistence/PlatformFileSystem.js'; import * as UI from '../../ui/legacy/legacy.js'; import {Directives, html, render} from '../../ui/lit/lit.js'; import editFileSystemViewStyles from './editFileSystemView.css.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('panels/settings/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<{columnId: string, valueBeforeEditing: string, newText: string}>) => void; onDelete: (event: CustomEvent) => void; } export type View = (input: EditFileSystemViewInput, output: object, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, _output, target) => { // clang-format off render(html`
${i18nString(UIStrings.url)} ${input.fileSystemPath} ${input.excludedFolderPaths.map((path, index) => html` `)}
${i18nString(UIStrings.excludedFolders)}
${path.path}
${input.excludedFolderPaths.filter(({status}) => status !== ExcludedFolderStatus.VALID).map(({status}) => html`${statusString(status)}`)}
`, target); // 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(element); this.#view = view; } set fileSystem(fileSystem: PlatformFileSystem) { this.#fileSystem = fileSystem; this.#resyncExcludedFolderPaths(); this.requestUpdate(); } override wasShown(): void { super.wasShown(); 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.currentTarget as HTMLElement).dataset.index ?? '-1', e.detail.valueBeforeEditing, e.detail.newText), onDelete: e => this.#onDelete((e.currentTarget as HTMLElement).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] === '/' ? '' : '/'); } }