Notebook Views Actions (#16207)

This adds the actions currently needed by the views
This commit is contained in:
Daniel Grajeda
2021-08-03 23:52:27 -06:00
committed by GitHub
parent 6985d95300
commit 0567141bc4
20 changed files with 1027 additions and 9 deletions

View File

@@ -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;
}

View File

@@ -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()};
}
`);
}
});

View File

@@ -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 }
]);
}

View File

@@ -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 });
}
}

View File

@@ -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;
}

View File

@@ -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];
}
}
}