Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/issue/browser/issueFormService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import { safeSetInnerHtml } from '../../../../base/browser/domSanitize.js';
6
import { createStyleSheet } from '../../../../base/browser/domStylesheets.js';
7
import { getMenuWidgetCSS, Menu, unthemedMenuStyles } from '../../../../base/browser/ui/menu/menu.js';
8
import { DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { isLinux, isWindows } from '../../../../base/common/platform.js';
10
import Severity from '../../../../base/common/severity.js';
11
import { localize } from '../../../../nls.js';
12
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
13
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
14
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
15
import { ExtensionIdentifier, ExtensionIdentifierSet } from '../../../../platform/extensions/common/extensions.js';
16
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
17
import { ILogService } from '../../../../platform/log/common/log.js';
18
import product from '../../../../platform/product/common/product.js';
19
import { IRectangle } from '../../../../platform/window/common/window.js';
20
import { AuxiliaryWindowMode, IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js';
21
import { IHostService } from '../../../services/host/browser/host.js';
22
import { IIssueFormService, IssueReporterData } from '../common/issue.js';
23
import BaseHtml from './issueReporterPage.js';
24
import { IssueWebReporter } from './issueReporterService.js';
25
import './media/issueReporter.css';
26
27
export interface IssuePassData {
28
issueTitle: string;
29
issueBody: string;
30
}
31
32
export class IssueFormService implements IIssueFormService {
33
34
readonly _serviceBrand: undefined;
35
36
protected currentData: IssueReporterData | undefined;
37
38
protected issueReporterWindow: Window | null = null;
39
protected extensionIdentifierSet: ExtensionIdentifierSet = new ExtensionIdentifierSet();
40
41
protected arch: string = '';
42
protected release: string = '';
43
protected type: string = '';
44
45
constructor(
46
@IInstantiationService protected readonly instantiationService: IInstantiationService,
47
@IAuxiliaryWindowService protected readonly auxiliaryWindowService: IAuxiliaryWindowService,
48
@IMenuService protected readonly menuService: IMenuService,
49
@IContextKeyService protected readonly contextKeyService: IContextKeyService,
50
@ILogService protected readonly logService: ILogService,
51
@IDialogService protected readonly dialogService: IDialogService,
52
@IHostService protected readonly hostService: IHostService
53
) { }
54
55
async openReporter(data: IssueReporterData): Promise<void> {
56
if (this.hasToReload(data)) {
57
return;
58
}
59
60
await this.openAuxIssueReporter(data);
61
62
if (this.issueReporterWindow) {
63
const issueReporter = this.instantiationService.createInstance(IssueWebReporter, false, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow);
64
issueReporter.render();
65
}
66
}
67
68
async openAuxIssueReporter(data: IssueReporterData, bounds?: IRectangle): Promise<void> {
69
70
let issueReporterBounds: Partial<IRectangle> = { width: 700, height: 800 };
71
72
// Center Issue Reporter Window based on bounds from native host service
73
if (bounds && bounds.x && bounds.y) {
74
const centerX = bounds.x + bounds.width / 2;
75
const centerY = bounds.y + bounds.height / 2;
76
issueReporterBounds = { ...issueReporterBounds, x: centerX - 350, y: centerY - 400 };
77
}
78
79
const disposables = new DisposableStore();
80
81
// Auxiliary Window
82
const auxiliaryWindow = disposables.add(await this.auxiliaryWindowService.open({ mode: AuxiliaryWindowMode.Normal, bounds: issueReporterBounds, nativeTitlebar: true, disableFullscreen: true }));
83
84
const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac';
85
86
if (auxiliaryWindow) {
87
await auxiliaryWindow.whenStylesHaveLoaded;
88
auxiliaryWindow.window.document.title = 'Issue Reporter';
89
auxiliaryWindow.window.document.body.classList.add('issue-reporter-body', 'monaco-workbench', platformClass);
90
91
// removes preset monaco-workbench container
92
auxiliaryWindow.container.remove();
93
94
// The Menu class uses a static globalStyleSheet that's created lazily on first menu creation.
95
// Since auxiliary windows clone stylesheets from main window, but Menu.globalStyleSheet
96
// may not exist yet in main window, we need to ensure menu styles are available here.
97
if (!Menu.globalStyleSheet) {
98
const menuStyleSheet = createStyleSheet(auxiliaryWindow.window.document.head);
99
menuStyleSheet.textContent = getMenuWidgetCSS(unthemedMenuStyles, false);
100
}
101
102
// custom issue reporter wrapper that preserves critical auxiliary window container styles
103
const div = document.createElement('div');
104
div.classList.add('monaco-workbench');
105
auxiliaryWindow.window.document.body.appendChild(div);
106
safeSetInnerHtml(div, BaseHtml(), {
107
// Also allow input elements
108
allowedTags: {
109
augment: [
110
'input',
111
'select',
112
'checkbox',
113
'textarea',
114
]
115
},
116
allowedAttributes: {
117
augment: [
118
'id',
119
'class',
120
'style',
121
'textarea',
122
]
123
}
124
});
125
126
this.issueReporterWindow = auxiliaryWindow.window;
127
} else {
128
console.error('Failed to open auxiliary window');
129
disposables.dispose();
130
}
131
132
// handle closing issue reporter
133
this.issueReporterWindow?.addEventListener('beforeunload', () => {
134
auxiliaryWindow.window.close();
135
disposables.dispose();
136
this.issueReporterWindow = null;
137
});
138
}
139
140
async sendReporterMenu(extensionId: string): Promise<IssueReporterData | undefined> {
141
const menu = this.menuService.createMenu(MenuId.IssueReporter, this.contextKeyService);
142
143
// render menu and dispose
144
const actions = menu.getActions({ renderShortTitle: true }).flatMap(entry => entry[1]);
145
for (const action of actions) {
146
try {
147
if (action.item && 'source' in action.item && action.item.source?.id.toLowerCase() === extensionId.toLowerCase()) {
148
this.extensionIdentifierSet.add(extensionId.toLowerCase());
149
await action.run();
150
}
151
} catch (error) {
152
console.error(error);
153
}
154
}
155
156
if (!this.extensionIdentifierSet.has(extensionId)) {
157
// send undefined to indicate no action was taken
158
return undefined;
159
}
160
161
// 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 identifiers
162
this.extensionIdentifierSet.delete(new ExtensionIdentifier(extensionId));
163
menu.dispose();
164
165
const result = this.currentData;
166
167
// reset current data.
168
this.currentData = undefined;
169
170
return result ?? undefined;
171
}
172
173
//#region used by issue reporter
174
175
async closeReporter(): Promise<void> {
176
this.issueReporterWindow?.close();
177
}
178
179
async reloadWithExtensionsDisabled(): Promise<void> {
180
if (this.issueReporterWindow) {
181
try {
182
await this.hostService.reload({ disableExtensions: true });
183
} catch (error) {
184
this.logService.error(error);
185
}
186
}
187
}
188
189
async showConfirmCloseDialog(): Promise<void> {
190
await this.dialogService.prompt({
191
type: Severity.Warning,
192
message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"),
193
buttons: [
194
{
195
label: localize({ key: 'yes', comment: ['&& denotes a mnemonic'] }, "&&Yes"),
196
run: () => {
197
this.closeReporter();
198
this.issueReporterWindow = null;
199
}
200
},
201
{
202
label: localize('cancel', "Cancel"),
203
run: () => { }
204
}
205
]
206
});
207
}
208
209
async showClipboardDialog(): Promise<boolean> {
210
let result = false;
211
212
await this.dialogService.prompt({
213
type: Severity.Warning,
214
message: 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."),
215
buttons: [
216
{
217
label: localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"),
218
run: () => { result = true; }
219
},
220
{
221
label: localize('cancel', "Cancel"),
222
run: () => { result = false; }
223
}
224
]
225
});
226
227
return result;
228
}
229
230
hasToReload(data: IssueReporterData): boolean {
231
if (data.extensionId && this.extensionIdentifierSet.has(data.extensionId)) {
232
this.currentData = data;
233
this.issueReporterWindow?.focus();
234
return true;
235
}
236
237
if (this.issueReporterWindow) {
238
this.issueReporterWindow.focus();
239
return true;
240
}
241
242
return false;
243
}
244
}
245
246