Notebook Views Insert Cells Modal (#16836)

* Add html-to-image package

* Add image card type
This commit is contained in:
Daniel Grajeda
2021-08-31 14:11:21 -06:00
committed by GitHub
parent a96bf181c0
commit e02ae0865a
14 changed files with 305 additions and 99 deletions

View File

@@ -767,6 +767,13 @@ declare module 'azdata' {
delete?: boolean; delete?: boolean;
} }
export enum CardType {
/**
* Card with the icon as a background image
*/
Image = 'Image'
}
export namespace workspace { export namespace workspace {
/** /**
* Creates and enters a workspace at the specified location * Creates and enters a workspace at the specified location

View File

@@ -396,7 +396,8 @@ export enum DeclarativeDataType {
export enum CardType { export enum CardType {
VerticalButton = 'VerticalButton', VerticalButton = 'VerticalButton',
Details = 'Details', Details = 'Details',
ListItem = 'ListItem' ListItem = 'ListItem',
Image = 'Image'
} }
export enum Orientation { export enum Orientation {

View File

@@ -2,7 +2,7 @@
<div #cardDiv role="radio" *ngIf="label" [class]="getClass() + ' horizontal'" (click)="onCardClick()" <div #cardDiv role="radio" *ngIf="label" [class]="getClass() + ' horizontal'" (click)="onCardClick()"
[attr.aria-checked]="selected" (mouseover)="onCardHoverChanged($event)" (mouseout)="onCardHoverChanged($event)" [attr.aria-checked]="selected" (mouseover)="onCardHoverChanged($event)" (mouseout)="onCardHoverChanged($event)"
tabIndex="0" [style.width]="width" [style.height]="height"> tabIndex="0" [style.width]="width" [style.height]="height">
<ng-container *ngIf="isVerticalButton || isDetailsCard"> <ng-container *ngIf="isVerticalButton || isDetailsCard || isImageCard">
<span *ngIf="hasStatus" class="card-status"> <span *ngIf="hasStatus" class="card-status">
<div class="status-content" [style.backgroundColor]="statusColor"></div> <div class="status-content" [style.backgroundColor]="statusColor"></div>
</span> </span>
@@ -47,6 +47,15 @@
</span> </span>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="isImageCard">
<div class="card-content">
<div [class]="'card-image ' + iconClass"></div>
<div *ngIf="label" class="card-label-overlay">
<h4 class="card-label">{{label}}</h4>
</div>
</div>
</ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="isListItemCard"> <ng-container *ngIf="isListItemCard">
<div class="list-item-content"> <div class="list-item-content">

View File

@@ -18,8 +18,9 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes'; import { KeyCode } from 'vs/base/common/keyCodes';
import * as DOM from 'vs/base/browser/dom'; import * as DOM from 'vs/base/browser/dom';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/platform/dashboard/browser/interfaces'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/platform/dashboard/browser/interfaces';
import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { CARD_OVERLAY_BACKGROUND, CARD_OVERLAY_FOREGROUND } from 'vs/workbench/common/theme';
export interface ActionDescriptor { export interface ActionDescriptor {
label: string; label: string;
@@ -52,7 +53,8 @@ export interface CardDescriptionItem {
export enum CardType { export enum CardType {
VerticalButton = 'VerticalButton', VerticalButton = 'VerticalButton',
Details = 'Details', Details = 'Details',
ListItem = 'ListItem' ListItem = 'ListItem',
Image = 'Image'
} }
@Component({ @Component({
@@ -119,6 +121,11 @@ export default class CardComponent extends ComponentWithIconBase<azdata.CardProp
public getClass(): string { public getClass(): string {
let cardClass = this.isListItemCard ? 'model-card-list-item-legacy' : 'model-card-legacy'; let cardClass = this.isListItemCard ? 'model-card-list-item-legacy' : 'model-card-legacy';
if (this.cardType === 'Image') {
cardClass += ' image-card';
}
return (this.selectable && this.selected || this._hasFocus) ? `${cardClass} selected` : return (this.selectable && this.selected || this._hasFocus) ? `${cardClass} selected` :
`${cardClass} unselected`; `${cardClass} unselected`;
} }
@@ -151,7 +158,7 @@ export default class CardComponent extends ComponentWithIconBase<azdata.CardProp
} }
private get selectable(): boolean { private get selectable(): boolean {
return this.enabled && (this.cardType === 'VerticalButton' || this.cardType === 'ListItem'); return this.enabled && (this.cardType === 'VerticalButton' || this.cardType === 'ListItem' || this.cardType === 'Image');
} }
// CSS-bound properties // CSS-bound properties
@@ -184,6 +191,10 @@ export default class CardComponent extends ComponentWithIconBase<azdata.CardProp
return !this.cardType || this.cardType === 'ListItem'; return !this.cardType || this.cardType === 'ListItem';
} }
public get isImageCard(): boolean {
return !this.cardType || this.cardType === 'Image';
}
public get isVerticalButton(): boolean { public get isVerticalButton(): boolean {
return this.cardType === 'VerticalButton'; return this.cardType === 'VerticalButton';
} }
@@ -236,3 +247,23 @@ export default class CardComponent extends ComponentWithIconBase<azdata.CardProp
} }
} }
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const backgroundColor = theme.getColor(CARD_OVERLAY_BACKGROUND);
const foregroundColor = theme.getColor(CARD_OVERLAY_FOREGROUND);
if (backgroundColor) {
collector.addRule(`
.model-card-legacy .card-label-overlay {
background-color: ${backgroundColor.toString()};
}
`);
}
if (foregroundColor) {
collector.addRule(`
.model-card-legacy.image-card .card-label {
color: ${foregroundColor.toString()};
}
`);
}
});

View File

@@ -26,6 +26,47 @@
min-width: 30px; min-width: 30px;
} }
.model-card-legacy.image-card {
height: auto;
margin: 0;
display: block;
}
.model-card-legacy.image-card .card-content {
display: block;
width: 100%;
height: 100%;
padding: 0;
min-height: 100px;
}
.model-card-legacy.image-card .card-image {
position: absolute;
height: 100%;
width: 100%;
background-size: contain;
background-position: initial;
background-repeat: no-repeat;
max-width: unset;
max-height: unset;
}
.model-card-legacy.image-card .card-label-overlay {
position: absolute;
bottom: 0;
width: 100%;
height: 25px;
line-height: 25px;
padding: 0;
margin: 0;
display: block;
}
.model-card-legacy.image-card .card-label {
margin: 0;
padding: 0px 10px
}
.model-card-legacy .card-vertical-button { .model-card-legacy .card-vertical-button {
position: relative; position: relative;
display: flex; display: flex;
@@ -103,6 +144,7 @@
border-width: 1px; border-width: 1px;
border-color: rgb(0, 120, 215); border-color: rgb(0, 120, 215);
border-style: solid; border-style: solid;
z-index: 1;
} }
.model-card-list-item-legacy .selection-indicator-container, .model-card-legacy .selection-indicator-container { .model-card-list-item-legacy .selection-indicator-container, .model-card-legacy .selection-indicator-container {

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
ApplicationRef, ComponentFactoryResolver, NgModule,
Inject, forwardRef, Type
} from '@angular/core';
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { providerIterator } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IBootstrapParams, ISelector } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
import CardComponent from 'sql/workbench/browser/modelComponents/card.component';
import DivContainer from 'sql/workbench/browser/modelComponents/divContainer.component';
import { ModelComponentWrapper } from 'sql/workbench/browser/modelComponents/modelComponentWrapper.component';
import { InsertCellsScreenshots } from 'sql/workbench/contrib/notebook/browser/notebookViews/insertCellsScreenshots.component';
import ImageComponent from 'sql/workbench/browser/modelComponents/image.component';
// Insertcells main angular module
export const InsertCellsModule = (params: IBootstrapParams, selector: string, instantiationService: IInstantiationService): Type<any> => {
@NgModule({
declarations: [
CardComponent,
DivContainer,
ModelComponentWrapper,
InsertCellsScreenshots,
ImageComponent,
],
entryComponents: [InsertCellsScreenshots, CardComponent, ImageComponent],
imports: [
FormsModule,
CommonModule,
BrowserModule
],
providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
{ provide: IBootstrapParams, useValue: params },
{ provide: ISelector, useValue: selector },
...providerIterator(instantiationService)
]
})
class ModuleClass {
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
@Inject(ISelector) private selector: string
) {
}
ngDoBootstrap(appRef: ApplicationRef) {
const factory = this._resolver.resolveComponentFactory(InsertCellsScreenshots);
(<any>factory).factory.selector = this.selector;
appRef.bootstrap(factory);
}
}
return ModuleClass;
};

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./insertCellsModal'; import 'vs/css!./insertCellsModal';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { Button } from 'sql/base/browser/ui/button/button'; import { Button } from 'sql/base/browser/ui/button/button';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
@@ -13,22 +12,24 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IThemeService } 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 { ServiceOptionType } from 'sql/platform/connection/common/interfaces';
import { ServiceOption } from 'azdata'; import { ServiceOption } from 'azdata';
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper'; import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component'; import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core'; import { NgModuleRef, ComponentFactoryResolver, ViewContainerRef } from '@angular/core';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { inputBorder, inputValidationInfoBorder } from 'vs/platform/theme/common/colorRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { bootstrapAngular } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { InsertCellsModule } from 'sql/workbench/contrib/notebook/browser/notebookViews/insertCellsModal.module';
import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { truncate } from 'vs/base/common/strings'; import { truncate } from 'vs/base/common/strings';
import { toJpeg } from 'html-to-image'; import { toJpeg } from 'html-to-image';
import { IComponentEventArgs } from 'sql/platform/dashboard/browser/interfaces';
import { Thumbnail } from 'sql/workbench/contrib/notebook/browser/notebookViews/insertCellsScreenshots.component';
type CellOption = { type CellOption = {
optionMetadata: ServiceOption, optionMetadata: ServiceOption,
@@ -54,6 +55,10 @@ export class CellOptionsModel {
}); });
} }
public get checkedOptions(): CellOption[] {
return Object.values(this._optionsMap).filter(o => o.currentValue === true);
}
private getDisplayValue(optionMetadata: ServiceOption, optionValue: string): boolean { private getDisplayValue(optionMetadata: ServiceOption, optionValue: string): boolean {
let displayValue: boolean = false; let displayValue: boolean = false;
switch (optionMetadata.valueType) { switch (optionMetadata.valueType) {
@@ -93,8 +98,8 @@ export class InsertCellsModal extends Modal {
private _submitButton: Button; private _submitButton: Button;
private _cancelButton: Button; private _cancelButton: Button;
private _optionsMap: { [name: string]: Checkbox } = {};
private _maxTitleLength: number = 20; private _maxTitleLength: number = 20;
private _moduleRef?: NgModuleRef<typeof InsertCellsModule>;
constructor( constructor(
private onInsert: (cell: ICellModel) => void, private onInsert: (cell: ICellModel) => void,
@@ -107,6 +112,7 @@ export class InsertCellsModal extends Modal {
@IClipboardService clipboardService: IClipboardService, @IClipboardService clipboardService: IClipboardService,
@IContextKeyService contextKeyService: IContextKeyService, @IContextKeyService contextKeyService: IContextKeyService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService, @IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IInstantiationService private _instantiationService: IInstantiationService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
) { ) {
super( super(
@@ -127,21 +133,7 @@ export class InsertCellsModal extends Modal {
} }
protected renderBody(container: HTMLElement): void { protected renderBody(container: HTMLElement): void {
const grid = DOM.$<HTMLDivElement>('div#insert-dialog-cell-grid'); this.createOptions(container)
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.")); }); .catch((e) => { this.setError(localize("insertCellsModal.thumbnailError", "Error: Unable to generate thumbnails.")); });
} }
@@ -153,44 +145,29 @@ export class InsertCellsModal extends Modal {
const activeView = this._context.getActiveView(); const activeView = this._context.getActiveView();
const cellsAvailableToInsert = activeView.hiddenCells; const cellsAvailableToInsert = activeView.hiddenCells;
cellsAvailableToInsert.forEach(async (cell) => { const thumbnails = await Promise.all(
const optionWidget = this.createCheckBoxHelper( cellsAvailableToInsert.map(async (cell) => {
container, return {
'<div class="loading-spinner-container"><div class="loading-spinner codicon in-progress"></div></div>', id: cell.cellGuid,
false, path: await this.generateScreenshot(cell),
() => this.onOptionChecked(cell.cellGuid) title: localize("insertCellsModal.cellTitle", "Cell {0}", Number.parseInt(cell.id) + 1)
} as Thumbnail;
})
); );
const img = await this.generateScreenshot(cell); this.bootstrapAngular(container, thumbnails);
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 { public onOptionChecked(e: IComponentEventArgs) {
const checkbox = new Checkbox(DOM.append(container, DOM.$('.dialog-input-section')), { if (e.args?.value) {
label: label, let optionName: string = e.args.value;
checked: isChecked, this.viewModel.setOptionValue(optionName, e.args.selected);
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(); this.validate();
} }
}
public async generateScreenshot(cell: ICellModel, screenshotWidth: number = 300, screenshowHeight: number = 300, backgroundColor: string = '#ffffff'): Promise<string> { public async generateScreenshot(cell: ICellModel, screenshotWidth: number = 300, screenshowHeight: number = 300, backgroundColor: string = '#ffffff'): Promise<string> {
try {
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(TextCellComponent); let componentFactory = this._componentFactoryResolver.resolveComponentFactory(TextCellComponent);
let component = this._containerRef.createComponent(componentFactory); let component = this._containerRef.createComponent(componentFactory);
@@ -206,6 +183,10 @@ export class InsertCellsModal extends Modal {
const canvasHeight = element.clientHeight / scale; const canvasHeight = element.clientHeight / scale;
return toJpeg(component.instance.outputRef.nativeElement, { quality: .6, canvasWidth, canvasHeight, backgroundColor }); return toJpeg(component.instance.outputRef.nativeElement, { quality: .6, canvasWidth, canvasHeight, backgroundColor });
} catch (e) {
this.logService.error(`Error generating screenshot: ${e}`);
return '';
}
} }
private getOptions(): ServiceOption[] { private getOptions(): ServiceOption[] {
@@ -239,7 +220,7 @@ export class InsertCellsModal extends Modal {
} }
private validate() { private validate() {
if (Object.keys(this._optionsMap).length) { if (this.viewModel.checkedOptions.length) {
this._submitButton.enabled = true; this._submitButton.enabled = true;
} else { } else {
this._submitButton.enabled = false; this._submitButton.enabled = false;
@@ -265,34 +246,25 @@ export class InsertCellsModal extends Modal {
public override dispose(): void { public override dispose(): void {
super.dispose(); super.dispose();
for (let key in this._optionsMap) {
let widget = this._optionsMap[key]; if (this._moduleRef) {
widget.dispose(); this._moduleRef.destroy();
delete this._optionsMap[key];
}
} }
} }
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { /**
const inputBorderColor = theme.getColor(inputBorder); * Get the bootstrap params and perform the bootstrap
if (inputBorderColor) { */
collector.addRule(` private bootstrapAngular(bodyContainer: HTMLElement, thumbnails: Thumbnail[]) {
#insert-dialog-cell-grid input[type="checkbox"] + label { this._instantiationService.invokeFunction<void, any[]>(bootstrapAngular,
border: 2px solid; InsertCellsModule,
border-color: ${inputBorderColor.toString()}; bodyContainer,
display: flex; 'insert-cells-screenshots-component',
height: 125px; {
overflow: hidden; thumbnails,
onClick: (e: IComponentEventArgs) => { this.onOptionChecked(e); }
},
undefined,
(moduleRef: NgModuleRef<typeof InsertCellsModule>) => this._moduleRef = moduleRef);
} }
`);
} }
const inputActiveOptionBorderColor = theme.getColor(inputValidationInfoBorder);
if (inputActiveOptionBorderColor) {
collector.addRule(`
#insert-dialog-cell-grid input[type="checkbox"]:checked + label {
border-color: ${inputActiveOptionBorderColor.toString()};
}
`);
}
});

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* 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!./insertCellsScreenshots';
import { Component, AfterViewInit, forwardRef, Inject, ComponentFactoryResolver, ViewContainerRef, ViewChild } from '@angular/core';
import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
import { IComponentEventArgs } from 'sql/platform/dashboard/browser/interfaces';
import CardComponent, { CardType } from 'sql/workbench/browser/modelComponents/card.component';
import { URI } from 'vs/base/common/uri';
export interface LayoutRequestParams {
modelViewId?: string;
alwaysRefresh?: boolean;
}
export interface Thumbnail {
id: string,
path: string,
title: string
}
export interface InsertCellsComponentParams extends IBootstrapParams {
thumbnails: Thumbnail[],
onClick: (e: IComponentEventArgs) => void
}
@Component({
selector: 'insert-cells-screenshots-component',
template: '<div class="insert-cells-screenshot-grid"><ng-container #divContainer></ng-container></div>'
})
export class InsertCellsScreenshots implements AfterViewInit {
@ViewChild('divContainer', { read: ViewContainerRef }) _containerRef: ViewContainerRef;
constructor(
@Inject(IBootstrapParams) private _params: InsertCellsComponentParams,
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver
) { }
ngAfterViewInit(): void {
this._params.thumbnails.forEach((thumbnail: Thumbnail, idx: number) => {
const cellImageUri = URI.parse(thumbnail.path);
let cardComponentFactory = this._componentFactoryResolver.resolveComponentFactory(CardComponent);
let cardComponent = this._containerRef.createComponent(cardComponentFactory);
cardComponent.instance.setProperties({ iconPath: cellImageUri, label: thumbnail.title, value: thumbnail.id, cardType: CardType.Image });
cardComponent.instance.enabled = true;
cardComponent.instance.registerEventHandler(e => this._params.onClick(e));
});
}
}

View File

@@ -0,0 +1,8 @@
.insert-cells-screenshot-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
padding: 10px;
overflow-y: auto;
max-height: calc(100% - 40px);
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Component, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef, ViewContainerRef, ComponentFactoryResolver } from '@angular/core'; import { Component, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { ICellModel, INotebookModel, ISingleNotebookEditOperation, NotebookContentChange } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { ICellModel, INotebookModel, ISingleNotebookEditOperation, NotebookContentChange } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import 'vs/css!./notebookViewsGrid';
import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component'; import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component';
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component'; import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
import { ICellEditorProvider, INotebookParams, INotebookService, INotebookEditor, NotebookRange, INotebookSection, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; import { ICellEditorProvider, INotebookParams, INotebookService, INotebookEditor, NotebookRange, INotebookSection, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService';
@@ -260,6 +261,10 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
let insertCellsAction = this._instantiationService.createInstance(InsertCellAction, this.insertCell.bind(this), this.views, this._containerRef, this._componentFactoryResolver); let insertCellsAction = this._instantiationService.createInstance(InsertCellAction, this.insertCell.bind(this), this.views, this._containerRef, this._componentFactoryResolver);
// Hide the insert cell action button if no cells can be added
insertCellsAction.enabled = this.activeView?.hiddenCells.length > 0;
this.activeView.onCellVisibilityChanged(e => { insertCellsAction.enabled = this.activeView?.hiddenCells.length > 0; });
this._runAllCellsAction = this._instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAllPreview', "Run all"), 'notebook-button masked-pseudo start-outline'); this._runAllCellsAction = this._instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAllPreview', "Run all"), 'notebook-button masked-pseudo start-outline');
let spacerElement = document.createElement('li'); let spacerElement = document.createElement('li');

View File

@@ -770,6 +770,7 @@ export class NotebookViewStub implements INotebookView {
displayedCells: readonly ICellModel[]; displayedCells: readonly ICellModel[];
onDeleted: vsEvent.Event<INotebookView>; onDeleted: vsEvent.Event<INotebookView>;
onCellVisibilityChanged: vsEvent.Event<ICellModel>;
initialize(): void { initialize(): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

View File

@@ -21,10 +21,12 @@ function cellCollides(c1: INotebookViewCell, c2: INotebookViewCell): boolean {
export class NotebookViewModel implements INotebookView { export class NotebookViewModel implements INotebookView {
private _onDeleted = new Emitter<INotebookView>(); private _onDeleted = new Emitter<INotebookView>();
private _onCellVisibilityChanged = new Emitter<ICellModel>();
private _isNew: boolean = false; private _isNew: boolean = false;
public readonly guid: string; public readonly guid: string;
public readonly onDeleted = this._onDeleted.event; public readonly onDeleted = this._onDeleted.event;
public readonly onCellVisibilityChanged = this._onCellVisibilityChanged.event;
constructor( constructor(
protected _name: string, protected _name: string,
@@ -119,10 +121,12 @@ export class NotebookViewModel implements INotebookView {
public insertCell(cell: ICellModel) { public insertCell(cell: ICellModel) {
this.updateCell(cell, this, { hidden: false }); this.updateCell(cell, this, { hidden: false });
this._onCellVisibilityChanged.fire(cell);
} }
public hideCell(cell: ICellModel) { public hideCell(cell: ICellModel) {
this.updateCell(cell, this, { hidden: true }); this.updateCell(cell, this, { hidden: true });
this._onCellVisibilityChanged.fire(cell);
} }
public moveCell(cell: ICellModel, x: number, y: number) { public moveCell(cell: ICellModel, x: number, y: number) {

View File

@@ -26,8 +26,9 @@ export interface INotebookViews {
export interface INotebookView { export interface INotebookView {
readonly guid: string; readonly guid: string;
readonly onDeleted: Event<INotebookView>; readonly onDeleted: Event<INotebookView>;
isNew: boolean; readonly onCellVisibilityChanged: Event<ICellModel>;
isNew: boolean;
cells: Readonly<ICellModel[]>; cells: Readonly<ICellModel[]>;
hiddenCells: Readonly<ICellModel[]>; hiddenCells: Readonly<ICellModel[]>;
displayedCells: Readonly<ICellModel[]>; displayedCells: Readonly<ICellModel[]>;

View File

@@ -696,3 +696,17 @@ export const WINDOW_INACTIVE_BORDER = registerColor('window.inactiveBorder', {
light: null, light: null,
hc: contrastBorder hc: contrastBorder
}, localize('windowInactiveBorder', "The color used for the border of the window when it is inactive. Only supported in the desktop client when using the custom title bar.")); }, localize('windowInactiveBorder', "The color used for the border of the window when it is inactive. Only supported in the desktop client when using the custom title bar."));
// < --- Card --- >
export const CARD_OVERLAY_FOREGROUND = registerColor('card.overlayForeground', {
dark: null,
light: null,
hc: null
}, localize('cardOverlayForeground', "The card overlay foreground color."));
export const CARD_OVERLAY_BACKGROUND = registerColor('card.overlayBackground', {
dark: '#252526',
light: '#F3F3F3',
hc: '#000000'
}, localize('cardOverlayBackground', "The card overlay background color."));