Prettier 2.0 (#393)

This commit is contained in:
Steve Faulkner
2021-01-20 09:15:01 -06:00
committed by GitHub
parent c1937ca464
commit 4be53284b5
500 changed files with 41927 additions and 41838 deletions

View File

@@ -1,26 +1,26 @@
abstract class CacheBase<T> {
public data: T[] | null;
public sortOrder: any;
public serverCallInProgress: boolean;
constructor() {
this.data = null;
this.sortOrder = null;
this.serverCallInProgress = false;
}
public get length(): number {
return this.data ? this.data.length : 0;
}
public clear() {
this.preClear();
this.data = null;
this.sortOrder = null;
this.serverCallInProgress = false;
}
protected abstract preClear(): void;
}
export default CacheBase;
abstract class CacheBase<T> {
public data: T[] | null;
public sortOrder: any;
public serverCallInProgress: boolean;
constructor() {
this.data = null;
this.sortOrder = null;
this.serverCallInProgress = false;
}
public get length(): number {
return this.data ? this.data.length : 0;
}
public clear() {
this.preClear();
this.data = null;
this.sortOrder = null;
this.serverCallInProgress = false;
}
protected abstract preClear(): void;
}
export default CacheBase;

View File

@@ -1,411 +1,396 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataTableBuilder from "./DataTableBuilder";
import DataTableOperationManager from "./DataTableOperationManager";
import * as DataTableOperations from "./DataTableOperations";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/**
* Custom binding manager of datatable
*/
var tableEntityListViewModelMap: {
[key: string]: {
tableViewModel: TableEntityListViewModel;
operationManager: DataTableOperationManager;
$dataTable: JQuery;
};
} = {};
function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
var tableEntityListViewModel = bindingContext.$data;
tableEntityListViewModel.notifyColumnChanges = onTableColumnChange;
var $dataTable = $(element);
var queryTablesTab = bindingContext.$parent;
var operationManager = new DataTableOperationManager(
$dataTable,
tableEntityListViewModel,
queryTablesTab.tableCommands
);
tableEntityListViewModelMap[queryTablesTab.tabId] = {
tableViewModel: tableEntityListViewModel,
operationManager: operationManager,
$dataTable: $dataTable
};
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
$(window).resize(updateTableScrollableRegionMetrics);
operationManager.focusTable(); // Also selects the first row if needed.
}
function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
var columnsFilter: boolean[] = null;
var tableEntityListViewModel = tableEntityListViewModelMap[queryTablesTab.tabId].tableViewModel;
if (queryTablesTab.queryViewModel()) {
queryTablesTab
.queryViewModel()
.queryBuilderViewModel()
.updateColumnOptions();
}
createDataTable(
tableEntityListViewModel.tablePageStartIndex,
tableEntityListViewModel,
queryTablesTab,
true,
columnsFilter
);
}
function createDataTable(
startIndex: number,
tableEntityListViewModel: TableEntityListViewModel,
queryTablesTab: QueryTablesTab,
destroy: boolean = false,
columnsFilter: boolean[] = null
): void {
var $dataTable = tableEntityListViewModelMap[queryTablesTab.tabId].$dataTable;
if (destroy) {
// Find currently displayed columns.
var currentColumns: string[] = tableEntityListViewModel.headers;
// Calculate how many more columns need to added to the current table.
var columnsToAdd: number = _.difference(tableEntityListViewModel.headers, currentColumns).length;
// This is needed as current solution of adding column is more like a workaround
// The official support for dynamically add column is not yet there
// Please track github issue https://github.com/DataTables/DataTables/issues/273 for its offical support
for (var i = 0; i < columnsToAdd; i++) {
$(".dataTables_scrollHead table thead tr th")
.eq(0)
.after("<th></th>");
}
tableEntityListViewModel.table.destroy();
$dataTable.empty();
}
var jsonColTable = [];
for (var i = 0; i < tableEntityListViewModel.headers.length; i++) {
jsonColTable.push({
sTitle: tableEntityListViewModel.headers[i],
data: tableEntityListViewModel.headers[i],
aTargets: [i],
mRender: bindColumn,
visible: !!columnsFilter ? columnsFilter[i] : true
});
}
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTables.Settings>{
// WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText)
// so that they don't get interpreted as HTML in our page.
colReorder: true,
aoColumnDefs: jsonColTable,
stateSave: false,
dom: "RZlfrtip",
oColReorder: {
iFixedColumns: 1
},
displayStart: startIndex,
bPaginate: true,
pagingType: "full_numbers",
bProcessing: true,
oLanguage: {
sInfo: "Results _START_ - _END_ of _TOTAL_",
oPaginate: {
sFirst: "<<",
sNext: ">",
sPrevious: "<",
sLast: ">>"
},
sProcessing: '<img style="width: 28px; height: 6px; " src="images/LoadingIndicator_3Squares.gif">',
oAria: {
sSortAscending: "",
sSortDescending: ""
}
},
destroy: destroy,
bInfo: true,
bLength: false,
bLengthChange: false,
scrollX: true,
scrollCollapse: true,
iDisplayLength: 100,
serverSide: true,
ajax: queryTablesTab.tabId, // Using this settings to make sure for getServerData we update the table based on the appropriate tab
fnServerData: getServerData,
fnRowCallback: bindClientId,
fnInitComplete: initializeTable,
fnDrawCallback: updateSelectionStatus
});
(tableEntityListViewModel.table.table(0).container() as Element)
.querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector)
.forEach(table => {
table.setAttribute(
"summary",
`Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`
);
});
(tableEntityListViewModel.table.table(0).container() as Element)
.querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector)
.forEach(table => {
table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`);
});
}
function bindColumn(data: any, type: string, full: any) {
var displayedValue: any = null;
if (data) {
displayedValue = data._;
// SECURITY: Make sure we don't allow cross-site scripting by interpreting the values as HTML
displayedValue = Utilities.htmlEncode(displayedValue);
// Css' empty psuedo class can only tell the difference of whether a cell has values.
// A cell has no values no matter it's empty or it has no such a property.
// To distinguish between an empty cell and a non-existing property cell,
// we add a whitespace to the empty cell so that css will treat it as a cell with values.
if (displayedValue === "" && data.$ === Constants.TableType.String) {
displayedValue = " ";
}
}
return displayedValue;
}
function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
tableEntityListViewModelMap[oSettings.ajax].tableViewModel.renderNextPageAndupdateCache(
sSource,
aoData,
fnCallback,
oSettings
);
}
/**
* Bind table data information to row element so that we can track back to the table data
* from UI elements.
*/
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
return nRow;
}
function selectionChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
$(".dataTable tr.selected")
.attr("tabindex", "-1")
.removeClass("selected");
const selected =
bindingContext && bindingContext.$data && bindingContext.$data.selected && bindingContext.$data.selected();
selected &&
selected.forEach((b: Entities.ITableEntity) => {
var sel = DataTableOperations.getRowSelector([
{
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
value: b.RowKey && b.RowKey._ && b.RowKey._.toString()
}
]);
$(sel)
.attr("tabindex", "0")
.focus()
.addClass("selected");
});
//selected = bindingContext.$data.selected();
}
function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
// do nothing for now
}
function initializeTable(): void {
updateTableScrollableRegionMetrics();
initializeEventHandlers();
}
function updateTableScrollableRegionMetrics(): void {
updateTableScrollableRegionHeight();
updateTableScrollableRegionWidth();
}
/*
* Update the table's scrollable region height. So the pagination control is always shown at the bottom of the page.
*/
function updateTableScrollableRegionHeight(): void {
$(".tab-pane").each(function(index, tabElement) {
if (!$(tabElement).hasClass("tableContainer")) {
return;
}
// Add some padding to the table so it doesn't get too close to the container border.
var dataTablePaddingBottom = 10;
var bodyHeight = $(window).height();
var dataTablesScrollBodyPosY = $(tabElement)
.find(Constants.htmlSelectors.dataTableScrollBodySelector)
.offset().top;
var dataTablesInfoElem = $(tabElement).find(".dataTables_info");
var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate");
const explorer = window.dataExplorer;
const notificationConsoleHeight = explorer.isNotificationConsoleExpanded()
? 252 /** 32px(header) + 220px(content height) **/
: 32 /** Header height **/;
var scrollHeight =
bodyHeight -
dataTablesScrollBodyPosY -
dataTablesPaginateElem.outerHeight(true) -
dataTablePaddingBottom -
notificationConsoleHeight;
//info and paginate control are stacked
if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) {
scrollHeight -= dataTablesInfoElem.outerHeight(true);
}
// TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue)
// in the current version of JQuery we are using. Ideally, we would upgrade JQuery and use this line instead:
// $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight);
var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0];
var style = getComputedStyle(element);
var actualHeight = parseInt(style.height);
var change = element.offsetHeight - scrollHeight;
$(tabElement)
.find(Constants.htmlSelectors.dataTableScrollBodySelector)
.height(actualHeight - change);
});
}
/*
* Update the table's scrollable region width to make efficient use of the remaining space.
*/
function updateTableScrollableRegionWidth(): void {
$(".tab-pane").each(function(index, tabElement) {
if (!$(tabElement).hasClass("tableContainer")) {
return;
}
var bodyWidth = $(window).width();
var dataTablesScrollBodyPosLeft = $(tabElement)
.find(Constants.htmlSelectors.dataTableScrollBodySelector)
.offset().left;
var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft;
// jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature.
// We work around that by setting the height for it's container instead.
$(tabElement)
.find(Constants.htmlSelectors.dataTableScrollContainerSelector)
.width(scrollWidth);
});
}
function initializeEventHandlers(): void {
var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector);
var $firstHeader: JQuery = $headers.first();
var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
$headers
.on("keydown", (event: JQueryEventObject) => {
Utilities.onEnter(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive);
});
// Bind shift+tab from first header back to search input field
Utilities.onTab(
event,
($sourceElement: JQuery) => {
var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
if (sourceIndex === firstIndex) {
event.preventDefault();
}
},
/* metaKey */ null,
/* shiftKey */ true,
/* altKey */ null
);
// Also reset color if [shift-] tabbing away from button while holding down 'enter'
Utilities.onTab(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", "");
});
})
.on("keyup", (event: JQueryEventObject) => {
Utilities.onEnter(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", "");
});
});
}
function updateSelectionStatus(oSettings: any): void {
var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
if ($dataTableRows) {
for (var i = 0; i < $dataTableRows.length; i++) {
var $row: JQuery = $dataTableRows.eq(i);
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
$row.attr("tabindex", "0");
}
}
}
updateDataTableFocus(oSettings.ajax);
DataTableOperations.setPaginationButtonEventHandlers();
}
// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
function updateDataTableFocus(queryTablesTabId: string): void {
var $activeElement: JQuery = $(document.activeElement);
var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
var storageExplorerFrameHasFocus: boolean = document.hasFocus();
var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;
if (operationManager) {
if (isFocusLost && storageExplorerFrameHasFocus) {
// We get here when no control is active, meaning that the table update was triggered
// from a dialog, the context menu or by clicking on a toolbar control or header.
// Note that giving focus to the table also selects the first row if needed.
// The document.hasFocus() ensures that the table will only get focus when the
// focus was lost (i.e. "body has the focus") within the Storage Explorer frame
// i.e. not when the focus is lost because it is in another frame
// e.g. a daytona dialog or in the Activity Log.
operationManager.focusTable();
}
if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) {
// If table header is selected, focus is shifted to the selected element as part of accessibility
$activeElement && $activeElement.focus();
} else {
// If some control is active, we don't give focus back to the table,
// just select the first row if needed (empty selection).
operationManager.selectFirstIfNeeded();
}
}
}
(<any>ko.bindingHandlers).tableSource = {
init: bindDataTable,
update: dataChanged
};
(<any>ko.bindingHandlers).tableSelection = {
update: selectionChanged
};
(<any>ko.bindingHandlers).readOnly = {
update: function(element: any, valueAccessor: any) {
var value = ko.utils.unwrapObservable(valueAccessor());
if (value) {
element.setAttribute("readOnly", true);
} else {
element.removeAttribute("readOnly");
}
}
};
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataTableBuilder from "./DataTableBuilder";
import DataTableOperationManager from "./DataTableOperationManager";
import * as DataTableOperations from "./DataTableOperations";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/**
* Custom binding manager of datatable
*/
var tableEntityListViewModelMap: {
[key: string]: {
tableViewModel: TableEntityListViewModel;
operationManager: DataTableOperationManager;
$dataTable: JQuery;
};
} = {};
function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
var tableEntityListViewModel = bindingContext.$data;
tableEntityListViewModel.notifyColumnChanges = onTableColumnChange;
var $dataTable = $(element);
var queryTablesTab = bindingContext.$parent;
var operationManager = new DataTableOperationManager(
$dataTable,
tableEntityListViewModel,
queryTablesTab.tableCommands
);
tableEntityListViewModelMap[queryTablesTab.tabId] = {
tableViewModel: tableEntityListViewModel,
operationManager: operationManager,
$dataTable: $dataTable,
};
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
$(window).resize(updateTableScrollableRegionMetrics);
operationManager.focusTable(); // Also selects the first row if needed.
}
function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
var columnsFilter: boolean[] = null;
var tableEntityListViewModel = tableEntityListViewModelMap[queryTablesTab.tabId].tableViewModel;
if (queryTablesTab.queryViewModel()) {
queryTablesTab.queryViewModel().queryBuilderViewModel().updateColumnOptions();
}
createDataTable(
tableEntityListViewModel.tablePageStartIndex,
tableEntityListViewModel,
queryTablesTab,
true,
columnsFilter
);
}
function createDataTable(
startIndex: number,
tableEntityListViewModel: TableEntityListViewModel,
queryTablesTab: QueryTablesTab,
destroy: boolean = false,
columnsFilter: boolean[] = null
): void {
var $dataTable = tableEntityListViewModelMap[queryTablesTab.tabId].$dataTable;
if (destroy) {
// Find currently displayed columns.
var currentColumns: string[] = tableEntityListViewModel.headers;
// Calculate how many more columns need to added to the current table.
var columnsToAdd: number = _.difference(tableEntityListViewModel.headers, currentColumns).length;
// This is needed as current solution of adding column is more like a workaround
// The official support for dynamically add column is not yet there
// Please track github issue https://github.com/DataTables/DataTables/issues/273 for its offical support
for (var i = 0; i < columnsToAdd; i++) {
$(".dataTables_scrollHead table thead tr th").eq(0).after("<th></th>");
}
tableEntityListViewModel.table.destroy();
$dataTable.empty();
}
var jsonColTable = [];
for (var i = 0; i < tableEntityListViewModel.headers.length; i++) {
jsonColTable.push({
sTitle: tableEntityListViewModel.headers[i],
data: tableEntityListViewModel.headers[i],
aTargets: [i],
mRender: bindColumn,
visible: !!columnsFilter ? columnsFilter[i] : true,
});
}
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTables.Settings>{
// WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText)
// so that they don't get interpreted as HTML in our page.
colReorder: true,
aoColumnDefs: jsonColTable,
stateSave: false,
dom: "RZlfrtip",
oColReorder: {
iFixedColumns: 1,
},
displayStart: startIndex,
bPaginate: true,
pagingType: "full_numbers",
bProcessing: true,
oLanguage: {
sInfo: "Results _START_ - _END_ of _TOTAL_",
oPaginate: {
sFirst: "<<",
sNext: ">",
sPrevious: "<",
sLast: ">>",
},
sProcessing: '<img style="width: 28px; height: 6px; " src="images/LoadingIndicator_3Squares.gif">',
oAria: {
sSortAscending: "",
sSortDescending: "",
},
},
destroy: destroy,
bInfo: true,
bLength: false,
bLengthChange: false,
scrollX: true,
scrollCollapse: true,
iDisplayLength: 100,
serverSide: true,
ajax: queryTablesTab.tabId, // Using this settings to make sure for getServerData we update the table based on the appropriate tab
fnServerData: getServerData,
fnRowCallback: bindClientId,
fnInitComplete: initializeTable,
fnDrawCallback: updateSelectionStatus,
});
(tableEntityListViewModel.table.table(0).container() as Element)
.querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector)
.forEach((table) => {
table.setAttribute(
"summary",
`Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`
);
});
(tableEntityListViewModel.table.table(0).container() as Element)
.querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector)
.forEach((table) => {
table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`);
});
}
function bindColumn(data: any, type: string, full: any) {
var displayedValue: any = null;
if (data) {
displayedValue = data._;
// SECURITY: Make sure we don't allow cross-site scripting by interpreting the values as HTML
displayedValue = Utilities.htmlEncode(displayedValue);
// Css' empty psuedo class can only tell the difference of whether a cell has values.
// A cell has no values no matter it's empty or it has no such a property.
// To distinguish between an empty cell and a non-existing property cell,
// we add a whitespace to the empty cell so that css will treat it as a cell with values.
if (displayedValue === "" && data.$ === Constants.TableType.String) {
displayedValue = " ";
}
}
return displayedValue;
}
function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
tableEntityListViewModelMap[oSettings.ajax].tableViewModel.renderNextPageAndupdateCache(
sSource,
aoData,
fnCallback,
oSettings
);
}
/**
* Bind table data information to row element so that we can track back to the table data
* from UI elements.
*/
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
return nRow;
}
function selectionChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
$(".dataTable tr.selected").attr("tabindex", "-1").removeClass("selected");
const selected =
bindingContext && bindingContext.$data && bindingContext.$data.selected && bindingContext.$data.selected();
selected &&
selected.forEach((b: Entities.ITableEntity) => {
var sel = DataTableOperations.getRowSelector([
{
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
},
]);
$(sel).attr("tabindex", "0").focus().addClass("selected");
});
//selected = bindingContext.$data.selected();
}
function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
// do nothing for now
}
function initializeTable(): void {
updateTableScrollableRegionMetrics();
initializeEventHandlers();
}
function updateTableScrollableRegionMetrics(): void {
updateTableScrollableRegionHeight();
updateTableScrollableRegionWidth();
}
/*
* Update the table's scrollable region height. So the pagination control is always shown at the bottom of the page.
*/
function updateTableScrollableRegionHeight(): void {
$(".tab-pane").each(function (index, tabElement) {
if (!$(tabElement).hasClass("tableContainer")) {
return;
}
// Add some padding to the table so it doesn't get too close to the container border.
var dataTablePaddingBottom = 10;
var bodyHeight = $(window).height();
var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top;
var dataTablesInfoElem = $(tabElement).find(".dataTables_info");
var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate");
const explorer = window.dataExplorer;
const notificationConsoleHeight = explorer.isNotificationConsoleExpanded()
? 252 /** 32px(header) + 220px(content height) **/
: 32; /** Header height **/
var scrollHeight =
bodyHeight -
dataTablesScrollBodyPosY -
dataTablesPaginateElem.outerHeight(true) -
dataTablePaddingBottom -
notificationConsoleHeight;
//info and paginate control are stacked
if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) {
scrollHeight -= dataTablesInfoElem.outerHeight(true);
}
// TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue)
// in the current version of JQuery we are using. Ideally, we would upgrade JQuery and use this line instead:
// $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight);
var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0];
var style = getComputedStyle(element);
var actualHeight = parseInt(style.height);
var change = element.offsetHeight - scrollHeight;
$(tabElement)
.find(Constants.htmlSelectors.dataTableScrollBodySelector)
.height(actualHeight - change);
});
}
/*
* Update the table's scrollable region width to make efficient use of the remaining space.
*/
function updateTableScrollableRegionWidth(): void {
$(".tab-pane").each(function (index, tabElement) {
if (!$(tabElement).hasClass("tableContainer")) {
return;
}
var bodyWidth = $(window).width();
var dataTablesScrollBodyPosLeft = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset()
.left;
var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft;
// jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature.
// We work around that by setting the height for it's container instead.
$(tabElement).find(Constants.htmlSelectors.dataTableScrollContainerSelector).width(scrollWidth);
});
}
function initializeEventHandlers(): void {
var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector);
var $firstHeader: JQuery = $headers.first();
var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
$headers
.on("keydown", (event: JQueryEventObject) => {
Utilities.onEnter(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive);
});
// Bind shift+tab from first header back to search input field
Utilities.onTab(
event,
($sourceElement: JQuery) => {
var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
if (sourceIndex === firstIndex) {
event.preventDefault();
}
},
/* metaKey */ null,
/* shiftKey */ true,
/* altKey */ null
);
// Also reset color if [shift-] tabbing away from button while holding down 'enter'
Utilities.onTab(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", "");
});
})
.on("keyup", (event: JQueryEventObject) => {
Utilities.onEnter(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", "");
});
});
}
function updateSelectionStatus(oSettings: any): void {
var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
if ($dataTableRows) {
for (var i = 0; i < $dataTableRows.length; i++) {
var $row: JQuery = $dataTableRows.eq(i);
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
$row.attr("tabindex", "0");
}
}
}
updateDataTableFocus(oSettings.ajax);
DataTableOperations.setPaginationButtonEventHandlers();
}
// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
function updateDataTableFocus(queryTablesTabId: string): void {
var $activeElement: JQuery = $(document.activeElement);
var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
var storageExplorerFrameHasFocus: boolean = document.hasFocus();
var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;
if (operationManager) {
if (isFocusLost && storageExplorerFrameHasFocus) {
// We get here when no control is active, meaning that the table update was triggered
// from a dialog, the context menu or by clicking on a toolbar control or header.
// Note that giving focus to the table also selects the first row if needed.
// The document.hasFocus() ensures that the table will only get focus when the
// focus was lost (i.e. "body has the focus") within the Storage Explorer frame
// i.e. not when the focus is lost because it is in another frame
// e.g. a daytona dialog or in the Activity Log.
operationManager.focusTable();
}
if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) {
// If table header is selected, focus is shifted to the selected element as part of accessibility
$activeElement && $activeElement.focus();
} else {
// If some control is active, we don't give focus back to the table,
// just select the first row if needed (empty selection).
operationManager.selectFirstIfNeeded();
}
}
}
(<any>ko.bindingHandlers).tableSource = {
init: bindDataTable,
update: dataChanged,
};
(<any>ko.bindingHandlers).tableSelection = {
update: selectionChanged,
};
(<any>ko.bindingHandlers).readOnly = {
update: function (element: any, valueAccessor: any) {
var value = ko.utils.unwrapObservable(valueAccessor());
if (value) {
element.setAttribute("readOnly", true);
} else {
element.removeAttribute("readOnly");
}
},
};

View File

@@ -1,52 +1,52 @@
import * as Utilities from "../Utilities";
/**
* Wrapper function for creating data tables. Call this method, not the
* data tables constructor when you want to create a data table. This
* function makes sure that content without a render function is properly
* encoded to prevent XSS.
* @param{$dataTableElem} JQuery data table element
* @param{$settings} Settings to use when creating the data table
*/
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
return $dataTableElem.DataTable(applyDefaultRendering(settings));
}
/**
* Go through the settings for a data table and apply a simple HTML encode to any column
* without a render function to prevent XSS.
* @param{settings} The settings to check
* @return The given settings with all columns having a rendering function
*/
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
var tableColumns: DataTables.ColumnLegacy[] = null;
if (settings.aoColumns) {
tableColumns = settings.aoColumns;
} else if (settings.aoColumnDefs) {
// for tables we use aoColumnDefs instead of aoColumns
tableColumns = settings.aoColumnDefs;
}
// either the settings had no columns defined, or they were called
// by a property name which we have not used before
if (!tableColumns) {
return settings;
}
for (var i = 0; i < tableColumns.length; i++) {
// the column does not have a render function
if (!tableColumns[i].mRender) {
tableColumns[i].mRender = defaultDataRender;
}
}
return settings;
}
/**
* Default data render function, whatever is done to data in here
* will be done to any data which we do not specify a render for.
*/
function defaultDataRender(data: any, type: string, full: any) {
return Utilities.htmlEncode(data);
}
import * as Utilities from "../Utilities";
/**
* Wrapper function for creating data tables. Call this method, not the
* data tables constructor when you want to create a data table. This
* function makes sure that content without a render function is properly
* encoded to prevent XSS.
* @param{$dataTableElem} JQuery data table element
* @param{$settings} Settings to use when creating the data table
*/
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
return $dataTableElem.DataTable(applyDefaultRendering(settings));
}
/**
* Go through the settings for a data table and apply a simple HTML encode to any column
* without a render function to prevent XSS.
* @param{settings} The settings to check
* @return The given settings with all columns having a rendering function
*/
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
var tableColumns: DataTables.ColumnLegacy[] = null;
if (settings.aoColumns) {
tableColumns = settings.aoColumns;
} else if (settings.aoColumnDefs) {
// for tables we use aoColumnDefs instead of aoColumns
tableColumns = settings.aoColumnDefs;
}
// either the settings had no columns defined, or they were called
// by a property name which we have not used before
if (!tableColumns) {
return settings;
}
for (var i = 0; i < tableColumns.length; i++) {
// the column does not have a render function
if (!tableColumns[i].mRender) {
tableColumns[i].mRender = defaultDataRender;
}
}
return settings;
}
/**
* Default data render function, whatever is done to data in here
* will be done to any data which we do not specify a render for.
*/
function defaultDataRender(data: any, type: string, full: any) {
return Utilities.htmlEncode(data);
}

View File

@@ -1,300 +1,300 @@
import ko from "knockout";
import * as DataTableOperations from "./DataTableOperations";
import * as Constants from "../Constants";
import TableCommands from "./TableCommands";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/*
* Base class for data table row selection.
*/
export default class DataTableOperationManager {
private _tableEntityListViewModel: TableEntityListViewModel;
private _tableCommands: TableCommands;
private dataTable: JQuery;
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
this.dataTable = table;
this._tableEntityListViewModel = viewModel;
this._tableCommands = tableCommands;
this.bind();
this._tableEntityListViewModel.bind(this);
}
private click = (event: JQueryEventObject) => {
var elem: JQuery = $(event.currentTarget);
this.updateLastSelectedItem(elem, event.shiftKey);
if (Utilities.isEnvironmentCtrlPressed(event)) {
this.applyCtrlSelection(elem);
} else if (event.shiftKey) {
this.applyShiftSelection(elem);
} else {
this.applySingleSelection(elem);
}
};
private doubleClick = (event: JQueryEventObject) => {
this.tryOpenEditor();
};
private keyDown = (event: JQueryEventObject): boolean => {
var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow,
isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow,
handled: boolean = false;
if (isUpArrowKey || isDownArrowKey) {
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
var maximumIndex = dataTableRows.length - 1;
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
// Simply select the first item in this case.
var lastSelectedItemIndex = lastSelectedItem
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._)
)
: -1;
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex);
if (selectedRowElement) {
if (event.shiftKey) {
this.applyShiftSelection(selectedRowElement);
} else {
this.applySingleSelection(selectedRowElement);
}
this.updateLastSelectedItem(selectedRowElement, event.shiftKey);
handled = true;
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
}
} else if (
Utilities.isEnvironmentCtrlPressed(event) &&
!Utilities.isEnvironmentShiftPressed(event) &&
!Utilities.isEnvironmentAltPressed(event) &&
event.keyCode === Constants.keyCodes.A
) {
this.applySelectAll();
handled = true;
}
return !handled;
};
// Note: There is one key up event each time a key is pressed;
// in contrast, there may be more than one key down and key
// pressed events.
private keyUp = (event: JQueryEventObject): boolean => {
var handled: boolean = false;
switch (event.keyCode) {
case Constants.keyCodes.Enter:
handled = this.tryOpenEditor();
break;
case Constants.keyCodes.Delete:
handled = this.tryHandleDeleteSelected();
break;
}
return !handled;
};
private itemDropped = (event: JQueryEventObject): boolean => {
var handled: boolean = false;
var items = (<any>event.originalEvent).dataTransfer.items;
if (!items) {
// On browsers outside of Chromium
// we can't discern between dirs and files
// so we will disable drag & drop for now
return null;
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
var entry = item.webkitGetAsEntry();
if (entry.isFile) {
// TODO: parse the file and insert content as entities
}
}
return !handled;
};
private tryOpenEditor(): boolean {
return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel);
}
private tryHandleDeleteSelected(): boolean {
var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected();
var handled: boolean = false;
if (selectedEntities && selectedEntities.length) {
this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel);
handled = true;
}
return handled;
}
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
return {
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr)
};
}
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
);
this._tableEntityListViewModel.lastSelectedItem = entity;
if (!isShiftSelect) {
this._tableEntityListViewModel.lastSelectedAnchorItem = entity;
}
}
private applySingleSelection($elem: JQuery) {
if ($elem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
this._tableEntityListViewModel.clearSelection();
this.addToSelection(entityIdentity.RowKey);
}
}
private applySelectAll() {
this._tableEntityListViewModel.clearSelection();
ko.utils.arrayPushAll<Entities.ITableEntity>(
this._tableEntityListViewModel.selected,
this._tableEntityListViewModel.getAllItemsInCurrentPage()
);
}
private applyCtrlSelection($elem: JQuery): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected
: null;
if (koSelected) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if (
!this._tableEntityListViewModel.isItemSelected(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
)
) {
// Adding item not previously in selection
this.addToSelection(entityIdentity.RowKey);
} else {
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
}
}
}
private applyShiftSelection($elem: JQuery): void {
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
// If anchor item doesn't exist, use the first available item of current page instead
if (!anchorItem && this._tableEntityListViewModel.items().length > 0) {
anchorItem = this._tableEntityListViewModel.items()[0];
}
if (anchorItem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
);
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._)
);
var startIndex = Math.min(elementIndex, anchorIndex);
var endIndex = Math.max(elementIndex, anchorIndex);
this._tableEntityListViewModel.clearSelection();
ko.utils.arrayPushAll<Entities.ITableEntity>(
this._tableEntityListViewModel.selected,
this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1)
);
}
}
private applyContextMenuSelection($elem: JQuery) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if (
!this._tableEntityListViewModel.isItemSelected(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
)
) {
if (this._tableEntityListViewModel.selected().length) {
this._tableEntityListViewModel.clearSelection();
}
this.addToSelection(entityIdentity.RowKey);
}
}
private addToSelection(rowKey: string) {
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(rowKey)
);
if (selectedEntity != null) {
this._tableEntityListViewModel.selected.push(selectedEntity);
}
}
// Selecting first row if the selection is empty.
public selectFirstIfNeeded(): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected
: null;
var koEntities: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.items
: null;
if (!koSelected().length && koEntities().length) {
var firstEntity: Entities.ITableEntity = koEntities()[0];
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
this._tableEntityListViewModel.clearLastSelected();
this.addToSelection(firstEntity.RowKey._);
// Update last selection
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
// Finally, make sure first row is visible
DataTableOperations.scrollToTopIfNeeded();
}
}
public bind() {
this.dataTable.on("click", "tr", this.click);
this.dataTable.on("dblclick", "tr", this.doubleClick);
this.dataTable.on("keydown", "td", this.keyDown);
this.dataTable.on("keyup", "td", this.keyUp);
// Keyboard navigation - selecting first row if the selection is empty when the table gains focus.
this.dataTable.on("focus", () => {
this.selectFirstIfNeeded();
return true;
});
// Bind drag & drop behavior
$("body").on("drop", this.itemDropped);
}
public focusTable(): void {
this.dataTable.focus();
}
}
import ko from "knockout";
import * as DataTableOperations from "./DataTableOperations";
import * as Constants from "../Constants";
import TableCommands from "./TableCommands";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/*
* Base class for data table row selection.
*/
export default class DataTableOperationManager {
private _tableEntityListViewModel: TableEntityListViewModel;
private _tableCommands: TableCommands;
private dataTable: JQuery;
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
this.dataTable = table;
this._tableEntityListViewModel = viewModel;
this._tableCommands = tableCommands;
this.bind();
this._tableEntityListViewModel.bind(this);
}
private click = (event: JQueryEventObject) => {
var elem: JQuery = $(event.currentTarget);
this.updateLastSelectedItem(elem, event.shiftKey);
if (Utilities.isEnvironmentCtrlPressed(event)) {
this.applyCtrlSelection(elem);
} else if (event.shiftKey) {
this.applyShiftSelection(elem);
} else {
this.applySingleSelection(elem);
}
};
private doubleClick = (event: JQueryEventObject) => {
this.tryOpenEditor();
};
private keyDown = (event: JQueryEventObject): boolean => {
var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow,
isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow,
handled: boolean = false;
if (isUpArrowKey || isDownArrowKey) {
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
var maximumIndex = dataTableRows.length - 1;
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
// Simply select the first item in this case.
var lastSelectedItemIndex = lastSelectedItem
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._)
)
: -1;
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex);
if (selectedRowElement) {
if (event.shiftKey) {
this.applyShiftSelection(selectedRowElement);
} else {
this.applySingleSelection(selectedRowElement);
}
this.updateLastSelectedItem(selectedRowElement, event.shiftKey);
handled = true;
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
}
} else if (
Utilities.isEnvironmentCtrlPressed(event) &&
!Utilities.isEnvironmentShiftPressed(event) &&
!Utilities.isEnvironmentAltPressed(event) &&
event.keyCode === Constants.keyCodes.A
) {
this.applySelectAll();
handled = true;
}
return !handled;
};
// Note: There is one key up event each time a key is pressed;
// in contrast, there may be more than one key down and key
// pressed events.
private keyUp = (event: JQueryEventObject): boolean => {
var handled: boolean = false;
switch (event.keyCode) {
case Constants.keyCodes.Enter:
handled = this.tryOpenEditor();
break;
case Constants.keyCodes.Delete:
handled = this.tryHandleDeleteSelected();
break;
}
return !handled;
};
private itemDropped = (event: JQueryEventObject): boolean => {
var handled: boolean = false;
var items = (<any>event.originalEvent).dataTransfer.items;
if (!items) {
// On browsers outside of Chromium
// we can't discern between dirs and files
// so we will disable drag & drop for now
return null;
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
var entry = item.webkitGetAsEntry();
if (entry.isFile) {
// TODO: parse the file and insert content as entities
}
}
return !handled;
};
private tryOpenEditor(): boolean {
return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel);
}
private tryHandleDeleteSelected(): boolean {
var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected();
var handled: boolean = false;
if (selectedEntities && selectedEntities.length) {
this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel);
handled = true;
}
return handled;
}
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
return {
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
};
}
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
);
this._tableEntityListViewModel.lastSelectedItem = entity;
if (!isShiftSelect) {
this._tableEntityListViewModel.lastSelectedAnchorItem = entity;
}
}
private applySingleSelection($elem: JQuery) {
if ($elem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
this._tableEntityListViewModel.clearSelection();
this.addToSelection(entityIdentity.RowKey);
}
}
private applySelectAll() {
this._tableEntityListViewModel.clearSelection();
ko.utils.arrayPushAll<Entities.ITableEntity>(
this._tableEntityListViewModel.selected,
this._tableEntityListViewModel.getAllItemsInCurrentPage()
);
}
private applyCtrlSelection($elem: JQuery): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected
: null;
if (koSelected) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if (
!this._tableEntityListViewModel.isItemSelected(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
)
) {
// Adding item not previously in selection
this.addToSelection(entityIdentity.RowKey);
} else {
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
}
}
}
private applyShiftSelection($elem: JQuery): void {
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
// If anchor item doesn't exist, use the first available item of current page instead
if (!anchorItem && this._tableEntityListViewModel.items().length > 0) {
anchorItem = this._tableEntityListViewModel.items()[0];
}
if (anchorItem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
);
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._)
);
var startIndex = Math.min(elementIndex, anchorIndex);
var endIndex = Math.max(elementIndex, anchorIndex);
this._tableEntityListViewModel.clearSelection();
ko.utils.arrayPushAll<Entities.ITableEntity>(
this._tableEntityListViewModel.selected,
this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1)
);
}
}
private applyContextMenuSelection($elem: JQuery) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if (
!this._tableEntityListViewModel.isItemSelected(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
)
) {
if (this._tableEntityListViewModel.selected().length) {
this._tableEntityListViewModel.clearSelection();
}
this.addToSelection(entityIdentity.RowKey);
}
}
private addToSelection(rowKey: string) {
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(rowKey)
);
if (selectedEntity != null) {
this._tableEntityListViewModel.selected.push(selectedEntity);
}
}
// Selecting first row if the selection is empty.
public selectFirstIfNeeded(): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected
: null;
var koEntities: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.items
: null;
if (!koSelected().length && koEntities().length) {
var firstEntity: Entities.ITableEntity = koEntities()[0];
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
this._tableEntityListViewModel.clearLastSelected();
this.addToSelection(firstEntity.RowKey._);
// Update last selection
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
// Finally, make sure first row is visible
DataTableOperations.scrollToTopIfNeeded();
}
}
public bind() {
this.dataTable.on("click", "tr", this.click);
this.dataTable.on("dblclick", "tr", this.doubleClick);
this.dataTable.on("keydown", "td", this.keyDown);
this.dataTable.on("keyup", "td", this.keyUp);
// Keyboard navigation - selecting first row if the selection is empty when the table gains focus.
this.dataTable.on("focus", () => {
this.selectFirstIfNeeded();
return true;
});
// Bind drag & drop behavior
$("body").on("drop", this.itemDropped);
}
public focusTable(): void {
this.dataTable.focus();
}
}

View File

@@ -1,192 +1,192 @@
import _ from "underscore";
import Q from "q";
import * as Entities from "../Entities";
import * as QueryBuilderConstants from "../Constants";
import * as Utilities from "../Utilities";
export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
var selector: string = "";
selectorSchema &&
selectorSchema.forEach((p: Entities.IProperty) => {
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
});
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
}
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
var isVisible = false;
if (dataTableScrollBodyQuery.length && element) {
var elementRect: ClientRect = element.getBoundingClientRect(),
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
}
return isVisible;
}
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
if (dataTableRows.length) {
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
if (dataTableScrollBodyQuery.length && selectedRowElement) {
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
if (!isVisible) {
var selectedRowQuery: JQuery = $(selectedRowElement),
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
selectedElementPosition: number = selectedRowQuery.position().top,
newScrollPosition: number = 0;
if (isScrollUp) {
newScrollPosition = scrollPosition + selectedElementPosition;
} else {
newScrollPosition =
scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height());
}
dataTableScrollBodyQuery.scrollTop(newScrollPosition);
}
}
}
}
export function scrollToTopIfNeeded(): void {
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
if ($dataTableRows.length && $dataTableScrollBody.length) {
$dataTableScrollBody.scrollTop(0);
}
}
export function setPaginationButtonEventHandlers(): void {
$(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector)
.on("mousedown", (event: JQueryEventObject) => {
// Prevents the table contents from briefly jumping when clicking on "Load more"
event.preventDefault();
})
.attr("role", "button");
}
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void {
settings &&
settings.forEach((value: boolean, index: number) => {
table.column(index).visible(value, false);
});
table.columns.adjust().draw(false);
}
/**
* Reorder columns based on current order.
* If no current order is specified, reorder the columns based on intial order.
*/
export function reorderColumns(
table: DataTables.DataTable,
targetOrder: number[],
currentOrder?: number[]
): Q.Promise<any> {
var columnsCount: number = targetOrder.length;
var isCurrentOrderPassedIn: boolean = !!currentOrder;
if (!isCurrentOrderPassedIn) {
currentOrder = getInitialOrder(columnsCount);
}
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
// if the targetOrder is the same as current order, do nothing.
if (!isSameOrder) {
// Otherwise, calculate the transformation order.
// If current order not specified, then it'll be set to initial order,
// i.e., either no reorder happened before or reordering to its initial order,
// Then the transformation order will be the same as target order.
// If current order is specified, then a transformation order is calculated.
// Refer to calculateTransformationOrder for details about transformation order.
var transformationOrder: number[] = isCurrentOrderPassedIn
? calculateTransformationOrder(currentOrder, targetOrder)
: targetOrder;
try {
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
} catch (err) {
return Q.reject(err);
}
}
return Q.resolve(null);
}
export function resetColumns(table: DataTables.DataTable): void {
$.fn.dataTable.ColReorder(table).fnReset();
}
/**
* A table's initial order is described in the form of a natural ascending order.
* E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8]
*/
export function getInitialOrder(columnsCount: number): number[] {
return _.range(columnsCount);
}
/**
* Get current table's column order which is described based on initial table. E.g.,
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
*/
export function getCurrentOrder(table: DataTables.DataTable): number[] {
return $.fn.dataTable.ColReorder(table).fnOrder();
}
/**
* Switch the index and value for each element of an array. e.g.,
* InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8]
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
*/
export function invertIndexValues(inputArray: number[]): number[] {
var invertedArray: number[] = [];
if (inputArray) {
inputArray.forEach((value: number, index: number) => {
invertedArray[inputArray[index]] = index;
});
}
return invertedArray;
}
/**
* DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table.
* An detailed example for this. Assume the table has 9 columns.
* Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
* Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4}
* Transformation order: an order passed to fnOrder API that transforms table from current order to target order.
* When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g.,
* column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6.
* Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as
* the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated.
* Here is an example of how to calculate the transformation order:
* In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5.
* As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
*/
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
var transformationOrder: number[] = [];
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
}
return transformationOrder;
}
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
var columns: DataTables.ColumnsMethods = table.columns();
var headers: string[] = [];
if (columns) {
// table.columns() return ColumnsMethods which is an array of arrays
var columnIndexes: number[] = (<any>columns)[0];
if (columnIndexes) {
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
}
}
return headers;
}
import _ from "underscore";
import Q from "q";
import * as Entities from "../Entities";
import * as QueryBuilderConstants from "../Constants";
import * as Utilities from "../Utilities";
export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
var selector: string = "";
selectorSchema &&
selectorSchema.forEach((p: Entities.IProperty) => {
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
});
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
}
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
var isVisible = false;
if (dataTableScrollBodyQuery.length && element) {
var elementRect: ClientRect = element.getBoundingClientRect(),
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
}
return isVisible;
}
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
if (dataTableRows.length) {
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
if (dataTableScrollBodyQuery.length && selectedRowElement) {
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
if (!isVisible) {
var selectedRowQuery: JQuery = $(selectedRowElement),
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
selectedElementPosition: number = selectedRowQuery.position().top,
newScrollPosition: number = 0;
if (isScrollUp) {
newScrollPosition = scrollPosition + selectedElementPosition;
} else {
newScrollPosition =
scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height());
}
dataTableScrollBodyQuery.scrollTop(newScrollPosition);
}
}
}
}
export function scrollToTopIfNeeded(): void {
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
if ($dataTableRows.length && $dataTableScrollBody.length) {
$dataTableScrollBody.scrollTop(0);
}
}
export function setPaginationButtonEventHandlers(): void {
$(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector)
.on("mousedown", (event: JQueryEventObject) => {
// Prevents the table contents from briefly jumping when clicking on "Load more"
event.preventDefault();
})
.attr("role", "button");
}
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void {
settings &&
settings.forEach((value: boolean, index: number) => {
table.column(index).visible(value, false);
});
table.columns.adjust().draw(false);
}
/**
* Reorder columns based on current order.
* If no current order is specified, reorder the columns based on intial order.
*/
export function reorderColumns(
table: DataTables.DataTable,
targetOrder: number[],
currentOrder?: number[]
): Q.Promise<any> {
var columnsCount: number = targetOrder.length;
var isCurrentOrderPassedIn: boolean = !!currentOrder;
if (!isCurrentOrderPassedIn) {
currentOrder = getInitialOrder(columnsCount);
}
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
// if the targetOrder is the same as current order, do nothing.
if (!isSameOrder) {
// Otherwise, calculate the transformation order.
// If current order not specified, then it'll be set to initial order,
// i.e., either no reorder happened before or reordering to its initial order,
// Then the transformation order will be the same as target order.
// If current order is specified, then a transformation order is calculated.
// Refer to calculateTransformationOrder for details about transformation order.
var transformationOrder: number[] = isCurrentOrderPassedIn
? calculateTransformationOrder(currentOrder, targetOrder)
: targetOrder;
try {
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
} catch (err) {
return Q.reject(err);
}
}
return Q.resolve(null);
}
export function resetColumns(table: DataTables.DataTable): void {
$.fn.dataTable.ColReorder(table).fnReset();
}
/**
* A table's initial order is described in the form of a natural ascending order.
* E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8]
*/
export function getInitialOrder(columnsCount: number): number[] {
return _.range(columnsCount);
}
/**
* Get current table's column order which is described based on initial table. E.g.,
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
*/
export function getCurrentOrder(table: DataTables.DataTable): number[] {
return $.fn.dataTable.ColReorder(table).fnOrder();
}
/**
* Switch the index and value for each element of an array. e.g.,
* InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8]
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
*/
export function invertIndexValues(inputArray: number[]): number[] {
var invertedArray: number[] = [];
if (inputArray) {
inputArray.forEach((value: number, index: number) => {
invertedArray[inputArray[index]] = index;
});
}
return invertedArray;
}
/**
* DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table.
* An detailed example for this. Assume the table has 9 columns.
* Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
* Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4}
* Transformation order: an order passed to fnOrder API that transforms table from current order to target order.
* When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g.,
* column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6.
* Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as
* the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated.
* Here is an example of how to calculate the transformation order:
* In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5.
* As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
*/
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
var transformationOrder: number[] = [];
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
}
return transformationOrder;
}
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
var columns: DataTables.ColumnsMethods = table.columns();
var headers: string[] = [];
if (columns) {
// table.columns() return ColumnsMethods which is an array of arrays
var columnIndexes: number[] = (<any>columns)[0];
if (columnIndexes) {
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
}
}
return headers;
}

View File

@@ -1,148 +1,148 @@
import * as _ from "underscore";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import * as TableEntityProcessor from "../TableEntityProcessor";
export enum IconState {
default,
hoverState,
toggleOn
}
/**
* Represents an html input element shown in context menu.
* name: the input name
* type: the input type, e.g., "text", "checkbox", "radio", etc.
* selected: optional. Used when the input type is checkbox. True means checkbox is selected. Otherwise, unselected.
*/
export interface IContextMenuInputItem {
name: string;
type: string;
selected?: boolean;
}
export interface IContextMenuOption {
[key: string]: IContextMenuInputItem;
}
export function containMultipleItems<T>(items: T[]): boolean {
return items && items.length > 1;
}
export function containSingleItem<T>(items: T[]): boolean {
return items && items.length === 1;
}
export function containItems<T>(items: T[]): boolean {
return items && items.length > 0;
}
// export function setTargetIcon(idToIconHandlerMap: CloudHub.Common.IToolbarElementIdIconMap, $sourceElement: JQuery, toIconState: IconState): void {
// if (idToIconHandlerMap) {
// var iconId: string = $sourceElement.attr("id");
// var iconHandler = idToIconHandlerMap[iconId];
// switch (toIconState) {
// case IconState.default:
// iconHandler.observable(iconHandler.default);
// break;
// case IconState.hoverState:
// iconHandler.observable(iconHandler.hoverState);
// break;
// default:
// window.console.log("error");
// }
// }
// }
export function addCssClass($sourceElement: JQuery, cssClassName: string): void {
if (!$sourceElement.hasClass(cssClassName)) {
$sourceElement.addClass(cssClassName);
}
}
export function removeCssClass($sourceElement: JQuery, cssClassName: string): void {
if ($sourceElement.hasClass(cssClassName)) {
$sourceElement.removeClass(cssClassName);
}
}
/**
* Get the property union of input entities.
* Example:
* Input:
* Entities: [{ PrimaryKey, id, Prop1, Prop2 }, { PrimaryKey, id, Prop2, Prop3, Prop4 }]
* Return:
* Union: [PrimaryKey, id, Prop1, Prop2, Prop3, Prop4]
*/
export function getPropertyIntersectionFromTableEntities(
entities: Entities.ITableEntity[],
isCassandraApi: boolean
): string[] {
var headerUnion: string[] = [];
entities &&
entities.forEach((row: any) => {
const keys = Object.keys(row);
keys &&
keys.forEach((key: string) => {
if (
key !== ".metadata" &&
!_.contains(headerUnion, key) &&
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!isCassandraApi || key !== Constants.EntityKeyNames.RowKey)
) {
headerUnion.push(key);
}
});
});
return headerUnion;
}
/**
* Compares the names of two Azure table columns and returns a number indicating which comes before the other.
* System-defined properties come before custom properties. Otherwise they are compared using string comparison.
*/
export function compareTableColumns(a: string, b: string): number {
if (a === "PartitionKey") {
if (b !== "PartitionKey") {
return -1;
}
} else if (a === "RowKey") {
if (b === "PartitionKey") {
return 1;
} else if (b !== "RowKey") {
return -1;
}
} else if (a === "Timestamp") {
if (b === "PartitionKey" || b === "RowKey") {
return 1;
} else if (b !== "Timestamp") {
return -1;
}
} else if (b === "PartitionKey" || b === "RowKey" || b === "Timestamp") {
return 1;
}
return a.localeCompare(b);
}
export function checkForDefaultHeader(headers: string[]): boolean {
return headers[0] === Constants.defaultHeader;
}
/**
* DataTableBindingManager registers an event handler of body.resize and recalculates the data table size.
* This method forces the event to happen.
*/
export function forceRecalculateTableSize(): void {
$("body").trigger("resize");
}
/**
* Turns off the spinning progress indicator on the data table.
*/
export function turnOffProgressIndicator(): void {
$("div.dataTables_processing").hide();
}
import * as _ from "underscore";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import * as TableEntityProcessor from "../TableEntityProcessor";
export enum IconState {
default,
hoverState,
toggleOn,
}
/**
* Represents an html input element shown in context menu.
* name: the input name
* type: the input type, e.g., "text", "checkbox", "radio", etc.
* selected: optional. Used when the input type is checkbox. True means checkbox is selected. Otherwise, unselected.
*/
export interface IContextMenuInputItem {
name: string;
type: string;
selected?: boolean;
}
export interface IContextMenuOption {
[key: string]: IContextMenuInputItem;
}
export function containMultipleItems<T>(items: T[]): boolean {
return items && items.length > 1;
}
export function containSingleItem<T>(items: T[]): boolean {
return items && items.length === 1;
}
export function containItems<T>(items: T[]): boolean {
return items && items.length > 0;
}
// export function setTargetIcon(idToIconHandlerMap: CloudHub.Common.IToolbarElementIdIconMap, $sourceElement: JQuery, toIconState: IconState): void {
// if (idToIconHandlerMap) {
// var iconId: string = $sourceElement.attr("id");
// var iconHandler = idToIconHandlerMap[iconId];
// switch (toIconState) {
// case IconState.default:
// iconHandler.observable(iconHandler.default);
// break;
// case IconState.hoverState:
// iconHandler.observable(iconHandler.hoverState);
// break;
// default:
// window.console.log("error");
// }
// }
// }
export function addCssClass($sourceElement: JQuery, cssClassName: string): void {
if (!$sourceElement.hasClass(cssClassName)) {
$sourceElement.addClass(cssClassName);
}
}
export function removeCssClass($sourceElement: JQuery, cssClassName: string): void {
if ($sourceElement.hasClass(cssClassName)) {
$sourceElement.removeClass(cssClassName);
}
}
/**
* Get the property union of input entities.
* Example:
* Input:
* Entities: [{ PrimaryKey, id, Prop1, Prop2 }, { PrimaryKey, id, Prop2, Prop3, Prop4 }]
* Return:
* Union: [PrimaryKey, id, Prop1, Prop2, Prop3, Prop4]
*/
export function getPropertyIntersectionFromTableEntities(
entities: Entities.ITableEntity[],
isCassandraApi: boolean
): string[] {
var headerUnion: string[] = [];
entities &&
entities.forEach((row: any) => {
const keys = Object.keys(row);
keys &&
keys.forEach((key: string) => {
if (
key !== ".metadata" &&
!_.contains(headerUnion, key) &&
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!isCassandraApi || key !== Constants.EntityKeyNames.RowKey)
) {
headerUnion.push(key);
}
});
});
return headerUnion;
}
/**
* Compares the names of two Azure table columns and returns a number indicating which comes before the other.
* System-defined properties come before custom properties. Otherwise they are compared using string comparison.
*/
export function compareTableColumns(a: string, b: string): number {
if (a === "PartitionKey") {
if (b !== "PartitionKey") {
return -1;
}
} else if (a === "RowKey") {
if (b === "PartitionKey") {
return 1;
} else if (b !== "RowKey") {
return -1;
}
} else if (a === "Timestamp") {
if (b === "PartitionKey" || b === "RowKey") {
return 1;
} else if (b !== "Timestamp") {
return -1;
}
} else if (b === "PartitionKey" || b === "RowKey" || b === "Timestamp") {
return 1;
}
return a.localeCompare(b);
}
export function checkForDefaultHeader(headers: string[]): boolean {
return headers[0] === Constants.defaultHeader;
}
/**
* DataTableBindingManager registers an event handler of body.resize and recalculates the data table size.
* This method forces the event to happen.
*/
export function forceRecalculateTableSize(): void {
$("body").trigger("resize");
}
/**
* Turns off the spinning progress indicator on the data table.
*/
export function turnOffProgressIndicator(): void {
$("div.dataTables_processing").hide();
}

View File

@@ -1,270 +1,270 @@
import * as ko from "knockout";
import * as _ from "underscore";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import CacheBase from "./CacheBase";
import * as CommonConstants from "../../../Common/Constants";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
// This is the format of the data we will have to pass to Datatable render callback,
// and property names are defined by Datatable as well.
export interface IDataTableRenderData {
draw: number;
aaData: any;
recordsTotal: number;
recordsFiltered: number;
}
abstract class DataTableViewModel {
private static lastPageLabel = ">>"; // Localize
private static loadMoreLabel = "Load more"; // Localize
/* Observables */
public items = ko.observableArray<Entities.ITableEntity>();
public selected = ko.observableArray<Entities.ITableEntity>();
public table: DataTables.DataTable;
// The anchor item is for shift selection. i.e., select all items between anchor item and a give item.
public lastSelectedAnchorItem: Entities.ITableEntity;
public lastSelectedItem: Entities.ITableEntity;
public cache: CacheBase<Entities.ITableEntity>;
protected continuationToken: any;
protected allDownloaded: boolean;
protected lastPrefetchTime: number;
protected downloadSize = 300;
protected _documentIterator: QueryIterator<ItemDefinition & Resource>;
// Used by table redraw throttling
protected pollingInterval = 1000;
private redrawInterval = 500;
private pendingRedraw = false;
private lastRedrawTime = new Date().getTime();
private dataTableOperationManager: IDataTableOperation;
public queryTablesTab: QueryTablesTab;
constructor() {
this.items([]);
this.selected([]);
// Late bound
this.dataTableOperationManager = null;
}
public bind(dataTableOperationManager: IDataTableOperation): void {
this.dataTableOperationManager = dataTableOperationManager;
}
public clearLastSelected(): void {
this.lastSelectedItem = null;
this.lastSelectedAnchorItem = null;
}
public clearCache(): void {
this.cache.clear();
this._documentIterator = null;
this.continuationToken = null;
this.allDownloaded = false;
}
public clearSelection(): void {
this.selected.removeAll();
}
// Redraws the table, but guarantees that multiple sequential calls will not incur
// another redraw until a certain time interval has passed.
public redrawTableThrottled() {
if (!this.pendingRedraw) {
this.pendingRedraw = true;
var current = new Date().getTime();
var timeSinceLastRedraw = current - this.lastRedrawTime;
var redraw = () => {
this.table.draw(false /*reset*/);
this.lastRedrawTime = new Date().getTime();
this.pendingRedraw = false;
};
if (timeSinceLastRedraw > this.redrawInterval) {
redraw();
} else {
var timeUntilNextRedraw = this.redrawInterval - timeSinceLastRedraw;
setTimeout(() => redraw(), timeUntilNextRedraw);
}
}
}
public focusDataTable(): void {
this.dataTableOperationManager.focusTable();
}
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
return _.find(this.selected(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
return _.find(this.items(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemIndexFromCurrentPage(itemKeys: Entities.IProperty[]): number {
return _.findIndex(this.items(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemIndexFromAllPages(itemKeys: Entities.IProperty[]): number {
return _.findIndex(this.cache.data, (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemsFromAllPagesWithinRange(start: number, end: number): Entities.ITableEntity[] {
return this.cache.data.slice(start, end);
}
public isItemSelected(itemKeys: Entities.IProperty[]): boolean {
return _.some(this.selected(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public isItemCached(itemKeys: Entities.IProperty[]): boolean {
return _.some(this.cache.data, (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getAllItemsInCurrentPage(): Entities.ITableEntity[] {
return this.items();
}
public getAllItemsInCache(): Entities.ITableEntity[] {
return this.cache.data;
}
protected abstract dataComparer(
item1: Entities.ITableEntity,
item2: Entities.ITableEntity,
sortOrder: any,
oSettings: any
): number;
protected abstract isCacheValid(validator: any): boolean;
protected sortColumns(sortOrder: any, oSettings: any) {
var self = this;
this.clearSelection();
this.cache.data.sort(function(a: any, b: any) {
return self.dataComparer(a, b, sortOrder, oSettings);
});
this.cache.sortOrder = sortOrder;
}
protected renderPage(
renderCallBack: any,
draw: number,
startIndex: number,
pageSize: number,
oSettings: any,
postRenderTasks: (startIndex: number, pageSize: number) => Promise<void> = null
) {
this.updatePaginationControls(oSettings);
// pageSize < 0 means to show all data
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
var renderData = this.cache.data.slice(startIndex, endIndex);
this.items(renderData);
var render: IDataTableRenderData = {
draw: draw,
aaData: renderData,
recordsTotal: this.cache.length,
recordsFiltered: this.cache.length
};
if (!!postRenderTasks) {
postRenderTasks(startIndex, pageSize).then(() => {
this.table.rows().invalidate();
});
}
renderCallBack(render);
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name,
databaseName: this.queryTablesTab.collection.databaseId,
collectionName: this.queryTablesTab.collection.id(),
defaultExperience: this.queryTablesTab.collection.container.defaultExperience(),
dataExplorerArea: CommonConstants.Areas.Tab,
tabTitle: this.queryTablesTab.tabTitle()
},
this.queryTablesTab.onLoadStartKey
);
this.queryTablesTab.onLoadStartKey = null;
}
}
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
return itemKeys.every((property: Entities.IProperty) => {
var itemValue = item[property.key];
// if (itemValue && property.subkey) {
// itemValue = itemValue._[property.subkey];
// if (!itemValue) {
// itemValue = "";
// }
// } else if (property.subkey) {
// itemValue = "";
// }
return this.stringCompare(itemValue._, property.value);
});
}
/**
* Default string comparison is case sensitive as most Azure resources' names are case sensitive.
* Override this if a name, i.e., Azure File/Directory name, is case insensitive.
*/
protected stringCompare(s1: string, s2: string): boolean {
return s1 === s2;
}
private updatePaginationControls(oSettings: any) {
var pageInfo = this.table.page.info();
var pageSize = pageInfo.length;
var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector);
if (this.allDownloaded) {
if (this.cache.length <= pageSize) {
// Hide pagination controls if everything fits in one page!.
paginateElement.hide();
} else {
// Enable pagination controls.
paginateElement.show();
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel;
}
} else {
// Enable pagination controls and show load more button.
paginateElement.show();
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel;
}
}
}
interface IDataTableOperation {
focusTable(): void;
}
export default DataTableViewModel;
import * as ko from "knockout";
import * as _ from "underscore";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import CacheBase from "./CacheBase";
import * as CommonConstants from "../../../Common/Constants";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
// This is the format of the data we will have to pass to Datatable render callback,
// and property names are defined by Datatable as well.
export interface IDataTableRenderData {
draw: number;
aaData: any;
recordsTotal: number;
recordsFiltered: number;
}
abstract class DataTableViewModel {
private static lastPageLabel = ">>"; // Localize
private static loadMoreLabel = "Load more"; // Localize
/* Observables */
public items = ko.observableArray<Entities.ITableEntity>();
public selected = ko.observableArray<Entities.ITableEntity>();
public table: DataTables.DataTable;
// The anchor item is for shift selection. i.e., select all items between anchor item and a give item.
public lastSelectedAnchorItem: Entities.ITableEntity;
public lastSelectedItem: Entities.ITableEntity;
public cache: CacheBase<Entities.ITableEntity>;
protected continuationToken: any;
protected allDownloaded: boolean;
protected lastPrefetchTime: number;
protected downloadSize = 300;
protected _documentIterator: QueryIterator<ItemDefinition & Resource>;
// Used by table redraw throttling
protected pollingInterval = 1000;
private redrawInterval = 500;
private pendingRedraw = false;
private lastRedrawTime = new Date().getTime();
private dataTableOperationManager: IDataTableOperation;
public queryTablesTab: QueryTablesTab;
constructor() {
this.items([]);
this.selected([]);
// Late bound
this.dataTableOperationManager = null;
}
public bind(dataTableOperationManager: IDataTableOperation): void {
this.dataTableOperationManager = dataTableOperationManager;
}
public clearLastSelected(): void {
this.lastSelectedItem = null;
this.lastSelectedAnchorItem = null;
}
public clearCache(): void {
this.cache.clear();
this._documentIterator = null;
this.continuationToken = null;
this.allDownloaded = false;
}
public clearSelection(): void {
this.selected.removeAll();
}
// Redraws the table, but guarantees that multiple sequential calls will not incur
// another redraw until a certain time interval has passed.
public redrawTableThrottled() {
if (!this.pendingRedraw) {
this.pendingRedraw = true;
var current = new Date().getTime();
var timeSinceLastRedraw = current - this.lastRedrawTime;
var redraw = () => {
this.table.draw(false /*reset*/);
this.lastRedrawTime = new Date().getTime();
this.pendingRedraw = false;
};
if (timeSinceLastRedraw > this.redrawInterval) {
redraw();
} else {
var timeUntilNextRedraw = this.redrawInterval - timeSinceLastRedraw;
setTimeout(() => redraw(), timeUntilNextRedraw);
}
}
}
public focusDataTable(): void {
this.dataTableOperationManager.focusTable();
}
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
return _.find(this.selected(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
return _.find(this.items(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemIndexFromCurrentPage(itemKeys: Entities.IProperty[]): number {
return _.findIndex(this.items(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemIndexFromAllPages(itemKeys: Entities.IProperty[]): number {
return _.findIndex(this.cache.data, (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getItemsFromAllPagesWithinRange(start: number, end: number): Entities.ITableEntity[] {
return this.cache.data.slice(start, end);
}
public isItemSelected(itemKeys: Entities.IProperty[]): boolean {
return _.some(this.selected(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public isItemCached(itemKeys: Entities.IProperty[]): boolean {
return _.some(this.cache.data, (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
});
}
public getAllItemsInCurrentPage(): Entities.ITableEntity[] {
return this.items();
}
public getAllItemsInCache(): Entities.ITableEntity[] {
return this.cache.data;
}
protected abstract dataComparer(
item1: Entities.ITableEntity,
item2: Entities.ITableEntity,
sortOrder: any,
oSettings: any
): number;
protected abstract isCacheValid(validator: any): boolean;
protected sortColumns(sortOrder: any, oSettings: any) {
var self = this;
this.clearSelection();
this.cache.data.sort(function (a: any, b: any) {
return self.dataComparer(a, b, sortOrder, oSettings);
});
this.cache.sortOrder = sortOrder;
}
protected renderPage(
renderCallBack: any,
draw: number,
startIndex: number,
pageSize: number,
oSettings: any,
postRenderTasks: (startIndex: number, pageSize: number) => Promise<void> = null
) {
this.updatePaginationControls(oSettings);
// pageSize < 0 means to show all data
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
var renderData = this.cache.data.slice(startIndex, endIndex);
this.items(renderData);
var render: IDataTableRenderData = {
draw: draw,
aaData: renderData,
recordsTotal: this.cache.length,
recordsFiltered: this.cache.length,
};
if (!!postRenderTasks) {
postRenderTasks(startIndex, pageSize).then(() => {
this.table.rows().invalidate();
});
}
renderCallBack(render);
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name,
databaseName: this.queryTablesTab.collection.databaseId,
collectionName: this.queryTablesTab.collection.id(),
defaultExperience: this.queryTablesTab.collection.container.defaultExperience(),
dataExplorerArea: CommonConstants.Areas.Tab,
tabTitle: this.queryTablesTab.tabTitle(),
},
this.queryTablesTab.onLoadStartKey
);
this.queryTablesTab.onLoadStartKey = null;
}
}
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
return itemKeys.every((property: Entities.IProperty) => {
var itemValue = item[property.key];
// if (itemValue && property.subkey) {
// itemValue = itemValue._[property.subkey];
// if (!itemValue) {
// itemValue = "";
// }
// } else if (property.subkey) {
// itemValue = "";
// }
return this.stringCompare(itemValue._, property.value);
});
}
/**
* Default string comparison is case sensitive as most Azure resources' names are case sensitive.
* Override this if a name, i.e., Azure File/Directory name, is case insensitive.
*/
protected stringCompare(s1: string, s2: string): boolean {
return s1 === s2;
}
private updatePaginationControls(oSettings: any) {
var pageInfo = this.table.page.info();
var pageSize = pageInfo.length;
var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector);
if (this.allDownloaded) {
if (this.cache.length <= pageSize) {
// Hide pagination controls if everything fits in one page!.
paginateElement.hide();
} else {
// Enable pagination controls.
paginateElement.show();
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel;
}
} else {
// Enable pagination controls and show load more button.
paginateElement.show();
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel;
}
}
}
interface IDataTableOperation {
focusTable(): void;
}
export default DataTableViewModel;

View File

@@ -1,156 +1,156 @@
import _ from "underscore";
import Q from "q";
import * as DataTableUtilities from "./DataTableUtilities";
import * as DataTableOperations from "./DataTableOperations";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Entities from "../Entities";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as TableColumnOptionsPane from "../../Panes/Tables/TableColumnOptionsPane";
import Explorer from "../../Explorer";
export default class TableCommands {
// Command Ids
public static editEntityCommand: string = "edit";
public static deleteEntitiesCommand: string = "delete";
public static reorderColumnsCommand: string = "reorder";
public static resetColumnsCommand: string = "reset";
public static customizeColumnsCommand: string = "customizeColumns";
private _container: Explorer;
constructor(container: Explorer) {
this._container = container;
}
public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean {
var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites);
var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites);
switch (commandName) {
case TableCommands.editEntityCommand:
return singleItemSelected;
case TableCommands.deleteEntitiesCommand:
case TableCommands.reorderColumnsCommand:
return atLeastOneItemSelected;
default:
break;
}
return false;
}
public tryOpenEntityEditor(viewModel: TableEntityListViewModel): boolean {
if (this.isEnabled(TableCommands.editEntityCommand, viewModel.selected())) {
this.editEntityCommand(viewModel);
return true;
}
return false;
}
/**
* Edit entity
*/
public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) {
return null; // Error
}
if (!DataTableUtilities.containSingleItem(viewModel.selected())) {
return null; // Erorr
}
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
this._container.editTableEntityPane.originEntity = entityToUpdate;
this._container.editTableEntityPane.tableViewModel = viewModel;
this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties;
this._container.editTableEntityPane.open();
return null;
}
public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) {
return null; // Error
}
if (!DataTableUtilities.containItems(viewModel.selected())) {
return null; // Error
}
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
let deleteMessage: string = "Are you sure you want to delete the selected entities?";
if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) {
deleteMessage = "Are you sure you want to delete the selected rows?";
}
if (window.confirm(deleteMessage)) {
viewModel.queryTablesTab.container.tableDataClient
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
.then((results: any) => {
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
viewModel.redrawTableThrottled();
});
});
}
return null;
}
public customizeColumnsCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
var table: DataTables.DataTable = viewModel.table;
var displayedColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
var columnsCount: number = displayedColumnNames.length;
var currentOrder: number[] = DataTableOperations.getInitialOrder(columnsCount);
//Debug.assert(!!table && !!currentOrder && displayedColumnNames.length === currentOrder.length);
var currentSettings: boolean[];
try {
currentSettings = currentOrder.map((value: number, index: number) => {
return table.column(index).visible();
});
} catch (err) {
// Error
}
let parameters: TableColumnOptionsPane.IColumnSetting = <TableColumnOptionsPane.IColumnSetting>{
columnNames: displayedColumnNames,
order: currentOrder,
visible: currentSettings
};
this._container.tableColumnOptionsPane.tableViewModel = viewModel;
this._container.tableColumnOptionsPane.parameters = parameters;
this._container.tableColumnOptionsPane.open();
return null;
}
public reorderColumnsBasedOnSelectedEntities(viewModel: TableEntityListViewModel): Q.Promise<boolean> {
var selected = viewModel.selected();
if (!selected || !selected.length) {
return null;
}
var table = viewModel.table;
var currentColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
var headersCount: number = currentColumnNames.length;
var headersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
selected,
viewModel.queryTablesTab.container.isPreferredApiCassandra()
);
// An array with elements representing indexes of selected entities' header union out of initial headers.
var orderOfLeftHeaders: number[] = headersUnion.map((item: string) => currentColumnNames.indexOf(item));
// An array with elements representing initial order of the table.
var initialOrder: number[] = DataTableOperations.getInitialOrder(headersCount);
// An array with elements representing indexes of headers not present in selected entities' header union.
var orderOfRightHeaders: number[] = _.difference(initialOrder, orderOfLeftHeaders);
// This will be the target order, with headers in selected entities on the left while others on the right, both in the initial order, respectively.
var targetOrder: number[] = orderOfLeftHeaders.concat(orderOfRightHeaders);
return DataTableOperations.reorderColumns(table, targetOrder);
}
public resetColumns(viewModel: TableEntityListViewModel): void {
viewModel.reloadTable();
}
}
import _ from "underscore";
import Q from "q";
import * as DataTableUtilities from "./DataTableUtilities";
import * as DataTableOperations from "./DataTableOperations";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Entities from "../Entities";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as TableColumnOptionsPane from "../../Panes/Tables/TableColumnOptionsPane";
import Explorer from "../../Explorer";
export default class TableCommands {
// Command Ids
public static editEntityCommand: string = "edit";
public static deleteEntitiesCommand: string = "delete";
public static reorderColumnsCommand: string = "reorder";
public static resetColumnsCommand: string = "reset";
public static customizeColumnsCommand: string = "customizeColumns";
private _container: Explorer;
constructor(container: Explorer) {
this._container = container;
}
public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean {
var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites);
var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites);
switch (commandName) {
case TableCommands.editEntityCommand:
return singleItemSelected;
case TableCommands.deleteEntitiesCommand:
case TableCommands.reorderColumnsCommand:
return atLeastOneItemSelected;
default:
break;
}
return false;
}
public tryOpenEntityEditor(viewModel: TableEntityListViewModel): boolean {
if (this.isEnabled(TableCommands.editEntityCommand, viewModel.selected())) {
this.editEntityCommand(viewModel);
return true;
}
return false;
}
/**
* Edit entity
*/
public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) {
return null; // Error
}
if (!DataTableUtilities.containSingleItem(viewModel.selected())) {
return null; // Erorr
}
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
this._container.editTableEntityPane.originEntity = entityToUpdate;
this._container.editTableEntityPane.tableViewModel = viewModel;
this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties;
this._container.editTableEntityPane.open();
return null;
}
public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) {
return null; // Error
}
if (!DataTableUtilities.containItems(viewModel.selected())) {
return null; // Error
}
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
let deleteMessage: string = "Are you sure you want to delete the selected entities?";
if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) {
deleteMessage = "Are you sure you want to delete the selected rows?";
}
if (window.confirm(deleteMessage)) {
viewModel.queryTablesTab.container.tableDataClient
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
.then((results: any) => {
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
viewModel.redrawTableThrottled();
});
});
}
return null;
}
public customizeColumnsCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
var table: DataTables.DataTable = viewModel.table;
var displayedColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
var columnsCount: number = displayedColumnNames.length;
var currentOrder: number[] = DataTableOperations.getInitialOrder(columnsCount);
//Debug.assert(!!table && !!currentOrder && displayedColumnNames.length === currentOrder.length);
var currentSettings: boolean[];
try {
currentSettings = currentOrder.map((value: number, index: number) => {
return table.column(index).visible();
});
} catch (err) {
// Error
}
let parameters: TableColumnOptionsPane.IColumnSetting = <TableColumnOptionsPane.IColumnSetting>{
columnNames: displayedColumnNames,
order: currentOrder,
visible: currentSettings,
};
this._container.tableColumnOptionsPane.tableViewModel = viewModel;
this._container.tableColumnOptionsPane.parameters = parameters;
this._container.tableColumnOptionsPane.open();
return null;
}
public reorderColumnsBasedOnSelectedEntities(viewModel: TableEntityListViewModel): Q.Promise<boolean> {
var selected = viewModel.selected();
if (!selected || !selected.length) {
return null;
}
var table = viewModel.table;
var currentColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
var headersCount: number = currentColumnNames.length;
var headersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
selected,
viewModel.queryTablesTab.container.isPreferredApiCassandra()
);
// An array with elements representing indexes of selected entities' header union out of initial headers.
var orderOfLeftHeaders: number[] = headersUnion.map((item: string) => currentColumnNames.indexOf(item));
// An array with elements representing initial order of the table.
var initialOrder: number[] = DataTableOperations.getInitialOrder(headersCount);
// An array with elements representing indexes of headers not present in selected entities' header union.
var orderOfRightHeaders: number[] = _.difference(initialOrder, orderOfLeftHeaders);
// This will be the target order, with headers in selected entities on the left while others on the right, both in the initial order, respectively.
var targetOrder: number[] = orderOfLeftHeaders.concat(orderOfRightHeaders);
return DataTableOperations.reorderColumns(table, targetOrder);
}
public resetColumns(viewModel: TableEntityListViewModel): void {
viewModel.reloadTable();
}
}

View File

@@ -1,27 +1,27 @@
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
import CacheBase from "./CacheBase";
export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
private _tableQuery: Entities.ITableQuery;
constructor() {
super();
this.data = null;
this._tableQuery = null;
this.serverCallInProgress = false;
this.sortOrder = null;
}
public get tableQuery(): Entities.ITableQuery {
return Utilities.copyTableQuery(this._tableQuery);
}
public set tableQuery(tableQuery: Entities.ITableQuery) {
this._tableQuery = Utilities.copyTableQuery(tableQuery);
}
public preClear() {
this.tableQuery = null;
}
}
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
import CacheBase from "./CacheBase";
export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
private _tableQuery: Entities.ITableQuery;
constructor() {
super();
this.data = null;
this._tableQuery = null;
this.serverCallInProgress = false;
this.sortOrder = null;
}
public get tableQuery(): Entities.ITableQuery {
return Utilities.copyTableQuery(this._tableQuery);
}
public set tableQuery(tableQuery: Entities.ITableQuery) {
this._tableQuery = Utilities.copyTableQuery(tableQuery);
}
public preClear() {
this.tableQuery = null;
}
}

File diff suppressed because it is too large Load Diff