Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/dialogs/common/dialogs.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
6
import { Event } from '../../../base/common/event.js';
7
import { ThemeIcon } from '../../../base/common/themables.js';
8
import { IMarkdownString } from '../../../base/common/htmlContent.js';
9
import { basename } from '../../../base/common/resources.js';
10
import Severity from '../../../base/common/severity.js';
11
import { URI } from '../../../base/common/uri.js';
12
import { localize } from '../../../nls.js';
13
import { createDecorator } from '../../instantiation/common/instantiation.js';
14
import { ITelemetryData } from '../../telemetry/common/telemetry.js';
15
import { MessageBoxOptions } from '../../../base/parts/sandbox/common/electronTypes.js';
16
import { mnemonicButtonLabel } from '../../../base/common/labels.js';
17
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
18
import { IProductService } from '../../product/common/productService.js';
19
import { deepClone } from '../../../base/common/objects.js';
20
21
export interface IDialogArgs {
22
readonly confirmArgs?: IConfirmDialogArgs;
23
readonly inputArgs?: IInputDialogArgs;
24
readonly promptArgs?: IPromptDialogArgs;
25
}
26
27
export interface IBaseDialogOptions {
28
readonly type?: Severity | DialogType;
29
30
readonly title?: string;
31
readonly message: string;
32
readonly detail?: string;
33
34
readonly checkbox?: ICheckbox;
35
36
/**
37
* Allows to enforce use of custom dialog even in native environments.
38
*/
39
readonly custom?: boolean | ICustomDialogOptions;
40
}
41
42
export interface IConfirmDialogArgs {
43
readonly confirmation: IConfirmation;
44
}
45
46
export interface IConfirmation extends IBaseDialogOptions {
47
48
/**
49
* If not provided, defaults to `Yes`.
50
*/
51
readonly primaryButton?: string;
52
53
/**
54
* If not provided, defaults to `Cancel`.
55
*/
56
readonly cancelButton?: string;
57
}
58
59
export interface IConfirmationResult extends ICheckboxResult {
60
61
/**
62
* Will be true if the dialog was confirmed with the primary button pressed.
63
*/
64
readonly confirmed: boolean;
65
}
66
67
export interface IInputDialogArgs {
68
readonly input: IInput;
69
}
70
71
export interface IInput extends IConfirmation {
72
readonly inputs: IInputElement[];
73
74
/**
75
* If not provided, defaults to `Ok`.
76
*/
77
readonly primaryButton?: string;
78
}
79
80
export interface IInputElement {
81
readonly type?: 'text' | 'password';
82
readonly value?: string;
83
readonly placeholder?: string;
84
}
85
86
export interface IInputResult extends IConfirmationResult {
87
88
/**
89
* Values for the input fields as provided by the user or `undefined` if none.
90
*/
91
readonly values?: string[];
92
}
93
94
export interface IPromptDialogArgs {
95
readonly prompt: IPrompt<unknown>;
96
}
97
98
export interface IPromptBaseButton<T> {
99
100
/**
101
* @returns the result of the prompt button will be returned
102
* as result from the `prompt()` call.
103
*/
104
run(checkbox: ICheckboxResult): T | Promise<T>;
105
}
106
107
export interface IPromptButton<T> extends IPromptBaseButton<T> {
108
readonly label: string;
109
}
110
111
export interface IPromptCancelButton<T> extends IPromptBaseButton<T> {
112
113
/**
114
* The cancel button to show in the prompt. Defaults to
115
* `Cancel` if not provided.
116
*/
117
readonly label?: string;
118
}
119
120
export interface IPrompt<T> extends IBaseDialogOptions {
121
122
/**
123
* The buttons to show in the prompt. Defaults to `OK`
124
* if no buttons or cancel button is provided.
125
*/
126
readonly buttons?: IPromptButton<T>[];
127
128
/**
129
* The cancel button to show in the prompt. Defaults to
130
* `Cancel` if set to `true`.
131
*/
132
readonly cancelButton?: IPromptCancelButton<T> | true | string;
133
}
134
135
export interface IPromptWithCustomCancel<T> extends IPrompt<T> {
136
readonly cancelButton: IPromptCancelButton<T>;
137
}
138
139
export interface IPromptWithDefaultCancel<T> extends IPrompt<T> {
140
readonly cancelButton: true | string;
141
}
142
143
export interface IPromptResult<T> extends ICheckboxResult {
144
145
/**
146
* The result of the `IPromptButton` that was pressed or `undefined` if none.
147
*/
148
readonly result?: T;
149
}
150
151
export interface IPromptResultWithCancel<T> extends IPromptResult<T> {
152
readonly result: T;
153
}
154
155
export interface IAsyncPromptResult<T> extends ICheckboxResult {
156
157
/**
158
* The result of the `IPromptButton` that was pressed or `undefined` if none.
159
*/
160
readonly result?: Promise<T>;
161
}
162
163
export interface IAsyncPromptResultWithCancel<T> extends IAsyncPromptResult<T> {
164
readonly result: Promise<T>;
165
}
166
167
export type IDialogResult = IConfirmationResult | IInputResult | IAsyncPromptResult<unknown>;
168
169
export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';
170
171
export interface ICheckbox {
172
readonly label: string;
173
readonly checked?: boolean;
174
}
175
176
export interface ICheckboxResult {
177
178
/**
179
* This will only be defined if the confirmation was created
180
* with the checkbox option defined.
181
*/
182
readonly checkboxChecked?: boolean;
183
}
184
185
export interface IPickAndOpenOptions {
186
readonly forceNewWindow?: boolean;
187
defaultUri?: URI;
188
readonly telemetryExtraData?: ITelemetryData;
189
availableFileSystems?: string[];
190
remoteAuthority?: string | null;
191
}
192
193
export interface FileFilter {
194
readonly extensions: string[];
195
readonly name: string;
196
}
197
198
export interface ISaveDialogOptions {
199
200
/**
201
* A human-readable string for the dialog title
202
*/
203
title?: string;
204
205
/**
206
* The resource the dialog shows when opened.
207
*/
208
defaultUri?: URI;
209
210
/**
211
* A set of file filters that are used by the dialog. Each entry is a human readable label,
212
* like "TypeScript", and an array of extensions.
213
*/
214
filters?: FileFilter[];
215
216
/**
217
* A human-readable string for the ok button
218
*/
219
readonly saveLabel?: { readonly withMnemonic: string; readonly withoutMnemonic: string } | string;
220
221
/**
222
* Specifies a list of schemas for the file systems the user can save to. If not specified, uses the schema of the defaultURI or, if also not specified,
223
* the schema of the current window.
224
*/
225
availableFileSystems?: readonly string[];
226
}
227
228
export interface IOpenDialogOptions {
229
230
/**
231
* A human-readable string for the dialog title
232
*/
233
readonly title?: string;
234
235
/**
236
* The resource the dialog shows when opened.
237
*/
238
defaultUri?: URI;
239
240
/**
241
* A human-readable string for the open button.
242
*/
243
readonly openLabel?: { readonly withMnemonic: string; readonly withoutMnemonic: string } | string;
244
245
/**
246
* Allow to select files, defaults to `true`.
247
*/
248
canSelectFiles?: boolean;
249
250
/**
251
* Allow to select folders, defaults to `false`.
252
*/
253
canSelectFolders?: boolean;
254
255
/**
256
* Allow to select many files or folders.
257
*/
258
readonly canSelectMany?: boolean;
259
260
/**
261
* A set of file filters that are used by the dialog. Each entry is a human readable label,
262
* like "TypeScript", and an array of extensions.
263
*/
264
filters?: FileFilter[];
265
266
/**
267
* Specifies a list of schemas for the file systems the user can load from. If not specified, uses the schema of the defaultURI or, if also not available,
268
* the schema of the current window.
269
*/
270
availableFileSystems?: readonly string[];
271
}
272
273
export const IDialogService = createDecorator<IDialogService>('dialogService');
274
275
export interface ICustomDialogOptions {
276
readonly buttonDetails?: string[];
277
readonly markdownDetails?: ICustomDialogMarkdown[];
278
readonly classes?: string[];
279
readonly icon?: ThemeIcon;
280
readonly disableCloseAction?: boolean;
281
}
282
283
export interface ICustomDialogMarkdown {
284
readonly markdown: IMarkdownString;
285
readonly classes?: string[];
286
/** Custom link handler for markdown content, see {@link IContentActionHandler}. Defaults to {@link openLinkFromMarkdown}. */
287
actionHandler?(link: string): Promise<boolean>;
288
}
289
290
/**
291
* A handler to bring up modal dialogs.
292
*/
293
export interface IDialogHandler {
294
295
/**
296
* Ask the user for confirmation with a modal dialog.
297
*/
298
confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
299
300
/**
301
* Prompt the user with a modal dialog.
302
*/
303
prompt<T>(prompt: IPrompt<T>): Promise<IAsyncPromptResult<T>>;
304
305
/**
306
* Present a modal dialog to the user asking for input.
307
*/
308
input(input: IInput): Promise<IInputResult>;
309
310
/**
311
* Present the about dialog to the user.
312
*/
313
about(title: string, details: string, detailsToCopy: string): Promise<void>;
314
}
315
316
enum DialogKind {
317
Confirmation = 1,
318
Prompt,
319
Input
320
}
321
322
export abstract class AbstractDialogHandler implements IDialogHandler {
323
324
protected getConfirmationButtons(dialog: IConfirmation): string[] {
325
return this.getButtons(dialog, DialogKind.Confirmation);
326
}
327
328
protected getPromptButtons(dialog: IPrompt<unknown>): string[] {
329
return this.getButtons(dialog, DialogKind.Prompt);
330
}
331
332
protected getInputButtons(dialog: IInput): string[] {
333
return this.getButtons(dialog, DialogKind.Input);
334
}
335
336
private getButtons(dialog: IConfirmation, kind: DialogKind.Confirmation): string[];
337
private getButtons(dialog: IPrompt<unknown>, kind: DialogKind.Prompt): string[];
338
private getButtons(dialog: IInput, kind: DialogKind.Input): string[];
339
private getButtons(dialog: IConfirmation | IInput | IPrompt<unknown>, kind: DialogKind): string[] {
340
341
// We put buttons in the order of "default" button first and "cancel"
342
// button last. There maybe later processing when presenting the buttons
343
// based on OS standards.
344
345
const buttons: string[] = [];
346
347
switch (kind) {
348
case DialogKind.Confirmation: {
349
const confirmationDialog = dialog as IConfirmation;
350
351
if (confirmationDialog.primaryButton) {
352
buttons.push(confirmationDialog.primaryButton);
353
} else {
354
buttons.push(localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));
355
}
356
357
if (confirmationDialog.cancelButton) {
358
buttons.push(confirmationDialog.cancelButton);
359
} else {
360
buttons.push(localize('cancelButton', "Cancel"));
361
}
362
363
break;
364
}
365
case DialogKind.Prompt: {
366
const promptDialog = dialog as IPrompt<unknown>;
367
368
if (Array.isArray(promptDialog.buttons) && promptDialog.buttons.length > 0) {
369
buttons.push(...promptDialog.buttons.map(button => button.label));
370
}
371
372
if (promptDialog.cancelButton) {
373
if (promptDialog.cancelButton === true) {
374
buttons.push(localize('cancelButton', "Cancel"));
375
} else if (typeof promptDialog.cancelButton === 'string') {
376
buttons.push(promptDialog.cancelButton);
377
} else {
378
if (promptDialog.cancelButton.label) {
379
buttons.push(promptDialog.cancelButton.label);
380
} else {
381
buttons.push(localize('cancelButton', "Cancel"));
382
}
383
}
384
}
385
386
if (buttons.length === 0) {
387
buttons.push(localize({ key: 'okButton', comment: ['&& denotes a mnemonic'] }, "&&OK"));
388
}
389
390
break;
391
}
392
case DialogKind.Input: {
393
const inputDialog = dialog as IInput;
394
395
if (inputDialog.primaryButton) {
396
buttons.push(inputDialog.primaryButton);
397
} else {
398
buttons.push(localize({ key: 'okButton', comment: ['&& denotes a mnemonic'] }, "&&OK"));
399
}
400
401
if (inputDialog.cancelButton) {
402
buttons.push(inputDialog.cancelButton);
403
} else {
404
buttons.push(localize('cancelButton', "Cancel"));
405
}
406
407
break;
408
}
409
}
410
411
return buttons;
412
}
413
414
protected getDialogType(type: Severity | DialogType | undefined): DialogType | undefined {
415
if (typeof type === 'string') {
416
return type;
417
}
418
419
if (typeof type === 'number') {
420
return (type === Severity.Info) ? 'info' : (type === Severity.Error) ? 'error' : (type === Severity.Warning) ? 'warning' : 'none';
421
}
422
423
return undefined;
424
}
425
426
protected getPromptResult<T>(prompt: IPrompt<T>, buttonIndex: number, checkboxChecked: boolean | undefined): IAsyncPromptResult<T> {
427
const promptButtons: IPromptBaseButton<T>[] = [...(prompt.buttons ?? [])];
428
if (prompt.cancelButton && typeof prompt.cancelButton !== 'string' && typeof prompt.cancelButton !== 'boolean') {
429
promptButtons.push(prompt.cancelButton);
430
}
431
432
let result = promptButtons[buttonIndex]?.run({ checkboxChecked });
433
if (!(result instanceof Promise)) {
434
result = Promise.resolve(result);
435
}
436
437
return { result, checkboxChecked };
438
}
439
440
abstract confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
441
abstract input(input: IInput): Promise<IInputResult>;
442
abstract prompt<T>(prompt: IPrompt<T>): Promise<IAsyncPromptResult<T>>;
443
abstract about(title: string, details: string, detailsToCopy: string): Promise<void>;
444
}
445
446
/**
447
* A service to bring up modal dialogs.
448
*
449
* Note: use the `INotificationService.prompt()` method for a non-modal way to ask
450
* the user for input.
451
*/
452
export interface IDialogService {
453
454
readonly _serviceBrand: undefined;
455
456
/**
457
* An event that fires when a dialog is about to show.
458
*/
459
onWillShowDialog: Event<void>;
460
461
/**
462
* An event that fires when a dialog did show (closed).
463
*/
464
onDidShowDialog: Event<void>;
465
466
/**
467
* Ask the user for confirmation with a modal dialog.
468
*/
469
confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
470
471
/**
472
* Prompt the user with a modal dialog. Provides a bit
473
* more control over the dialog compared to the simpler
474
* `confirm` method. Specifically, allows to show more
475
* than 2 buttons and makes it easier to just show a
476
* message to the user.
477
*
478
* @returns a promise that resolves to the `T` result
479
* from the provided `IPromptButton<T>` or `undefined`.
480
*/
481
prompt<T>(prompt: IPromptWithCustomCancel<T>): Promise<IPromptResultWithCancel<T>>;
482
prompt<T>(prompt: IPromptWithDefaultCancel<T>): Promise<IPromptResult<T>>;
483
prompt<T>(prompt: IPrompt<T>): Promise<IPromptResult<T>>;
484
485
/**
486
* Present a modal dialog to the user asking for input.
487
*/
488
input(input: IInput): Promise<IInputResult>;
489
490
/**
491
* Show a modal info dialog.
492
*/
493
info(message: string, detail?: string): Promise<void>;
494
495
/**
496
* Show a modal warning dialog.
497
*/
498
warn(message: string, detail?: string): Promise<void>;
499
500
/**
501
* Show a modal error dialog.
502
*/
503
error(message: string, detail?: string): Promise<void>;
504
505
/**
506
* Present the about dialog to the user.
507
*/
508
about(): Promise<void>;
509
}
510
511
export const IFileDialogService = createDecorator<IFileDialogService>('fileDialogService');
512
513
/**
514
* A service to bring up file dialogs.
515
*/
516
export interface IFileDialogService {
517
518
readonly _serviceBrand: undefined;
519
520
/**
521
* The default path for a new file based on previously used files.
522
* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.
523
* Falls back to user home in the absence of enough information to find a better URI.
524
*/
525
defaultFilePath(schemeFilter?: string): Promise<URI>;
526
527
/**
528
* The default path for a new folder based on previously used folders.
529
* @param schemeFilter The scheme of the folder path. If no filter given, the scheme of the current window is used.
530
* Falls back to user home in the absence of enough information to find a better URI.
531
*/
532
defaultFolderPath(schemeFilter?: string): Promise<URI>;
533
534
/**
535
* The default path for a new workspace based on previously used workspaces.
536
* @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used.
537
* Falls back to user home in the absence of enough information to find a better URI.
538
*/
539
defaultWorkspacePath(schemeFilter?: string): Promise<URI>;
540
541
/**
542
* Shows a file-folder selection dialog and opens the selected entry.
543
*/
544
pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
545
546
/**
547
* Shows a file selection dialog and opens the selected entry.
548
*/
549
pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;
550
551
/**
552
* Shows a folder selection dialog and opens the selected entry.
553
*/
554
pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
555
556
/**
557
* Shows a workspace selection dialog and opens the selected entry.
558
*/
559
pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;
560
561
/**
562
* Shows a save file dialog and save the file at the chosen file URI.
563
*/
564
pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;
565
566
/**
567
* The preferred folder path to open the dialog at.
568
* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.
569
* Falls back to user home in the absence of a setting.
570
*/
571
preferredHome(schemeFilter?: string): Promise<URI>;
572
573
/**
574
* Shows a save file dialog and returns the chosen file URI.
575
*/
576
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
577
578
/**
579
* Shows a confirm dialog for saving 1-N files.
580
*/
581
showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult>;
582
583
/**
584
* Shows a open file dialog and returns the chosen file URI.
585
*/
586
showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
587
}
588
589
export const enum ConfirmResult {
590
SAVE,
591
DONT_SAVE,
592
CANCEL
593
}
594
595
const MAX_CONFIRM_FILES = 10;
596
export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI)[]): string {
597
const message: string[] = [];
598
message.push(...fileNamesOrResources.slice(0, MAX_CONFIRM_FILES).map(fileNameOrResource => typeof fileNameOrResource === 'string' ? fileNameOrResource : basename(fileNameOrResource)));
599
600
if (fileNamesOrResources.length > MAX_CONFIRM_FILES) {
601
if (fileNamesOrResources.length - MAX_CONFIRM_FILES === 1) {
602
message.push(localize('moreFile', "...1 additional file not shown"));
603
} else {
604
message.push(localize('moreFiles', "...{0} additional files not shown", fileNamesOrResources.length - MAX_CONFIRM_FILES));
605
}
606
}
607
608
message.push('');
609
return message.join('\n');
610
}
611
612
export interface INativeOpenDialogOptions {
613
readonly forceNewWindow?: boolean;
614
615
readonly defaultPath?: string;
616
617
readonly telemetryEventName?: string;
618
readonly telemetryExtraData?: ITelemetryData;
619
}
620
621
export interface IMassagedMessageBoxOptions {
622
623
/**
624
* OS massaged message box options.
625
*/
626
readonly options: MessageBoxOptions;
627
628
/**
629
* Since the massaged result of the message box options potentially
630
* changes the order of buttons, we have to keep a map of these
631
* changes so that we can still return the correct index to the caller.
632
*/
633
readonly buttonIndeces: number[];
634
}
635
636
/**
637
* A utility method to ensure the options for the message box dialog
638
* are using properties that are consistent across all platforms and
639
* specific to the platform where necessary.
640
*/
641
export function massageMessageBoxOptions(options: MessageBoxOptions, productService: IProductService): IMassagedMessageBoxOptions {
642
const massagedOptions = deepClone(options);
643
644
let buttons = (massagedOptions.buttons ?? []).map(button => mnemonicButtonLabel(button).withMnemonic);
645
let buttonIndeces = (options.buttons || []).map((button, index) => index);
646
647
let defaultId = 0; // by default the first button is default button
648
let cancelId = massagedOptions.cancelId ?? buttons.length - 1; // by default the last button is cancel button
649
650
// Apply HIG per OS when more than one button is used
651
if (buttons.length > 1) {
652
const cancelButton = typeof cancelId === 'number' ? buttons[cancelId] : undefined;
653
654
if (isLinux || isMacintosh) {
655
656
// Linux: the GNOME HIG (https://developer.gnome.org/hig/patterns/feedback/dialogs.html?highlight=dialog)
657
// recommend the following:
658
// "Always ensure that the cancel button appears first, before the affirmative button. In left-to-right
659
// locales, this is on the left. This button order ensures that users become aware of, and are reminded
660
// of, the ability to cancel prior to encountering the affirmative button."
661
//
662
// Electron APIs do not reorder buttons for us, so we ensure a reverse order of buttons and a position
663
// of the cancel button (if provided) that matches the HIG
664
665
// macOS: the HIG (https://developer.apple.com/design/human-interface-guidelines/components/presentation/alerts)
666
// recommend the following:
667
// "Place buttons where people expect. In general, place the button people are most likely to choose on the trailing side in a
668
// row of buttons or at the top in a stack of buttons. Always place the default button on the trailing side of a row or at the
669
// top of a stack. Cancel buttons are typically on the leading side of a row or at the bottom of a stack."
670
//
671
// However: it seems that older macOS versions where 3 buttons were presented in a row differ from this
672
// recommendation. In fact, cancel buttons were placed to the left of the default button and secondary
673
// buttons on the far left. To support these older macOS versions we have to manually shuffle the cancel
674
// button in the same way as we do on Linux. This will not have any impact on newer macOS versions where
675
// shuffling is done for us.
676
677
if (typeof cancelButton === 'string' && buttons.length > 1 && cancelId !== 1) {
678
buttons.splice(cancelId, 1);
679
buttons.splice(1, 0, cancelButton);
680
681
const cancelButtonIndex = buttonIndeces[cancelId];
682
buttonIndeces.splice(cancelId, 1);
683
buttonIndeces.splice(1, 0, cancelButtonIndex);
684
685
cancelId = 1;
686
}
687
688
if (isLinux && buttons.length > 1) {
689
buttons = buttons.reverse();
690
buttonIndeces = buttonIndeces.reverse();
691
692
defaultId = buttons.length - 1;
693
if (typeof cancelButton === 'string') {
694
cancelId = defaultId - 1;
695
}
696
}
697
} else if (isWindows) {
698
699
// Windows: the HIG (https://learn.microsoft.com/en-us/windows/win32/uxguide/win-dialog-box)
700
// recommend the following:
701
// "One of the following sets of concise commands: Yes/No, Yes/No/Cancel, [Do it]/Cancel,
702
// [Do it]/[Don't do it], [Do it]/[Don't do it]/Cancel."
703
//
704
// Electron APIs do not reorder buttons for us, so we ensure the position of the cancel button
705
// (if provided) that matches the HIG
706
707
if (typeof cancelButton === 'string' && buttons.length > 1 && cancelId !== buttons.length - 1 /* last action */) {
708
buttons.splice(cancelId, 1);
709
buttons.push(cancelButton);
710
711
const buttonIndex = buttonIndeces[cancelId];
712
buttonIndeces.splice(cancelId, 1);
713
buttonIndeces.push(buttonIndex);
714
715
cancelId = buttons.length - 1;
716
}
717
}
718
}
719
720
massagedOptions.buttons = buttons;
721
massagedOptions.defaultId = defaultId;
722
massagedOptions.cancelId = cancelId;
723
massagedOptions.noLink = true;
724
massagedOptions.title = massagedOptions.title || productService.nameLong;
725
726
return {
727
options: massagedOptions,
728
buttonIndeces
729
};
730
}
731
732