Introduce tabs for notebook views (#19526)

* Introduce tabs for notebook views

Cards have been restructured to contain tabs instead of cells directly.
Tabs then contain the cards that are displayed. Cards may contain one or
more cards.

The panel component has been reused to implement the cells. There is
still some cleanup left to do of unused functions, but I want to reduce
the size of the PR as much as possible.
This commit is contained in:
Daniel Grajeda
2022-06-06 03:07:08 -07:00
committed by GitHub
parent 535799fe23
commit 4fd2f92e27
14 changed files with 509 additions and 209 deletions

View File

@@ -39,7 +39,9 @@ import { NotebookViewsCardComponent } from 'sql/workbench/contrib/notebook/brows
import { NotebookViewsGridComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component';
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
import { NotebookViewsModalComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.component';
import { TabComponent } from 'sql/base/browser/ui/panel/tab.component';
import { PanelComponent } from 'sql/base/browser/ui/panel/panel.component';
import { TabHeaderComponent } from 'sql/base/browser/ui/panel/tabHeader.component';
const outputComponentRegistry = Registry.as<ICellComponentRegistry>(OutputComponentExtensions.CellComponentContributions);
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
@@ -66,6 +68,9 @@ export const NotebookModule = (params, selector: string, instantiationService: I
NotebookViewsGridComponent,
NotebookViewsCodeCellComponent,
NotebookViewsModalComponent,
TabComponent,
TabHeaderComponent,
PanelComponent,
ComponentHostDirective,
OutputAreaComponent,
OutputComponent,

View File

@@ -6,28 +6,39 @@
-->
<ng-template #templateRef>
<div
*ngIf="visible"
class="grid-stack-item"
attr.data-cell-id="{{cell.cellGuid}}"
attr.data-card-guid="{{guid}}"
attr.gs-w="{{width}}"
attr.gs-h="{{height}}"
attr.gs-x="{{x}}"
attr.gs-y="{{y}}"
(click)="selectCell(cell, $event)"
>
<div
#item
class="grid-stack-item-content notebook-cell"
[class.active]="cell.active"
>
<div #actionbar [style.display]="showActionBar ? 'block' : 'none'" class="actionbar"></div>
<div class="grid-stack-content-wrapper">
<div class="grid-stack-header" ></div>
<div class="grid-stack-content-wrapper-inner">
<views-code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId" [visible]="visible">
</views-code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</text-cell-component>
<!--<div #actionbar [style.display]="showActionBar ? 'block' : 'none'" class="actionbar"></div>-->
<panel (onTabChange)="handleTabChange($event)" (onTabClose)="handleTabClose($event)">
<tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize" [identifier]="tab.id" [type]="'tab'" [canClose]="true">
<ng-template>
<ng-container>
<div #container class="card-container">
<div *ngIf="tab.cellModel !== undefined">
<views-code-cell-component *ngIf="tab.cellModel.cellType === 'code'" [cellModel]="tab.cellModel" [model]="model" [activeCellId]="tab.cellModel.id" [visible]="visible">
</views-code-cell-component>
<text-cell-component *ngIf="tab.cellModel.cellType === 'markdown'" [cellModel]="tab.cellModel" [model]="model" [activeCellId]="tab.cellModel.id">
</text-cell-component>
</div>
</div>
</ng-container>
</ng-template>
</tab>
</panel>
</div>
<div class="grid-stack-footer"></div>
</div>
</div>
</div>

View File

@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./cellToolbar';
import * as DOM from 'vs/base/browser/dom';
import { Component, OnInit, Input, ViewChild, TemplateRef, ElementRef, Inject, Output, EventEmitter, ChangeDetectorRef, forwardRef, SimpleChanges } from '@angular/core';
import { Component, OnInit, Input, ViewChild, TemplateRef, ElementRef, Inject, Output, EventEmitter, ChangeDetectorRef, forwardRef, SimpleChange } from '@angular/core';
import { CellExecutionState, 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 { CellChangeEventType, INotebookView, INotebookViewCell } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { DEFAULT_VIEW_CARD_HEIGHT, DEFAULT_VIEW_CARD_WIDTH, ViewsTab } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel';
import { CellChangeEventType, INotebookView, INotebookViewCard } 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';
@@ -17,22 +17,27 @@ import { CellTypes } from 'sql/workbench/services/notebook/common/contracts';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { cellBorder, notebookToolbarSelectBackground } from 'sql/platform/theme/common/colorRegistry';
import { TabComponent } from 'sql/base/browser/ui/panel/tab.component';
import { EDITOR_GROUP_HEADER_TABS_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_BORDER, TAB_INACTIVE_BACKGROUND } from 'vs/workbench/common/theme';
@Component({
selector: 'view-card-component',
templateUrl: decodeURI(require.toUrl('./notebookViewsCard.component.html'))
})
export class NotebookViewsCardComponent extends AngularDisposable implements OnInit {
cell: ICellModel;
private _actionbar: Taskbar;
private _metadata: INotebookViewCell;
private _executionState: CellExecutionState;
private _pendingReinitialize: boolean = false;
public _cellToggleMoreActions: ViewCellToggleMoreActions;
@Input() cell: ICellModel;
@Input() card: INotebookViewCard;
@Input() cells: ICellModel[];
@Input() model: NotebookModel;
@Input() activeView: INotebookView;
@Input() activeTab: ViewsTab;
@Input() meta: boolean;
@Input() ready: boolean;
@Output() onChange: EventEmitter<any> = new EventEmitter();
@@ -52,19 +57,11 @@ export class NotebookViewsCardComponent extends AngularDisposable implements OnI
this.initialize();
}
ngOnChanges(changes: SimpleChanges) {
if (this.activeView && changes['activeView'] && changes['activeView'].currentValue?.guid !== changes['activeView'].previousValue?.guid) {
this._metadata = this.activeView.getCellMetadata(this.cell);
this._pendingReinitialize = true;
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
this.detectChanges();
}
ngAfterContentInit() {
if (this.activeView) {
this._metadata = this.activeView.getCellMetadata(this.cell);
this._pendingReinitialize = true;
}
ngAfterViewInit() {
this.detectChanges();
}
@@ -75,6 +72,27 @@ export class NotebookViewsCardComponent extends AngularDisposable implements OnI
}
}
handleTabChange(selectedTab: TabComponent) {
const tab = this.tabs.find(t => t.id === selectedTab.identifier);
if (tab && this.cell?.cellGuid !== tab.cell?.guid) {
this.cell = this.cells.find(c => c.cellGuid === tab.cell.guid);
this.model.updateActiveCell(this.cell);
this.changed('active');
this.initActionBar();
}
}
handleTabClose(selectedTab: TabComponent) {
const tab = this.tabs.find(t => t.id === selectedTab.identifier);
if (tab) {
const cell = this.cells.find(c => c.cellGuid === tab.cell.guid);
if (cell) {
this.activeView.hideCell(cell);
}
}
}
override ngOnDestroy() {
if (this._actionbar) {
this._actionbar.dispose();
@@ -83,9 +101,17 @@ export class NotebookViewsCardComponent extends AngularDisposable implements OnI
public initialize(): void {
this.initActionBar();
if (this.card.activeTab !== undefined) {
this.cell = this.cells.find(c => c.cellGuid === this.card.activeTab.cell.guid);
}
this.detectChanges();
}
public get tabs(): ViewsTab[] {
return this.card?.tabs ?? [];
}
initActionBar() {
if (this._actionbarRef) {
let taskbarContent: ITaskbarContent[] = [];
@@ -119,10 +145,12 @@ export class NotebookViewsCardComponent extends AngularDisposable implements OnI
return this._item;
}
changed(event: CellChangeEventType) {
this.onChange.emit({ cell: this.cell, event: event });
}
get displayInputModal(): boolean {
return this.awaitingInput;
}
@@ -154,8 +182,13 @@ export class NotebookViewsCardComponent extends AngularDisposable implements OnI
this.changed('hide');
}
public get metadata(): INotebookViewCell {
return this._metadata;
public get metadata(): INotebookViewCard {
return this.card;
}
public get guid(): string {
return this.metadata.guid;
}
public get width(): number {
@@ -182,7 +215,7 @@ export class NotebookViewsCardComponent extends AngularDisposable implements OnI
return true;
}
if (!this._metadata) { //Means not initialized
if (!this.cell) { //Means not initialized
return false;
}
@@ -204,8 +237,8 @@ export class NotebookViewsCardComponent extends AngularDisposable implements OnI
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const cellBorderColor = theme.getColor(cellBorder);
if (cellBorderColor) {
collector.addRule(`.notebookEditor .nb-grid-stack .notebook-cell.active .actionbar { border-color: ${cellBorderColor};}`);
collector.addRule(`.notebookEditor .nb-grid-stack .notebook-cell.active .actionbar .codicon:before { background-color: ${cellBorderColor};}`);
collector.addRule(`.notebookEditor .nb-grid-stack .actionbar { border-color: ${cellBorderColor};}`);
collector.addRule(`.notebookEditor .nb-grid-stack .actionbar .codicon:before { background-color: ${cellBorderColor};}`);
}
// Cell toolbar background
@@ -213,4 +246,51 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
if (notebookToolbarSelectBackgroundColor) {
collector.addRule(`.notebookEditor .nb-grid-stack .notebook-cell.active .actionbar { background-color: ${notebookToolbarSelectBackgroundColor};}`);
}
const tabBorder = theme.getColor(TAB_BORDER);
const tabBackground = theme.getColor(TAB_INACTIVE_BACKGROUND);
const tabActiveBackground = theme.getColor(TAB_ACTIVE_BACKGROUND);
const headerBackground = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND);
if (headerBackground) {
collector.addRule(`
.notebook-cell .grid-stack-header {
background-color: ${headerBackground.toString()};
}
`);
}
if (tabBackground && tabBorder) {
collector.addRule(`
.notebook-cell .tabbedPanel.horizontal > .title .tabList {
border-color: ${tabBorder.toString()};
background-color: ${tabBackground.toString()};
}
.notebook-cell .tabbedPanel.horizontal > .title .tabList .tab-header {
border-right: 1px solid ${tabBorder.toString()};
background-color: ${tabBackground.toString()};
margin: 0;
}
.notebook-cell .tabbedPanel.horizontal > .title .tabList a.action-label.codicon.close {
background-size: 9px 9px !important;
margin-top: -1px;
}
.notebook-cell .tabbedPanel.horizontal > .title .tabList .actions-container {
margin-right: 0px;
margin-left: 8px;
}
`);
}
if (tabActiveBackground) {
collector.addRule(`
.notebook-cell .tabbedPanel.horizontal > .title .tabList .tab-header.active {
background-color: ${tabActiveBackground.toString()};
}
`);
}
});

View File

@@ -5,8 +5,8 @@
*--------------------------------------------------------------------------------------------*/
-->
<div class="nb-grid-stack grid-stack">
<ng-container *ngFor="let cell of cells">
<view-card-component #wrapper [ready]="loaded" [activeView]="activeView" [meta]="cell.metadata" [cell]="cell" [model]="model" (onChange)="onCellChanged($event)"></view-card-component>
<ng-container #divContainer *ngFor="let card of cards; trackBy: trackByCardId">
<view-card-component #wrapper [activeView]="activeView" [card]="card" [model]="model" [cells]="cells"></view-card-component>
<ng-content *ngTemplateOutlet='wrapper.templateRef'></ng-content>
</ng-container>
</div>

View File

@@ -4,16 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./notebookViewsGrid';
import { Component, OnInit, ViewChildren, QueryList, Input, Inject, forwardRef, ChangeDetectorRef, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, ViewChildren, QueryList, Input, Inject, forwardRef, ChangeDetectorRef, ViewEncapsulation, ChangeDetectionStrategy, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { NotebookViewsCardComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { GridItemHTMLElement, GridStack, GridStackEvent, GridStackNode } from 'gridstack';
import { localize } from 'vs/nls';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { CellChangeEvent, INotebookView, INotebookViewCell } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { CellChangeEvent, INotebookView, INotebookViewCard } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { generateLayout } from 'sql/workbench/services/notebook/browser/notebookViews/autodash';
export interface INotebookViewsGridOptions {
cellHeight?: number;
@@ -31,6 +30,7 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
@Input() activeView: INotebookView;
@Input() views: NotebookViewsExtension;
@ViewChild('divContainer', { read: ViewContainerRef }) _containerRef: ViewContainerRef;
@ViewChildren(NotebookViewsCardComponent) private _items: QueryList<NotebookViewsCardComponent>;
protected _grid: GridStack;
@@ -45,13 +45,18 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver
) {
super();
this._loaded = false;
}
public get cards(): INotebookViewCard[] {
return this.activeView ? this.activeView.cards : [];
}
public get empty(): boolean {
return !this._items || !this._items.find(item => item.visible);
return !this._items?.length;
}
public get hiddenItems(): NotebookViewsCardComponent[] {
@@ -89,11 +94,6 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
this._grid = undefined;
}
}
if (this.model?.activeCell?.id !== this._activeCell?.id) {
this._activeCell = this.model.activeCell;
this.detectChanges();
}
}
ngAfterViewChecked() {
@@ -135,11 +135,12 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
}
this._grid = GridStack.init({
alwaysShowResizeHandle: false,
alwaysShowResizeHandle: true,
styleInHead: true,
margin: 2,
margin: 5,
cellHeight: this._options.cellHeight,
staticGrid: false,
handleClass: 'grid-stack-header'
});
@@ -157,41 +158,48 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
}
this._grid.batchUpdate();
this.activeView.cells.forEach(cell => {
const el = this._grid.getGridItems().find(x => x.getAttribute('data-cell-id') === cell.cellGuid);
this.activeView.cards.forEach(card => {
const el = this._grid.getGridItems().find(x => x.getAttribute('data-card-guid') === card.guid);
if (el) {
const cellData = this.activeView.getCellMetadata(cell);
this._grid.update(el, { x: cellData.x, y: cellData.y, w: cellData.width, h: cellData.height });
if (cellData?.hidden) {
this._grid.removeWidget(el, false); // Do not trigger event for batch update
}
this._grid.update(el, { x: card.x, y: card.y, w: card.width, h: card.height });
}
});
this._grid.commit();
}
private resizeCells(): void {
private resizeCards(): void {
this._items.forEach((i: NotebookViewsCardComponent) => {
if (i.elementRef) {
const cellHeight = this._options.cellHeight;
let maxTabHeight = 30;
const naturalHeight = i.elementRef.nativeElement.clientHeight;
const heightInCells = Math.ceil(naturalHeight / cellHeight);
const cardContainers: HTMLCollection = i.elementRef.nativeElement.getElementsByClassName('card-container');
if (cardContainers) {
maxTabHeight = Array.from(cardContainers).reduce((accum, cardContainer) => {
return Math.max(cardContainer.children[0]?.clientHeight ?? 0, accum);
}, maxTabHeight);
}
const update: INotebookViewCell = {
const cardNaturalHeight = i.elementRef.nativeElement.clientHeight + maxTabHeight;
const heightInCells = Math.ceil(cardNaturalHeight / cellHeight);
const update: INotebookViewCard = {
height: heightInCells
};
this.views.updateCell(i.cell, this.activeView, update);
this.views.updateCard(i, update, this.activeView);
}
});
}
private runAutoLayout(view: INotebookView): void {
//Resize the cells before regenerating layout so that we know the natural height of the cells
this.resizeCells();
generateLayout(view);
this.resizeCards();
//generateLayout(view);
}
trackByCardId(index, item) {
return item ? item.guid : undefined;
}
private detectChanges(): void {
@@ -208,20 +216,29 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
this.activeView.hideCell(e.cell);
}
if (e.cell && e.event === 'insert') {
const component = this._items.find(x => x.cell.cellGuid === e.cell.cellGuid);
this.activeView.insertCell(e.cell);
if (e.cell && e.event === 'insert') {
const card = this.activeView.insertCell(e.cell);
let cardComponentFactory = this._componentFactoryResolver.resolveComponentFactory(NotebookViewsCardComponent);
let cardComponent = this._containerRef.createComponent(cardComponentFactory);
cardComponent.instance.ready = true;
cardComponent.instance.activeView = this.activeView;
cardComponent.instance.card = card;
cardComponent.instance.model = this.model;
cardComponent.instance.cells = this.cells;
cardComponent.instance.initialize();
this.detectChanges();
const el = this._grid.getGridItems().find(x => x.getAttribute('data-cell-id') === e.cell.cellGuid);
this._grid.makeWidget(el);
const el = this._grid.getGridItems().find(x => x.getAttribute('data-card-guid') === card.guid);
this._grid.addWidget(el);
this._grid.update(el, { x: 0, y: 0 });
this._grid.resizable(el, true);
this._grid.movable(el, true);
component.initialize();
}
if (e.cell && e.event === 'update') {
@@ -243,25 +260,19 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
*/
persist(action: GridStackEvent, changedItems: GridStackNode[] = [], grid: GridStack, items: QueryList<NotebookViewsCardComponent>): void {
changedItems.forEach((changedItem) => {
const cellId = changedItem.el.getAttribute('data-cell-id');
const item = items.toArray().find(item => item.cell.cellGuid === cellId);
const cellId = changedItem.el.getAttribute('data-card-guid');
const item = items.toArray().find(item => item.metadata.guid === cellId);
if (item && this.activeView) {
const update: INotebookViewCell = {
guid: this.activeView.guid,
const update: INotebookViewCard = {
guid: item.guid,
x: changedItem.x,
y: changedItem.y,
width: changedItem.w,
height: changedItem.h
};
if (action === 'added') {
update.hidden = false;
} else if (action === 'removed') {
update.hidden = true;
}
this.views.updateCell(item.cell, this.activeView, update);
this.views.updateCard(item.metadata, update, this.activeView);
}
});
}

View File

@@ -30,6 +30,7 @@
.nb-grid-stack > .grid-stack-item > .grid-stack-item-content {
margin: 0;
border: 0;
position: absolute;
width: auto;
overflow-x: hidden;
@@ -310,6 +311,14 @@
.nb-grid-stack > .grid-stack-item {
margin: 0;
}
.nb-grid-stack > .grid-stack-item .grid-stack-header {
width: 100%;
height: 20px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-footer {
width: 100%;
height: 20px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content {
display: flex;
cursor: move;
@@ -325,9 +334,13 @@
margin-bottom: 0px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-wrapper-inner {
height: calc(100% - 25px);
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-wrapper-inner .card-container {
overflow-y: auto;
height: 100%;
cursor: auto;
padding: 0 10px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content ::-webkit-scrollbar {
width: 8px;

View File

@@ -32,7 +32,6 @@ import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/
import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NotebookViewModel } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService';
import { NBFORMAT, NBFORMAT_MINOR } from 'sql/workbench/common/constants';
@@ -95,25 +94,22 @@ suite('NotebookViewModel', function (): void {
test('initialize', async function (): Promise<void> {
let notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
let viewModel = new NotebookViewModel(defaultViewName, notebookViews);
viewModel.initialize();
viewModel.initialize(true); //is new view
let cellsWithNewView = notebookViews.getCells().filter(cell => cell.views.find(v => v.guid === viewModel.guid));
assert.strictEqual(cellsWithNewView.length, 2);
assert.strictEqual(viewModel.cells.length, 2);
assert.strictEqual(viewModel.name, defaultViewName);
assert.strictEqual(viewModel.cards.length, 2, 'View model was not initialized with the correct number of cards');
assert.strictEqual(viewModel.cells.length, 2, 'View model was not initialized with the correct number of cells');
assert.strictEqual(viewModel.name, defaultViewName, 'View model was not inirialized with the correct name');
});
test('initialize notebook with no metadata', async function (): Promise<void> {
let notebookViews = await initializeNotebookViewsExtension(notebookContentWithoutMeta);
let viewModel = new NotebookViewModel(defaultViewName, notebookViews);
viewModel.initialize();
viewModel.initialize(true);
let cellsWithNewView = notebookViews.getCells().filter(cell => cell.views.find(v => v.guid === viewModel.guid));
assert.strictEqual(cellsWithNewView.length, 2);
assert.strictEqual(viewModel.cells.length, 2);
assert.strictEqual(viewModel.name, defaultViewName);
assert.strictEqual(viewModel.cards.length, 2, 'View model with no metadata was not initialized with the correct number of cards');
assert.strictEqual(viewModel.cells.length, 2, 'View model with no metadata was not initialized with the correct number of cells');
assert.strictEqual(viewModel.name, defaultViewName, 'View model with no metadata was not inirialized with the correct name');
});
test('rename', async function (): Promise<void> {
@@ -128,7 +124,7 @@ suite('NotebookViewModel', function (): void {
exceptionThrown = true;
}
assert.strictEqual(view.name, `${defaultViewName} 1`);
assert.strictEqual(view.name, `${defaultViewName} 1`, 'Rename did not result in expected name');
assert(!exceptionThrown);
});
@@ -146,74 +142,52 @@ suite('NotebookViewModel', function (): void {
exceptionThrown = true;
}
assert(exceptionThrown);
assert(exceptionThrown, 'Duplicating a view name should throw an exception');
});
test('hide cell', async function (): Promise<void> {
let notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
let viewModel = new NotebookViewModel(defaultViewName, notebookViews);
viewModel.initialize();
let viewModel = notebookViews.createNewView(defaultViewName);
let cellToHide = viewModel.cells[0];
viewModel.hideCell(cellToHide);
assert.strictEqual(viewModel.hiddenCells.length, 1);
assert(viewModel.hiddenCells.includes(cellToHide));
assert.strictEqual(viewModel.hiddenCells.length, 1, 'Hiding a cell should add it to hiddenCells');
assert(viewModel.hiddenCells.includes(cellToHide), 'Hiding a cell should add it to hiddenCells');
});
test('insert cell', async function (): Promise<void> {
let notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
let viewModel = new NotebookViewModel(defaultViewName, notebookViews);
viewModel.initialize();
let viewModel = notebookViews.createNewView(defaultViewName);
let cellToInsert = viewModel.cells[0];
viewModel.hideCell(cellToInsert);
assert(viewModel.hiddenCells.includes(cellToInsert));
assert(viewModel.hiddenCells.includes(cellToInsert), 'Expecting a hidden cell');
viewModel.insertCell(cellToInsert);
assert(!viewModel.hiddenCells.includes(cellToInsert));
assert(!viewModel.hiddenCells.includes(cellToInsert), 'Inserting a cell should remove it from hiddenCells');
});
test('move cell', async function (): Promise<void> {
test('move card', async function (): Promise<void> {
let notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
let viewModel = new NotebookViewModel(defaultViewName, notebookViews);
viewModel.initialize();
let viewModel = notebookViews.createNewView(defaultViewName);
let cellToMove = viewModel.cells[0];
viewModel.moveCard(viewModel.cards[0], 98, 99);
viewModel.moveCell(cellToMove, 98, 99);
let cellMeta = viewModel.getCellMetadata(cellToMove);
assert.strictEqual(cellMeta.x, 98);
assert.strictEqual(cellMeta.y, 99);
assert.strictEqual(viewModel.cards[0].x, 98, 'Card x position did not update on move');
assert.strictEqual(viewModel.cards[0].y, 99, 'Card y position did not update on move');
});
test('resize cell', async function (): Promise<void> {
test('resize card', async function (): Promise<void> {
let notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
let viewModel = new NotebookViewModel(defaultViewName, notebookViews);
viewModel.initialize();
let viewModel = notebookViews.createNewView(defaultViewName);
let cellToResize = viewModel.cells[0];
viewModel.resizeCard(viewModel.cards[0], 3, 4);
viewModel.resizeCell(cellToResize, 3, 4);
let cellMeta = viewModel.getCellMetadata(cellToResize);
assert.strictEqual(cellMeta.width, 3);
assert.strictEqual(cellMeta.height, 4);
});
test('get cell metadata', async function (): Promise<void> {
let notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
let viewModel = new NotebookViewModel(defaultViewName, notebookViews);
viewModel.initialize();
let cell = viewModel.cells[0];
let cellMeta = notebookViews.getExtensionCellMetadata(cell);
assert(!isUndefinedOrNull(cellMeta.views.find(v => v.guid === viewModel.guid)));
assert.deepStrictEqual(viewModel.getCellMetadata(cell), cellMeta.views.find(v => v.guid === viewModel.guid));
assert.strictEqual(viewModel.cards[0].width, 3, 'Card width did not update on resize');
assert.strictEqual(viewModel.cards[0].height, 4, 'Card height did not update on resize');
});
test('delete', async function (): Promise<void> {
@@ -278,6 +252,9 @@ suite('NotebookViewModel', function (): void {
await model.loadContents();
await model.requestModelLoad();
return new NotebookViewsExtension(model);
const notebookViews = new NotebookViewsExtension(model);
notebookViews.initialize();
return notebookViews;
}
});

View File

@@ -87,10 +87,8 @@ suite('NotebookViews', function (): void {
test('should not modify the notebook document until a view is created', async () => {
//Create some content
notebookViews.notebook.addCell(CellTypes.Code, 0);
const cell = notebookViews.notebook.cells[0];
assert.strictEqual(notebookViews.getExtensionMetadata(), undefined);
assert.strictEqual(notebookViews.getExtensionCellMetadata(cell), undefined);
//Check that the view is created
notebookViews.createNewView(defaultViewName);
@@ -101,11 +99,10 @@ suite('NotebookViews', function (): void {
assert.strictEqual(notebookViews.getViews().length, 0, 'notebook should not initially generate any views');
let newView = notebookViews.createNewView(defaultViewName);
let cellsWithMatchingGuid = newView.cells.filter(cell => newView.getCellMetadata(cell).guid === newView.guid);
assert.strictEqual(notebookViews.getViews().length, 1, 'only one view was created');
assert.strictEqual(newView.name, defaultViewName, 'view was not created with its given name');
assert.strictEqual(newView.cells.length, 2, 'view did not contain the same number of cells as the notebook used to create it');
assert.strictEqual(cellsWithMatchingGuid.length, newView.cells.length, 'cell metadata was not created for all cells in view');
});
test('remove view', async function (): Promise<void> {
@@ -113,10 +110,7 @@ suite('NotebookViews', function (): void {
notebookViews.removeView(newView.guid);
let cellsWithNewView = notebookViews.getCells().filter(cell => cell.views.find(v => v.guid === newView.guid));
assert.strictEqual(notebookViews.getViews().length, 0, 'view not removed from notebook metadata');
assert.strictEqual(cellsWithNewView.length, 0, 'view not removed from cells');
});
test('default view name', async function (): Promise<void> {
@@ -134,21 +128,20 @@ suite('NotebookViews', function (): void {
assert.strictEqual(notebookViews.getActiveView(), newView);
});
test('update cell', async function (): Promise<void> {
test('update card', async function (): Promise<void> {
let newView = notebookViews.createNewView();
let c1 = newView.cells[0];
let card = newView.cards[0];
let cellData = newView.getCellMetadata(c1);
cellData = { ...cellData, x: 0, y: 0, hidden: true, width: 0, height: 0 };
notebookViews.updateCell(c1, newView, cellData);
let cardData = { ...card, x: 0, y: 0, width: 0, height: 0 };
notebookViews.updateCard(card, cardData, newView);
cellData = { ...cellData, x: 1, y: 1, hidden: false, width: 1, height: 1 };
notebookViews.updateCell(c1, newView, cellData);
assert.deepStrictEqual(newView.getCellMetadata(c1), cellData, 'update did not set all values');
cardData = { ...cardData, x: 1, y: 1, width: 1, height: 1 };
notebookViews.updateCard(newView.cards[0], cardData, newView);
assert.deepStrictEqual(newView.cards[0], cardData, 'update did not set all values');
cellData = { ...cellData, x: 3 };
notebookViews.updateCell(c1, newView, { x: 3 });
assert.deepStrictEqual(newView.getCellMetadata(c1), cellData, 'update should only override set values');
cardData = { ...cardData, x: 3 };
notebookViews.updateCard(newView.cards[0], { x: 3 }, newView);
assert.deepStrictEqual(newView.cards[0], cardData, 'update should only override set values');
});
function setupServices() {

View File

@@ -20,7 +20,7 @@ import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/
import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor';
import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { INotebookView, INotebookViewCell, INotebookViewMetadata, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { INotebookView, INotebookViewCard, INotebookViewMetadata, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry';
import { INotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
@@ -799,6 +799,7 @@ export class NotebookViewStub implements INotebookView {
isNew: boolean;
name: string = '';
guid: string = '';
cards: INotebookViewCard[];
cells: readonly ICellModel[] = [];
hiddenCells: readonly ICellModel[];
displayedCells: readonly ICellModel[];
@@ -811,13 +812,16 @@ export class NotebookViewStub implements INotebookView {
nameAvailable(name: string): boolean {
throw new Error('Method not implemented.');
}
getCellMetadata(cell: ICellModel): INotebookViewCell {
getCellMetadata(cell: ICellModel): INotebookViewCard {
throw new Error('Method not implemented.');
}
hideCell(cell: ICellModel): void {
throw new Error('Method not implemented.');
}
moveCell(cell: ICellModel, x: number, y: number): void {
moveCard(card: INotebookViewCard, x: number, y: number): void {
throw new Error('Method not implemented.');
}
resizeCard(card: INotebookViewCard, width: number, height: number): void {
throw new Error('Method not implemented.');
}
resizeCell(cell: ICellModel, width: number, height: number): void {
@@ -832,7 +836,7 @@ export class NotebookViewStub implements INotebookView {
getCell(guid: string): Readonly<ICellModel> {
throw new Error('Method not implemented.');
}
insertCell(cell: ICellModel): void {
insertCell(cell: ICellModel): INotebookViewCard {
throw new Error('Method not implemented.');
}
save(): void {