Path: blob/main/src/vs/workbench/contrib/issue/browser/issueFormService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import { safeSetInnerHtml } from '../../../../base/browser/domSanitize.js';5import { createStyleSheet } from '../../../../base/browser/domStylesheets.js';6import { getMenuWidgetCSS, Menu, unthemedMenuStyles } from '../../../../base/browser/ui/menu/menu.js';7import { DisposableStore } from '../../../../base/common/lifecycle.js';8import { isLinux, isWindows } from '../../../../base/common/platform.js';9import Severity from '../../../../base/common/severity.js';10import { localize } from '../../../../nls.js';11import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';12import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';13import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';14import { ExtensionIdentifier, ExtensionIdentifierSet } from '../../../../platform/extensions/common/extensions.js';15import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';16import { ILogService } from '../../../../platform/log/common/log.js';17import product from '../../../../platform/product/common/product.js';18import { IRectangle } from '../../../../platform/window/common/window.js';19import { AuxiliaryWindowMode, IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js';20import { IHostService } from '../../../services/host/browser/host.js';21import { IIssueFormService, IssueReporterData } from '../common/issue.js';22import BaseHtml from './issueReporterPage.js';23import { IssueWebReporter } from './issueReporterService.js';24import './media/issueReporter.css';2526export interface IssuePassData {27issueTitle: string;28issueBody: string;29}3031export class IssueFormService implements IIssueFormService {3233readonly _serviceBrand: undefined;3435protected currentData: IssueReporterData | undefined;3637protected issueReporterWindow: Window | null = null;38protected extensionIdentifierSet: ExtensionIdentifierSet = new ExtensionIdentifierSet();3940protected arch: string = '';41protected release: string = '';42protected type: string = '';4344constructor(45@IInstantiationService protected readonly instantiationService: IInstantiationService,46@IAuxiliaryWindowService protected readonly auxiliaryWindowService: IAuxiliaryWindowService,47@IMenuService protected readonly menuService: IMenuService,48@IContextKeyService protected readonly contextKeyService: IContextKeyService,49@ILogService protected readonly logService: ILogService,50@IDialogService protected readonly dialogService: IDialogService,51@IHostService protected readonly hostService: IHostService52) { }5354async openReporter(data: IssueReporterData): Promise<void> {55if (this.hasToReload(data)) {56return;57}5859await this.openAuxIssueReporter(data);6061if (this.issueReporterWindow) {62const issueReporter = this.instantiationService.createInstance(IssueWebReporter, false, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow);63issueReporter.render();64}65}6667async openAuxIssueReporter(data: IssueReporterData, bounds?: IRectangle): Promise<void> {6869let issueReporterBounds: Partial<IRectangle> = { width: 700, height: 800 };7071// Center Issue Reporter Window based on bounds from native host service72if (bounds && bounds.x && bounds.y) {73const centerX = bounds.x + bounds.width / 2;74const centerY = bounds.y + bounds.height / 2;75issueReporterBounds = { ...issueReporterBounds, x: centerX - 350, y: centerY - 400 };76}7778const disposables = new DisposableStore();7980// Auxiliary Window81const auxiliaryWindow = disposables.add(await this.auxiliaryWindowService.open({ mode: AuxiliaryWindowMode.Normal, bounds: issueReporterBounds, nativeTitlebar: true, disableFullscreen: true }));8283const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac';8485if (auxiliaryWindow) {86await auxiliaryWindow.whenStylesHaveLoaded;87auxiliaryWindow.window.document.title = 'Issue Reporter';88auxiliaryWindow.window.document.body.classList.add('issue-reporter-body', 'monaco-workbench', platformClass);8990// removes preset monaco-workbench container91auxiliaryWindow.container.remove();9293// The Menu class uses a static globalStyleSheet that's created lazily on first menu creation.94// Since auxiliary windows clone stylesheets from main window, but Menu.globalStyleSheet95// may not exist yet in main window, we need to ensure menu styles are available here.96if (!Menu.globalStyleSheet) {97const menuStyleSheet = createStyleSheet(auxiliaryWindow.window.document.head);98menuStyleSheet.textContent = getMenuWidgetCSS(unthemedMenuStyles, false);99}100101// custom issue reporter wrapper that preserves critical auxiliary window container styles102const div = document.createElement('div');103div.classList.add('monaco-workbench');104auxiliaryWindow.window.document.body.appendChild(div);105safeSetInnerHtml(div, BaseHtml(), {106// Also allow input elements107allowedTags: {108augment: [109'input',110'select',111'checkbox',112'textarea',113]114},115allowedAttributes: {116augment: [117'id',118'class',119'style',120'textarea',121]122}123});124125this.issueReporterWindow = auxiliaryWindow.window;126} else {127console.error('Failed to open auxiliary window');128disposables.dispose();129}130131// handle closing issue reporter132this.issueReporterWindow?.addEventListener('beforeunload', () => {133auxiliaryWindow.window.close();134disposables.dispose();135this.issueReporterWindow = null;136});137}138139async sendReporterMenu(extensionId: string): Promise<IssueReporterData | undefined> {140const menu = this.menuService.createMenu(MenuId.IssueReporter, this.contextKeyService);141142// render menu and dispose143const actions = menu.getActions({ renderShortTitle: true }).flatMap(entry => entry[1]);144for (const action of actions) {145try {146if (action.item && 'source' in action.item && action.item.source?.id.toLowerCase() === extensionId.toLowerCase()) {147this.extensionIdentifierSet.add(extensionId.toLowerCase());148await action.run();149}150} catch (error) {151console.error(error);152}153}154155if (!this.extensionIdentifierSet.has(extensionId)) {156// send undefined to indicate no action was taken157return undefined;158}159160// we found the extension, now we clean up the menu and remove it from the set. This is to ensure that we do duplicate extension identifiers161this.extensionIdentifierSet.delete(new ExtensionIdentifier(extensionId));162menu.dispose();163164const result = this.currentData;165166// reset current data.167this.currentData = undefined;168169return result ?? undefined;170}171172//#region used by issue reporter173174async closeReporter(): Promise<void> {175this.issueReporterWindow?.close();176}177178async reloadWithExtensionsDisabled(): Promise<void> {179if (this.issueReporterWindow) {180try {181await this.hostService.reload({ disableExtensions: true });182} catch (error) {183this.logService.error(error);184}185}186}187188async showConfirmCloseDialog(): Promise<void> {189await this.dialogService.prompt({190type: Severity.Warning,191message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"),192buttons: [193{194label: localize({ key: 'yes', comment: ['&& denotes a mnemonic'] }, "&&Yes"),195run: () => {196this.closeReporter();197this.issueReporterWindow = null;198}199},200{201label: localize('cancel', "Cancel"),202run: () => { }203}204]205});206}207208async showClipboardDialog(): Promise<boolean> {209let result = false;210211await this.dialogService.prompt({212type: Severity.Warning,213message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub directly. The data will be copied to the clipboard, please paste it into the GitHub issue page that is opened."),214buttons: [215{216label: localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"),217run: () => { result = true; }218},219{220label: localize('cancel', "Cancel"),221run: () => { result = false; }222}223]224});225226return result;227}228229hasToReload(data: IssueReporterData): boolean {230if (data.extensionId && this.extensionIdentifierSet.has(data.extensionId)) {231this.currentData = data;232this.issueReporterWindow?.focus();233return true;234}235236if (this.issueReporterWindow) {237this.issueReporterWindow.focus();238return true;239}240241return false;242}243}244245246