diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.module.ts b/src/sql/workbench/contrib/notebook/browser/notebook.module.ts index 044ff2e6d1..03534ff13c 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.module.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.module.ts @@ -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(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, diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html index d93cdc2393..6d0b0e7e92 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html @@ -6,28 +6,39 @@ -->
-
+
- - - - + + + + + +
+
+ + + + +
+
+
+
+
+
+
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts index 377d443081..8e2fe4665a 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts @@ -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 = 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()}; + } + `); + } }); diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html index 9bd11d8562..bf7b5c0658 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html @@ -5,8 +5,8 @@ *--------------------------------------------------------------------------------------------*/ -->
- - + +
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts index a5ef038feb..a783279057 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts @@ -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; 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): 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); } }); } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css index d70346755a..7e83aac192 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css @@ -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; diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index 2d32530341..b8ece2d471 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -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 { 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 { 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 { @@ -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 { 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 { 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 { + test('move card', async function (): Promise { 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 { + test('resize card', async function (): Promise { 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 { - 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 { @@ -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; } }); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts index bf1c286007..21fadf00fb 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewsExtension.test.ts @@ -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 { @@ -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 { @@ -134,21 +128,20 @@ suite('NotebookViews', function (): void { assert.strictEqual(notebookViews.getActiveView(), newView); }); - test('update cell', async function (): Promise { + test('update card', async function (): Promise { 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() { diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 69545b4c25..6f3b8c61dd 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -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 { throw new Error('Method not implemented.'); } - insertCell(cell: ICellModel): void { + insertCell(cell: ICellModel): INotebookViewCard { throw new Error('Method not implemented.'); } save(): void { diff --git a/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts b/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts index c8ef461edf..51b7b77f7a 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts @@ -8,7 +8,7 @@ import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contr import { deepClone } from 'vs/base/common/objects'; export class NotebookExtension { - readonly version = 1; + readonly version: number = 1; readonly extensionNamespace = 'extensions'; private _extensionName: string; diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts index fe191e89d1..9608fc2452 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts @@ -6,8 +6,9 @@ import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/ import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { Emitter } from 'vs/base/common/event'; import { localize } from 'vs/nls'; -import { INotebookView, INotebookViewCell } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; +import { INotebookView, INotebookViewCard, INotebookViewCell, ViewsTabConfig } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; import { generateUuid } from 'vs/base/common/uuid'; +import { IconPath } from 'azdata'; export const DEFAULT_VIEW_CARD_HEIGHT = 4; export const DEFAULT_VIEW_CARD_WIDTH = 12; @@ -15,10 +16,42 @@ export const GRID_COLUMNS = 12; export class ViewNameTakenError extends Error { } -function cellCollides(c1: INotebookViewCell, c2: INotebookViewCell): boolean { +function cellCollides(c1: INotebookViewCard, c2: INotebookViewCard): boolean { return !((c1.y + c1.height <= c2.y) || (c1.x + c1.width <= c2.x) || (c1.x + c1.width <= c2.x) || (c2.x + c2.width <= c1.x)); } +export class ViewsTab implements ViewsTabConfig { + cell: INotebookViewCell; + title: string; + id?: string; + group: string; + icon?: IconPath; + cellModel: ICellModel; + + constructor(config: ViewsTabConfig, cell: ICellModel) { + this.fromJSON(config, cell); + } + + public toJSON() { + return { + cell: this.cell, + title: this.title, + id: this.id, + group: this.group, + icon: this.icon + }; + } + + public fromJSON(config: ViewsTabConfig, cell: ICellModel): void { + this.cell = config.cell; + this.title = config.title; + this.id = config.id; + this.group = config.group; + this.icon = config.icon; + this.cellModel = cell; + } +} + export class NotebookViewModel implements INotebookView { private _onDeleted = new Emitter(); private _onCellVisibilityChanged = new Emitter(); @@ -31,14 +64,22 @@ export class NotebookViewModel implements INotebookView { constructor( protected _name: string, private _notebookViews: NotebookViewsExtension, + private _cards: INotebookViewCard[] = [], guid?: string ) { this.guid = guid ?? generateUuid(); + + + this._cards.forEach(card => { + card.tabs.forEach(tab => { + tab.cellModel = this.cells.find(c => c.cellGuid === tab.cell.guid); + }); + }); } public static load(guid: string, notebookViews: NotebookViewsExtension): INotebookView { const view = notebookViews.getViews().find(v => v.guid === guid); - return new NotebookViewModel(view.name, notebookViews, view.guid); + return new NotebookViewModel(view.name, notebookViews, view.cards, view.guid); } public initialize(isNew?: boolean): void { @@ -46,29 +87,43 @@ export class NotebookViewModel implements INotebookView { this._isNew = isNew; } - const cells = this._notebookViews.notebook.cells; - cells.forEach((cell, idx) => { this.initializeCell(cell, idx); }); + /// Initialize cards + /// 0. Check that the cards object is created, or load them + /// 1. Create a card per cell + /// 2. Create a tab per cell + /// 3. Title the tab with a sequential number i.e., Untitled Tab {1,2,3..} + if (isNew) { + this.initializeCards(); + } } - protected initializeCell(cell: ICellModel, idx: number) { - let meta = this._notebookViews.getExtensionCellMetadata(cell); + public initializeCards() { + const cells = this._notebookViews.notebook.cells; - if (!meta) { - this._notebookViews.initializeCell(cell); - meta = this._notebookViews.getExtensionCellMetadata(cell); - } - - // Ensure that we are not duplicting view entries in cell metadata - if (!meta.views.find(v => v.guid === this.guid)) { - meta.views.push({ - guid: this.guid, - hidden: false, + let card: INotebookViewCard; + cells.forEach((cell, idx) => { + card = { + guid: generateUuid(), y: idx * DEFAULT_VIEW_CARD_HEIGHT, x: 0, width: DEFAULT_VIEW_CARD_WIDTH, - height: DEFAULT_VIEW_CARD_HEIGHT - }); + height: DEFAULT_VIEW_CARD_HEIGHT, + tabs: [] + }; + + this.createTab(cell, card); + + this._cards.push(card); + }); + } + + protected createTab(cell: ICellModel, card: INotebookViewCard): void { + if (card === undefined) { + throw new Error('A card must be specified to create a tab'); } + + const newTab: ViewsTabConfig = { title: localize('Untitled', 'Untitled'), id: generateUuid(), group: card.guid, cell: { guid: cell.cellGuid } }; + card.tabs.push(new ViewsTab(newTab, cell)); } public cellInitialized(cell: ICellModel): boolean { @@ -90,13 +145,18 @@ export class NotebookViewModel implements INotebookView { return !this._notebookViews.viewNameIsTaken(name); } - public getCellMetadata(cell: ICellModel): INotebookViewCell { + public getCellMetadata(cell: ICellModel): INotebookViewCard { const meta = this._notebookViews.getExtensionCellMetadata(cell); return meta?.views?.find(view => view.guid === this.guid); } public get hiddenCells(): Readonly { - return this.cells.filter(cell => this.getCellMetadata(cell)?.hidden !== false); + const allTabs = this.cards.flatMap(card => card.tabs); + return this.cells.filter(cell => !allTabs.find(t => t.cell.guid === cell.cellGuid)); + } + + public get cards(): INotebookViewCard[] { + return this._cards; } public get cells(): Readonly { @@ -111,30 +171,69 @@ export class NotebookViewModel implements INotebookView { return this._notebookViews.notebook.cells.find(cell => cell.cellGuid === guid); } - public updateCell(cell: ICellModel, currentView: INotebookView, cellData: INotebookViewCell, override: boolean = false) { - if (!this.cellInitialized(cell)) { - this.initializeCell(cell, 0); - } - + public updateCell(cell: ICellModel, currentView: INotebookView, cellData: INotebookViewCard, override: boolean = false) { this._notebookViews.updateCell(cell, currentView, cellData, override); } - public insertCell(cell: ICellModel) { - this.updateCell(cell, this, { hidden: false }); + public insertCell(cell: ICellModel): INotebookViewCard { + let card: INotebookViewCard = { + guid: generateUuid(), + y: 0, + x: 0, + width: DEFAULT_VIEW_CARD_WIDTH, + height: DEFAULT_VIEW_CARD_HEIGHT, + tabs: [] + }; + + this.createTab(cell, card); + + this._cards.push(card); + this._onCellVisibilityChanged.fire(cell); + + this.save(); + + return card; } public hideCell(cell: ICellModel) { - this.updateCell(cell, this, { hidden: true }); + this.cards.forEach((card) => { + const updatedTabs = card.tabs.filter(t => t.cell.guid !== cell.cellGuid); + this._notebookViews.updateCard(card, { tabs: updatedTabs }, this); + + // If there are no tabs left in the card, delete the card + if (!updatedTabs.length) { + const index = this.cards.findIndex(c => c.guid === card.guid); + const removedCard = this._cards.splice(index, 1); + if (removedCard.length === 1) { + this.compactCells(); + this.save(); + } + } + }); this._onCellVisibilityChanged.fire(cell); } - public moveCell(cell: ICellModel, x: number, y: number) { - this.updateCell(cell, this, { x, y }); + public moveCard(card: INotebookViewCard, x: number, y: number) { + this._notebookViews.updateCard(card, { x, y }, this); + } + + public resizeCard(card: INotebookViewCard, width?: number, height?: number) { + let data: INotebookViewCard = {}; + + if (width) { + data.width = width; + } + + if (height) { + data.height = height; + } + + this._notebookViews.updateCard(card, data, this); } public resizeCell(cell: ICellModel, width?: number, height?: number) { - let data: INotebookViewCell = {}; + let data: INotebookViewCard = {}; if (width) { data.width = width; @@ -153,22 +252,21 @@ export class NotebookViewModel implements INotebookView { } public compactCells() { - let cellsPlaced: INotebookViewCell[] = []; + let cardsPlaced: INotebookViewCard[] = []; - this.displayedCells.forEach((cell: ICellModel) => { - const c1 = this.getCellMetadata(cell); + this.cards.forEach((card: INotebookViewCard) => { for (let i = 0; ; i++) { const row = i % GRID_COLUMNS; const column = Math.floor(i / GRID_COLUMNS); - if (row + c1.width > GRID_COLUMNS) { + if (row + card.width > GRID_COLUMNS) { continue; } - if (!cellsPlaced.find((c2) => cellCollides(c2, { ...c1, x: row, y: column }))) { - this._notebookViews.updateCell(cell, this, { x: row, y: column }); - cellsPlaced.push({ ...c1, x: row, y: column }); + if (!cardsPlaced.find((c2) => cellCollides(c2, { ...card, x: row, y: column }))) { + this._notebookViews.updateCard(card, { x: row, y: column }, this); + cardsPlaced.push({ ...card, x: row, y: column }); break; } } @@ -193,6 +291,6 @@ export class NotebookViewModel implements INotebookView { } public toJSON() { - return { guid: this.guid, name: this._name } as NotebookViewModel; + return { guid: this.guid, name: this._name, cards: this.cards } as INotebookView; } } diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts index 1861e78e08..b2990c5a1b 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts @@ -3,7 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TabConfig } from 'sql/workbench/browser/modelComponents/tabbedPanel.component'; import { ICellModel, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { ViewsTab } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel'; +import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { Event } from 'vs/base/common/event'; export type CellChangeEventType = 'hide' | 'insert' | 'active' | 'execution' | 'update'; @@ -13,6 +16,13 @@ export type CellChangeEvent = { event: CellChangeEventType }; +export interface INotebookViewsExtensionUpgrade { + readonly sourceVersion: number; + readonly targetVersion: number; + versionCheck(version: number): boolean; + apply(extension: NotebookViewsExtension): void; +} + export interface INotebookViews { onActiveViewChanged: Event; createNewView(name?: string): INotebookView; @@ -29,19 +39,21 @@ export interface INotebookView { readonly onCellVisibilityChanged: Event; isNew: boolean; + cards: INotebookViewCard[]; cells: Readonly; hiddenCells: Readonly; displayedCells: Readonly; name: string; initialize(isNew?: boolean): void; nameAvailable(name: string): boolean; - getCellMetadata(cell: ICellModel): INotebookViewCell; + getCellMetadata(cell: ICellModel): INotebookViewCard; hideCell(cell: ICellModel): void; - moveCell(cell: ICellModel, x: number, y: number): void; + moveCard(card: INotebookViewCard, x: number, y: number): void; compactCells(); + resizeCard(card: INotebookViewCard, width: number, height: number): void; resizeCell(cell: ICellModel, width: number, height: number): void; getCell(guid: string): Readonly; - insertCell(cell: ICellModel): void; + insertCell(cell: ICellModel): INotebookViewCard; markAsViewed(): void; save(): void; delete(): void; @@ -49,11 +61,22 @@ export interface INotebookView { export interface INotebookViewCell { readonly guid?: string; +} + + +export interface INotebookViewCard { + readonly guid?: string; hidden?: boolean; x?: number; y?: number; width?: number; height?: number; + tabs?: ViewsTab[]; + activeTab?: ViewsTab; +} + +interface ViewsTabConfig extends TabConfig { + cell: INotebookViewCell } /* @@ -71,7 +94,7 @@ export interface INotebookViewMetadata { * view at the cell level. */ export interface INotebookViewCellMetadata { - views: INotebookViewCell[]; + views: INotebookViewCard[]; } export interface INotebookViews { diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts index b188626d3e..6a71365f33 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts @@ -9,14 +9,20 @@ import { Emitter, Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { NotebookViewModel } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel'; import { NotebookExtension } from 'sql/workbench/services/notebook/browser/models/notebookExtension'; -import { INotebookView, INotebookViewCell, INotebookViewCellMetadata, INotebookViewMetadata, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; +import { INotebookView, INotebookViewCard, INotebookViewCellMetadata, INotebookViewMetadata, INotebookViews, INotebookViewsExtensionUpgrade } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; +import { NotebookViewsUpgrades } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsUpgrades'; + + export class NotebookViewsExtension extends NotebookExtension implements INotebookViews { static readonly defaultViewName = localize('notebookView.untitledView', "Untitled View"); static readonly extension = 'notebookviews'; + static readonly upgrades: Array = [ + new NotebookViewsUpgrades.V1ToV2NotebookViewsExtensionUpgrade() + ]; readonly maxNameIterationAttempts = 100; - override readonly version = 1; + override readonly version: number = 2; protected _metadata: INotebookViewMetadata | undefined; private _initialized: boolean = false; @@ -32,6 +38,13 @@ export class NotebookViewsExtension extends NotebookExtension { + if (upgrade.versionCheck(this._metadata.version)) { + upgrade.apply(this); + this._metadata = this.getExtensionMetadata(); + } + }); + this._metadata.views = this._metadata.views.map(view => NotebookViewModel.load(view.guid, this)); this._initialized = true; } @@ -94,16 +107,7 @@ export class NotebookViewsExtension extends NotebookExtension view.guid === guid); if (viewToRemove >= 0) { - let removedView = this._metadata?.views.splice(viewToRemove, 1); - - // Remove view data for each cell - if (removedView.length === 1) { - this._notebook?.cells.forEach((cell) => { - let meta = this.getExtensionCellMetadata(cell); - meta.views = meta.views.filter(x => x.guid !== removedView[0].guid); - this.setExtensionCellMetadata(cell, meta); - }); - } + this._metadata?.views.splice(viewToRemove, 1); } if (guid === this._metadata?.activeView) { @@ -127,7 +131,7 @@ export class NotebookViewsExtension extends NotebookExtension view.guid === currentView.guid); @@ -139,6 +143,22 @@ export class NotebookViewsExtension extends NotebookExtension view.guid === currentView.guid); + + if (viewToUpdate >= 0) { + const cardToUpdate = notebookMetadata.views[viewToUpdate].cards.findIndex(c => c.guid === card.guid); + + if (cardToUpdate >= 0) { + notebookMetadata.views[viewToUpdate].cards[cardToUpdate] = override ? cardData : { ...notebookMetadata.views[viewToUpdate].cards[cardToUpdate], ...cardData }; + this.setExtensionMetadata(this._notebook, notebookMetadata); + } + } + } + } + public get notebook(): INotebookModel { return this._notebook; } diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsUpgrades.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsUpgrades.ts new file mode 100644 index 0000000000..f1cd6028a8 --- /dev/null +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsUpgrades.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; +import { generateUuid } from 'vs/base/common/uuid'; + +export namespace NotebookViewsUpgrades { + export class V1ToV2NotebookViewsExtensionUpgrade { + sourceVersion = 1; + targetVersion = 2; + + versionCheck(version: number): boolean { + return version > this.targetVersion; + } + + apply(notebookViewsExtension: NotebookViewsExtension): void { + const extensions = notebookViewsExtension.notebook.getMetaValue('extensions'); + const notebookviews = extensions['notebookviews']; + const views = notebookviews['views']; + + const newmeta = { + version: 2, + activeView: null, + views: [] + }; + + views.forEach((view, viewIdx) => { + const viewData = { + guid: view.guid, + name: view.name, + cards: [] + }; + + const cells = notebookViewsExtension.notebook.cells; + cells.forEach((cell) => { + const cellmeta = cell.metadata['extensions']?.['notebookviews']?.['views']?.[viewIdx]; + if (cellmeta && !cellmeta?.hidden) { + const card = { + guid: generateUuid(), + y: cellmeta.y, + x: cellmeta.x, + width: cellmeta.width, + height: cellmeta.height, + tabs: [{ + title: 'Untitled', + guid: generateUuid(), + cell: { + guid: cell.cellGuid + } + }] + }; + + viewData.cards.push(card); + } + }); + + newmeta.views.push(viewData); + }); + + notebookViewsExtension.setExtensionMetadata(notebookViewsExtension.notebook, newmeta); + } + } +}