mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 17:23:19 -05:00
Notebook Views Actions (#16207)
This adds the actions currently needed by the views
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
#insert-dialog-cell-grid .loading-spinner-container {
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
}
|
||||
#insert-dialog-cell-grid .loading-spinner {
|
||||
margin: auto;
|
||||
}
|
||||
#insert-dialog-cell-grid input[type="checkbox"] {
|
||||
display: flex;
|
||||
-webkit-appearance: none;
|
||||
outline: none !important;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./insertCellsModal';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { attachCheckboxStyler } from 'sql/platform/theme/common/styler';
|
||||
import { ServiceOptionType } from 'sql/platform/connection/common/interfaces';
|
||||
import { ServiceOption } from 'azdata';
|
||||
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
||||
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
|
||||
import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core';
|
||||
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
|
||||
import { inputBorder, inputValidationInfoBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
|
||||
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
|
||||
import { toJpeg } from 'html-to-image';
|
||||
import { truncate } from 'vs/base/common/strings';
|
||||
|
||||
type CellOption = {
|
||||
optionMetadata: ServiceOption,
|
||||
defaultValue: string,
|
||||
currentValue: boolean
|
||||
};
|
||||
|
||||
export class CellOptionsModel {
|
||||
private _optionsMap: { [name: string]: CellOption } = {};
|
||||
|
||||
constructor(
|
||||
optionsMetadata: ServiceOption[],
|
||||
private onInsert: (cell: ICellModel) => void,
|
||||
private _context: NotebookViewsExtension,
|
||||
) {
|
||||
optionsMetadata.forEach(optionMetadata => {
|
||||
let defaultValue = this.getDisplayValue(optionMetadata, optionMetadata.defaultValue);
|
||||
this._optionsMap[optionMetadata.name] = {
|
||||
optionMetadata: optionMetadata,
|
||||
defaultValue: optionMetadata.defaultValue,
|
||||
currentValue: defaultValue
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getDisplayValue(optionMetadata: ServiceOption, optionValue: string): boolean {
|
||||
let displayValue: boolean = false;
|
||||
switch (optionMetadata.valueType) {
|
||||
case ServiceOptionType.boolean:
|
||||
displayValue = DialogHelper.getBooleanValueFromStringOrBoolean(optionValue);
|
||||
break;
|
||||
}
|
||||
return displayValue;
|
||||
}
|
||||
|
||||
restoreCells(): void {
|
||||
for (let key in this._optionsMap) {
|
||||
let optionElement = this._optionsMap[key];
|
||||
if (optionElement.currentValue === true) {
|
||||
const activeView = this._context.getActiveView();
|
||||
const cellToInsert = activeView.getCell(optionElement.optionMetadata.name);
|
||||
if (cellToInsert) {
|
||||
this.onInsert(cellToInsert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setOptionValue(optionName: string, value: boolean): void {
|
||||
if (this._optionsMap[optionName] !== undefined) {
|
||||
this._optionsMap[optionName].currentValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
public getOptionValue(optionName: string): boolean | undefined {
|
||||
return this._optionsMap[optionName]?.currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
export class InsertCellsModal extends Modal {
|
||||
public viewModel: CellOptionsModel;
|
||||
|
||||
private _submitButton: Button;
|
||||
private _cancelButton: Button;
|
||||
private _optionsMap: { [name: string]: Checkbox } = {};
|
||||
private _maxTitleLength: number = 20;
|
||||
|
||||
constructor(
|
||||
private onInsert: (cell: ICellModel) => void,
|
||||
private _context: NotebookViewsExtension,
|
||||
private _containerRef: ViewContainerRef,
|
||||
private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@ILogService logService: ILogService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@ILayoutService layoutService: ILayoutService,
|
||||
@IClipboardService clipboardService: IClipboardService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
|
||||
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
|
||||
) {
|
||||
super(
|
||||
localize("insertCellsModal.title", "Insert cells"),
|
||||
'InsertCellsModal',
|
||||
telemetryService,
|
||||
layoutService,
|
||||
clipboardService,
|
||||
themeService,
|
||||
logService,
|
||||
textResourcePropertiesService,
|
||||
contextKeyService,
|
||||
{ hasErrors: true, hasSpinner: true }
|
||||
);
|
||||
|
||||
const options = this.getOptions();
|
||||
this.viewModel = new CellOptionsModel(options, this.onInsert, this._context);
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
const grid = DOM.$<HTMLDivElement>('div#insert-dialog-cell-grid');
|
||||
|
||||
grid.style.display = 'grid';
|
||||
grid.style.gridTemplateColumns = '1fr 1fr';
|
||||
grid.style.gap = '10px';
|
||||
grid.style.padding = '10px';
|
||||
grid.style.overflowY = 'auto';
|
||||
grid.style.maxHeight = 'calc(100% - 40px)';
|
||||
|
||||
const gridTitle = DOM.$<HTMLHeadElement>('h2.grid-title');
|
||||
gridTitle.title = localize("insertCellsModal.selectCells", "Select cell sources");
|
||||
|
||||
DOM.append(container, grid);
|
||||
|
||||
this.createOptions(grid)
|
||||
.catch((e) => { this.setError(localize("insertCellsModal.thumbnailError", "Error: Unable to generate thumbnails.")); });
|
||||
}
|
||||
|
||||
protected layout(height: number): void {
|
||||
// No-op for now. No need to relayout.
|
||||
}
|
||||
|
||||
private async createOptions(container: HTMLElement): Promise<void> {
|
||||
const activeView = this._context.getActiveView();
|
||||
const cellsAvailableToInsert = activeView.hiddenCells;
|
||||
|
||||
cellsAvailableToInsert.forEach(async (cell) => {
|
||||
const optionWidget = this.createCheckBoxHelper(
|
||||
container,
|
||||
'<div class="loading-spinner-container"><div class="loading-spinner codicon in-progress"></div></div>',
|
||||
false,
|
||||
() => this.onOptionChecked(cell.cellGuid)
|
||||
);
|
||||
|
||||
const img = await this.generateScreenshot(cell);
|
||||
const wrapper = DOM.$<HTMLDivElement>('div.thumnail-wrapper');
|
||||
const thumbnail = DOM.$<HTMLImageElement>('img.thumbnail');
|
||||
|
||||
thumbnail.src = img;
|
||||
thumbnail.style.maxWidth = '100%';
|
||||
DOM.append(wrapper, thumbnail);
|
||||
optionWidget.label = wrapper.outerHTML;
|
||||
|
||||
this._optionsMap[cell.cellGuid] = optionWidget;
|
||||
});
|
||||
}
|
||||
|
||||
private createCheckBoxHelper(container: HTMLElement, label: string, isChecked: boolean, onCheck: (viaKeyboard: boolean) => void): Checkbox {
|
||||
const checkbox = new Checkbox(DOM.append(container, DOM.$('.dialog-input-section')), {
|
||||
label: label,
|
||||
checked: isChecked,
|
||||
onChange: onCheck,
|
||||
ariaLabel: label
|
||||
});
|
||||
this._register(attachCheckboxStyler(checkbox, this._themeService));
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
public onOptionChecked(optionName: string) {
|
||||
this.viewModel.setOptionValue(optionName, (<Checkbox>this._optionsMap[optionName]).checked);
|
||||
this.validate();
|
||||
}
|
||||
|
||||
private getOptions(): ServiceOption[] {
|
||||
const activeView = this._context.getActiveView();
|
||||
const cellsAvailableToInsert = activeView.hiddenCells;
|
||||
return cellsAvailableToInsert.map((cell) => ({
|
||||
name: cell.cellGuid,
|
||||
displayName: truncate(cell.renderedOutputTextContent[0] ?? '', this._maxTitleLength) || localize("insertCellsModal.untitled", "Untitled Cell : {0}", cell.cellGuid),
|
||||
description: '',
|
||||
groupName: undefined,
|
||||
valueType: ServiceOptionType.boolean,
|
||||
defaultValue: '',
|
||||
objectType: undefined,
|
||||
categoryValues: [],
|
||||
isRequired: false,
|
||||
isArray: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
public override render() {
|
||||
super.render();
|
||||
|
||||
this._submitButton = this.addFooterButton(localize('insertCellsModal.Insert', "Insert"), () => this.onSubmitHandler());
|
||||
this._cancelButton = this.addFooterButton(localize('insertCellsModal.Cancel', "Cancel"), () => this.onCancelHandler(), 'right', true);
|
||||
|
||||
this._register(attachButtonStyler(this._submitButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._cancelButton, this._themeService));
|
||||
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
this.validate();
|
||||
}
|
||||
|
||||
private validate() {
|
||||
if (Object.keys(this._optionsMap).length) {
|
||||
this._submitButton.enabled = true;
|
||||
} else {
|
||||
this._submitButton.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private onSubmitHandler() {
|
||||
this.viewModel.restoreCells();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private onCancelHandler() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
public async open(): Promise<void> {
|
||||
this.show();
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
for (let key in this._optionsMap) {
|
||||
let widget = this._optionsMap[key];
|
||||
widget.dispose();
|
||||
delete this._optionsMap[key];
|
||||
}
|
||||
}
|
||||
|
||||
public async generateScreenshot(cell: ICellModel, screenshotWidth: number = 300, screenshowHeight: number = 300, backgroundColor: string = '#ffffff'): Promise<string> {
|
||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(TextCellComponent);
|
||||
let component = this._containerRef.createComponent(componentFactory);
|
||||
|
||||
component.instance.model = this._context.notebook as NotebookModel;
|
||||
component.instance.cellModel = cell;
|
||||
|
||||
component.instance.handleContentChanged();
|
||||
|
||||
const element: HTMLElement = component.instance.outputRef.nativeElement;
|
||||
|
||||
const scale = element.clientWidth / screenshotWidth;
|
||||
const canvasWidth = element.clientWidth / scale;
|
||||
const canvasHeight = element.clientHeight / scale;
|
||||
|
||||
return toJpeg(component.instance.outputRef.nativeElement, { quality: .6, canvasWidth, canvasHeight, backgroundColor });
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
|
||||
const inputBorderColor = theme.getColor(inputBorder);
|
||||
if (inputBorderColor) {
|
||||
collector.addRule(`
|
||||
#insert-dialog-cell-grid input[type="checkbox"] + label {
|
||||
border: 2px solid;
|
||||
border-color: ${inputBorderColor.toString()};
|
||||
display: flex;
|
||||
height: 125px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const inputActiveOptionBorderColor = theme.getColor(inputValidationInfoBorder);
|
||||
if (inputActiveOptionBorderColor) {
|
||||
collector.addRule(`
|
||||
#insert-dialog-cell-grid input[type="checkbox"]:checked + label {
|
||||
border-color: ${inputActiveOptionBorderColor.toString()};
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Component, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Component, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
|
||||
import { ICellModel, INotebookModel, ISingleNotebookEditOperation } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component';
|
||||
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
|
||||
@@ -26,8 +26,11 @@ import { CellType, CellTypes } from 'sql/workbench/services/notebook/common/cont
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
|
||||
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
|
||||
import { INotebookView, INotebookViewMetadata } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
|
||||
import { NotebookViewsGridComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { DeleteViewAction, InsertCellAction, ViewSettingsAction } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsActions';
|
||||
import { RunAllCellsAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
|
||||
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
|
||||
export const NOTEBOOKVIEWS_SELECTOR: string = 'notebook-view-component';
|
||||
@@ -41,6 +44,7 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
|
||||
@Input() model: NotebookModel;
|
||||
@Input() activeView: INotebookView;
|
||||
@Input() views: NotebookViewsExtension;
|
||||
@Input() notebookMeta: INotebookViewMetadata;
|
||||
|
||||
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
|
||||
@ViewChild('viewsToolbar', { read: ElementRef }) private _viewsToolbar: ElementRef;
|
||||
@@ -51,18 +55,21 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
|
||||
protected _actionBar: Taskbar;
|
||||
public previewFeaturesEnabled: boolean = false;
|
||||
private _modelReadyDeferred = new Deferred<NotebookModel>();
|
||||
|
||||
private _runAllCellsAction: RunAllCellsAction;
|
||||
private _scrollTop: number;
|
||||
|
||||
constructor(
|
||||
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IKeybindingService) private _keybindingService: IKeybindingService,
|
||||
@Inject(INotificationService) private _notificationService: INotificationService,
|
||||
@Inject(INotebookService) private _notebookService: INotebookService,
|
||||
@Inject(IConnectionManagementService) private _connectionManagementService: IConnectionManagementService,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
|
||||
@Inject(IEditorService) private _editorService: IEditorService
|
||||
@Inject(IEditorService) private _editorService: IEditorService,
|
||||
@Inject(ViewContainerRef) private _containerRef: ViewContainerRef,
|
||||
@Inject(ComponentFactoryResolver) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
) {
|
||||
super();
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
@@ -235,9 +242,25 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
|
||||
titleElement.style.marginRight = '25px';
|
||||
titleElement.style.minHeight = '25px';
|
||||
|
||||
let insertCellsAction = this._instantiationService.createInstance(InsertCellAction, this.insertCell.bind(this), this.views, this._containerRef, this._componentFactoryResolver);
|
||||
|
||||
this._runAllCellsAction = this._instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAllPreview', "Run all"), 'notebook-button masked-pseudo start-outline');
|
||||
|
||||
let spacerElement = document.createElement('li');
|
||||
spacerElement.style.marginLeft = 'auto';
|
||||
|
||||
let viewOptions = this._instantiationService.createInstance(ViewSettingsAction, this.views);
|
||||
|
||||
let deleteView = this._instantiationService.createInstance(DeleteViewAction, this.views);
|
||||
|
||||
this._actionBar.setContent([
|
||||
{ element: titleElement },
|
||||
{ element: Taskbar.createTaskbarSeparator() },
|
||||
{ action: insertCellsAction },
|
||||
{ action: this._runAllCellsAction },
|
||||
{ element: spacerElement },
|
||||
{ action: viewOptions },
|
||||
{ action: deleteView }
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ViewOptionsModal } from 'sql/workbench/contrib/notebook/browser/notebookViews/viewOptionsModal';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { localize } from 'vs/nls';
|
||||
import { InsertCellsModal } from 'sql/workbench/contrib/notebook/browser/notebookViews/insertCellsModal';
|
||||
import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { CellExecutionState, ICellModel, ViewMode } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { CellActionBase, CellContext, IMultiStateData, MultiStateAction } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Separator } from 'sql/base/browser/ui/separator/separator';
|
||||
import { ToggleMoreActions } from 'sql/workbench/contrib/notebook/browser/cellToolbarActions';
|
||||
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
|
||||
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
|
||||
|
||||
export class ViewSettingsAction extends Action {
|
||||
private static readonly ID = 'notebookView.viewSettings';
|
||||
private static readonly LABEL = undefined;
|
||||
private static readonly ICON = 'notebook-button settings masked-icon';
|
||||
|
||||
constructor(
|
||||
private _context: NotebookViewsExtension,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super(ViewSettingsAction.ID, ViewSettingsAction.LABEL, ViewSettingsAction.ICON);
|
||||
}
|
||||
|
||||
override async run(): Promise<void> {
|
||||
const optionsModal = this._instantiationService.createInstance(ViewOptionsModal, this._context.getActiveView());
|
||||
optionsModal.render();
|
||||
optionsModal.open();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteViewAction extends Action {
|
||||
private static readonly ID = 'notebookView.deleteView';
|
||||
private static readonly LABEL = undefined;
|
||||
private static readonly ICON = 'notebook-button delete masked-icon';
|
||||
|
||||
constructor(
|
||||
private _notebookViews: NotebookViewsExtension,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@INotificationService private readonly notificationService: INotificationService
|
||||
) {
|
||||
super(DeleteViewAction.ID, DeleteViewAction.LABEL, DeleteViewAction.ICON);
|
||||
}
|
||||
|
||||
override async run(): Promise<void> {
|
||||
const activeView = this._notebookViews.getActiveView();
|
||||
if (activeView) {
|
||||
const confirmDelete = await this.confirmDelete(activeView);
|
||||
if (confirmDelete) {
|
||||
this._notebookViews.removeView(activeView.guid);
|
||||
this._notebookViews.notebook.viewMode = ViewMode.Notebook;
|
||||
}
|
||||
} else {
|
||||
this.notificationService.error(localize('viewsUnableToRemove', "Unable to remove view"));
|
||||
}
|
||||
}
|
||||
|
||||
private async confirmDelete(view: INotebookView): Promise<boolean> {
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('confirmDelete', "Are you sure you want to delete view \"{0}\"?", view.name),
|
||||
primaryButton: localize('delete', "&&Delete"),
|
||||
type: 'question'
|
||||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class InsertCellAction extends Action {
|
||||
private static readonly ID = 'notebookView.insertCell';
|
||||
private static readonly LABEL = localize('insertCells', "Insert Cells");
|
||||
private static readonly ICON = 'notebook-button masked-pseudo add-new';
|
||||
|
||||
constructor(
|
||||
private onInsert: (cell: ICellModel) => void,
|
||||
private _context: NotebookViewsExtension,
|
||||
private _containerRef: ViewContainerRef,
|
||||
private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super(InsertCellAction.ID, InsertCellAction.LABEL, InsertCellAction.ICON);
|
||||
}
|
||||
|
||||
override async run(): Promise<void> {
|
||||
const optionsModal = this._instantiationService.createInstance(InsertCellsModal, this.onInsert, this._context, this._containerRef, this._componentFactoryResolver);
|
||||
optionsModal.render();
|
||||
optionsModal.open();
|
||||
}
|
||||
}
|
||||
|
||||
export class RunCellAction extends MultiStateAction<CellExecutionState> {
|
||||
public static ID = 'notebookView.runCell';
|
||||
public static LABEL = localize('runCell', "Run cell");
|
||||
private _executionChangedDisposable: IDisposable;
|
||||
private _context: CellContext;
|
||||
constructor(context: CellContext, @INotificationService private notificationService: INotificationService,
|
||||
@IConnectionManagementService private connectionManagementService: IConnectionManagementService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@ILogService logService: ILogService
|
||||
) {
|
||||
super(RunCellAction.ID, new IMultiStateData<CellExecutionState>([
|
||||
{ key: CellExecutionState.Hidden, value: { label: '', className: '', tooltip: '', hideIcon: true } },
|
||||
{ key: CellExecutionState.Stopped, value: { label: '', className: 'action-label notebook-button masked-pseudo start-outline masked-icon', tooltip: localize('runCell', "Run cell"), commandId: 'notebook.command.runactivecell' } },
|
||||
{ key: CellExecutionState.Running, value: { label: '', className: 'action-label codicon notebook-button toolbarIconStop', tooltip: localize('stopCell', "Cancel execution") } },
|
||||
{ key: CellExecutionState.Error, value: { label: '', className: 'toolbarIconRunError', tooltip: localize('errorRunCell', "Error on last run. Click to run again") } },
|
||||
], CellExecutionState.Hidden), keybindingService, logService);
|
||||
this.ensureContextIsUpdated(context);
|
||||
}
|
||||
|
||||
public override run(): Promise<void> {
|
||||
return this.doRun();
|
||||
}
|
||||
|
||||
public async doRun(): Promise<void> {
|
||||
if (!this._context) {
|
||||
// TODO should we error?
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this._context.cell.runCell(this.notificationService, this.connectionManagementService);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
this.notificationService.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureContextIsUpdated(context: CellContext) {
|
||||
if (context && context !== this._context) {
|
||||
if (this._executionChangedDisposable) {
|
||||
this._executionChangedDisposable.dispose();
|
||||
}
|
||||
this._context = context;
|
||||
this.updateStateAndExecutionCount(context.cell.executionState);
|
||||
this._executionChangedDisposable = this._context.cell.onExecutionStateChange((state) => {
|
||||
this.updateStateAndExecutionCount(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateStateAndExecutionCount(state: CellExecutionState) {
|
||||
let label = '';
|
||||
let className = '';
|
||||
if (!types.isUndefinedOrNull(this._context.cell.executionCount)) {
|
||||
label = `[${this._context.cell.executionCount}]`;
|
||||
// Heuristic to try and align correctly independent of execution count length. Moving left margin
|
||||
// back by a few px seems to make things "work" OK, but isn't a super clean solution
|
||||
if (label.length === 4) {
|
||||
className = 'execCountTen';
|
||||
} else if (label.length > 4) {
|
||||
className = 'execCountHundred';
|
||||
}
|
||||
}
|
||||
this.states.updateStateData(CellExecutionState.Hidden, (data) => {
|
||||
data.label = label;
|
||||
data.className = className;
|
||||
});
|
||||
this.updateState(state);
|
||||
}
|
||||
}
|
||||
|
||||
export class HideCellAction extends Action {
|
||||
private static readonly ID = 'notebookView.hideCell';
|
||||
private static readonly LABEL = undefined;
|
||||
private static readonly ICON = 'notebook-button delete masked-icon';
|
||||
|
||||
constructor(
|
||||
private hideFn: () => void,
|
||||
private context: any
|
||||
) {
|
||||
super(HideCellAction.ID, HideCellAction.LABEL, HideCellAction.ICON);
|
||||
}
|
||||
|
||||
override async run(): Promise<void> {
|
||||
this.hideFn.apply(this.context);
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewCellInNotebook extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context?.model?.updateActiveCell(context.cell);
|
||||
context.model.viewMode = ViewMode.Notebook;
|
||||
} catch (error) {
|
||||
let message = localize('unableToNavigateToCell', "Unable to navigate to notebook cell.");
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewCellToggleMoreActions {
|
||||
private _actions: (Action | CellActionBase)[] = [];
|
||||
private _moreActions: ActionBar;
|
||||
private _moreActionsElement: HTMLElement;
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) {
|
||||
this._actions.push(
|
||||
instantiationService.createInstance(ViewCellInNotebook, 'viewCellInNotebook', localize('viewCellInNotebook', "View Cell In Notebook")),
|
||||
);
|
||||
}
|
||||
|
||||
public onInit(elementRef: HTMLElement, context: CellContext) {
|
||||
this._moreActionsElement = elementRef;
|
||||
this._moreActionsElement.setAttribute('aria-haspopup', 'menu');
|
||||
if (this._moreActionsElement.childNodes.length > 0) {
|
||||
this._moreActionsElement.removeChild(this._moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(this._moreActionsElement, { orientation: ActionsOrientation.VERTICAL, ariaLabel: localize('moreActionsLabel', "More") });
|
||||
this._moreActions.context = { target: this._moreActionsElement };
|
||||
let validActions = this._actions.filter(a => a instanceof Separator || a instanceof CellActionBase && a.canRun(context));
|
||||
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreActions, validActions, context), { icon: true, label: false });
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,27 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./cellToolbar';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Component, OnInit, Input, ViewChild, TemplateRef, ElementRef, Inject, Output, EventEmitter, ChangeDetectorRef, forwardRef } from '@angular/core';
|
||||
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
|
||||
import { DEFAULT_VIEW_CARD_HEIGHT, DEFAULT_VIEW_CARD_WIDTH } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel';
|
||||
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
|
||||
import { CellChangeEventType, INotebookView, INotebookViewCellMetadata } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
|
||||
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
|
||||
import { RunCellAction, HideCellAction, ViewCellToggleMoreActions } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsActions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CellTypes } from 'sql/workbench/services/notebook/common/contracts';
|
||||
|
||||
@Component({
|
||||
selector: 'view-card-component',
|
||||
templateUrl: decodeURI(require.toUrl('./notebookViewsCard.component.html'))
|
||||
})
|
||||
export class NotebookViewsCardComponent implements OnInit {
|
||||
public _cellToggleMoreActions: ViewCellToggleMoreActions;
|
||||
|
||||
private _actionbar: Taskbar;
|
||||
private _metadata: INotebookViewCellMetadata;
|
||||
private _activeView: INotebookView;
|
||||
|
||||
@@ -26,12 +35,16 @@ export class NotebookViewsCardComponent implements OnInit {
|
||||
|
||||
@ViewChild('templateRef') templateRef: TemplateRef<any>;
|
||||
@ViewChild('item', { read: ElementRef }) private _item: ElementRef;
|
||||
@ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService
|
||||
) { }
|
||||
|
||||
ngOnInit() { }
|
||||
ngOnInit() {
|
||||
this.initActionBar();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.views) {
|
||||
@@ -51,6 +64,31 @@ export class NotebookViewsCardComponent implements OnInit {
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
initActionBar() {
|
||||
if (this._actionbarRef) {
|
||||
let taskbarContent: ITaskbarContent[] = [];
|
||||
let context = new CellContext(this.model, this.cell);
|
||||
|
||||
this._actionbar = new Taskbar(this._actionbarRef.nativeElement);
|
||||
this._actionbar.context = { target: this._actionbarRef.nativeElement };
|
||||
|
||||
if (this.cell.cellType === CellTypes.Code) {
|
||||
let runCellAction = this._instantiationService.createInstance(RunCellAction, context);
|
||||
taskbarContent.push({ action: runCellAction });
|
||||
}
|
||||
|
||||
let hideButton = new HideCellAction(this.hide, this);
|
||||
taskbarContent.push({ action: hideButton });
|
||||
|
||||
let moreActionsContainer = DOM.$('li.action-item');
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(ViewCellToggleMoreActions);
|
||||
this._cellToggleMoreActions.onInit(moreActionsContainer, context);
|
||||
taskbarContent.push({ element: moreActionsContainer });
|
||||
|
||||
this._actionbar.setContent(taskbarContent);
|
||||
}
|
||||
}
|
||||
|
||||
get elementRef(): ElementRef {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
|
||||
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInputOptions, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
|
||||
|
||||
export class ViewOptionsModal extends Modal {
|
||||
private _submitButton: Button;
|
||||
private _cancelButton: Button;
|
||||
private _optionsMap: { [name: string]: InputBox | Checkbox } = {};
|
||||
private _viewNameInput: InputBox;
|
||||
|
||||
constructor(
|
||||
private _view: INotebookView,
|
||||
@ILogService logService: ILogService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@ILayoutService layoutService: ILayoutService,
|
||||
@IClipboardService clipboardService: IClipboardService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
|
||||
) {
|
||||
super(
|
||||
localize("viewOptionsModal.title", "Configure View"),
|
||||
'ViewOptionsModal',
|
||||
telemetryService,
|
||||
layoutService,
|
||||
clipboardService,
|
||||
themeService,
|
||||
logService,
|
||||
textResourcePropertiesService,
|
||||
contextKeyService,
|
||||
{ hasErrors: true, hasSpinner: true }
|
||||
);
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
const formWrapper = DOM.$<HTMLDivElement>('div#view-options-form');
|
||||
formWrapper.style.padding = '10px';
|
||||
|
||||
DOM.append(container, formWrapper);
|
||||
|
||||
this._viewNameInput = this.createNameInput(formWrapper);
|
||||
|
||||
}
|
||||
|
||||
protected layout(height: number): void {
|
||||
|
||||
}
|
||||
|
||||
protected createNameInput(container: HTMLElement): InputBox {
|
||||
return this.createInputBoxHelper(container, localize('viewOptionsModal.name', "View Name"), this._view.name, {
|
||||
validationOptions: {
|
||||
validation: (value: string) => {
|
||||
if (!value) {
|
||||
return ({ type: MessageType.ERROR, content: localize('viewOptionsModal.missingRequireField', "This field is required.") });
|
||||
}
|
||||
if (this._view.name !== value && !this._view.nameAvailable(value)) {
|
||||
return ({ type: MessageType.ERROR, content: localize('viewOptionsModal.nameTaken', "This view name has already been taken.") });
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
ariaLabel: localize('viewOptionsModal.name', "View Name")
|
||||
});
|
||||
}
|
||||
|
||||
private createInputBoxHelper(container: HTMLElement, label: string, defaultValue: string = '', options?: IInputOptions): InputBox {
|
||||
const inputContainer = DOM.append(container, DOM.$('.dialog-input-section'));
|
||||
DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = label;
|
||||
const input = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, options);
|
||||
input.value = defaultValue;
|
||||
return input;
|
||||
}
|
||||
|
||||
override render() {
|
||||
super.render();
|
||||
|
||||
this._submitButton = this.addFooterButton(localize('save', "Save"), () => this.onSubmitHandler());
|
||||
this._cancelButton = this.addFooterButton(localize('cancel', "Cancel"), () => this.onCancelHandler(), 'right', true);
|
||||
|
||||
this._register(attachInputBoxStyler(this._viewNameInput!, this._themeService));
|
||||
this._register(attachButtonStyler(this._submitButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._cancelButton, this._themeService));
|
||||
|
||||
this._register(this._viewNameInput.onDidChange(v => this.validate()));
|
||||
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
this.validate();
|
||||
}
|
||||
|
||||
private validate() {
|
||||
let valid = true;
|
||||
|
||||
if (this._viewNameInput.validate()) {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
this._submitButton.enabled = valid;
|
||||
}
|
||||
|
||||
private onSubmitHandler() {
|
||||
this._view.name = this._viewNameInput.value;
|
||||
this._view.save();
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
private onCancelHandler() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
this.show();
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
for (let key in this._optionsMap) {
|
||||
let widget = this._optionsMap[key];
|
||||
widget.dispose();
|
||||
delete this._optionsMap[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user