UNPKG

@eclipse-scout/core

Version:
502 lines (438 loc) 20.2 kB
/* * Copyright (c) 2010, 2025 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import { arrays, AutoLeafPageWithNodes, BookmarkSupport, BookmarkTableRowIdentifierDo, dataObjects, DoEntity, Event, EventHandler, Form, InitModelOf, LimitedResultInfoContributionDo, ObjectOrModel, Page, PageWithTableEventMap, PageWithTableModel, PropertyChangeEvent, scout, SearchFormTableControl, Status, Table, TableAllRowsDeletedEvent, TableControl, TableMaxResultsHelper, TableOrganizerMenu, TableReloadEvent, TableReloadReason, TableRow, TableRowActionEvent, TableRowOrderChangedEvent, TableRowsDeletedEvent, TableRowsInsertedEvent, TableRowsUpdatedEvent } from '../../../index'; import $ from 'jquery'; export class PageWithTable extends Page implements PageWithTableModel { declare model: PageWithTableModel; declare eventMap: PageWithTableEventMap; alwaysCreateChildPage: boolean; protected _tableRowDeleteHandler: EventHandler<TableRowsDeletedEvent | TableAllRowsDeletedEvent>; protected _tableRowInsertHandler: EventHandler<TableRowsInsertedEvent>; protected _tableRowUpdateHandler: EventHandler<TableRowsUpdatedEvent>; protected _tableRowActionHandler: EventHandler<TableRowActionEvent>; protected _tableRowOrderChangeHandler: EventHandler<TableRowOrderChangedEvent>; protected _tableDataLoadHandler: EventHandler<TableReloadEvent>; protected _tableControlsChangeHandler: EventHandler<PropertyChangeEvent<TableControl[]>> = this._onTableControlsChange.bind(this); protected _searchFormTableControlSearchHandler: EventHandler<Event<SearchFormTableControl>> = this._onSearchFormTableControlSearch.bind(this); protected _searchFormTableControlResetHandler: EventHandler<Event<SearchFormTableControl>> = this._onSearchFormTableControlReset.bind(this); constructor() { super(); this.nodeType = Page.NodeType.TABLE; this.inheritMenusFromParentTablePage = false; this.alwaysCreateChildPage = false; this._tableRowDeleteHandler = this._onTableRowsDeleted.bind(this); this._tableRowInsertHandler = this._onTableRowsInserted.bind(this); this._tableRowUpdateHandler = this._onTableRowsUpdated.bind(this); this._tableRowActionHandler = this._onTableRowAction.bind(this); this._tableRowOrderChangeHandler = this._onTableRowOrderChanged.bind(this); this._tableDataLoadHandler = this._onTableReload.bind(this); } protected override _init(model: InitModelOf<this>) { super._init(model); // display rows as AutoLeafPageWithNodes if outline is compact if (this.outline.compact) { this.setAlwaysCreateChildPage(true); this.setLeaf(false); } } setAlwaysCreateChildPage(alwaysCreateChildPage: boolean) { this.alwaysCreateChildPage = alwaysCreateChildPage; } protected override _initDetailTable(table: Table) { super._initDetailTable(table); if (this.outline.compact) { // set table compact if outline is compact table.setCompact(true); // disable more-link and enable html to plain text on compact handler table.compactHandler.setMoreLinkAvailable(false); table.compactHandler.setLineCustomizer(line => line.textBlock.setHtmlToPlainTextEnabled(true)); // hide all table controls except search form table control const searchFormTableControl = this._findSearchFormTableControl(table); for (const tableControl of table.tableControls) { if (tableControl === searchFormTableControl) { continue; } tableControl.setVisibleGranted(false); } } table.on('rowsDeleted allRowsDeleted', this._tableRowDeleteHandler); table.on('rowsInserted', this._tableRowInsertHandler); table.on('rowsUpdated', this._tableRowUpdateHandler); table.on('rowAction', this._tableRowActionHandler); table.on('rowOrderChanged', this._tableRowOrderChangeHandler); table.on('reload', this._tableDataLoadHandler); table.hasReloadHandler = true; table.insertMenus([scout.create(TableOrganizerMenu, {parent: table})]); table.on('propertyChange:tableControls', this._tableControlsChangeHandler); this._addSearchFormTableControlListeners(this._findSearchFormTableControl(table)); // Ensure _initDetailTableUiPreferences is only called after _initDetailTable has been completed (including subclasses). this.one('propertyChange:detailTable', event => { if (event.newValue === table) { this._initDetailTableUiPreferences(table); } }); } protected _initDetailTableUiPreferences(table: Table) { table.setUiPreferencesEnabled(true); } protected override _destroyDetailTable(table: Table) { table.off('rowsDeleted allRowsDeleted', this._tableRowDeleteHandler); table.off('rowsInserted', this._tableRowInsertHandler); table.off('rowsUpdated', this._tableRowUpdateHandler); table.off('rowAction', this._tableRowActionHandler); table.off('rowOrderChanged', this._tableRowOrderChangeHandler); table.off('reload', this._tableDataLoadHandler); table.off('propertyChange:tableControls', this._tableControlsChangeHandler); this._removeSearchFormTableControlListeners(this._findSearchFormTableControl(table)); super._destroyDetailTable(table); } protected _onTableRowsDeleted(event: TableRowsDeletedEvent | TableAllRowsDeletedEvent) { if (this.leaf) { // when page is a leaf we do nothing at all return; } const rows = arrays.ensure(event.rows); const childPages = []; rows.forEach(row => { const childPage = row.page; if (!childPage) { return; } childPage.unlinkWithRow(row); childPages.push(childPage); }); this.outline.mediator.onTableRowsDeleted(rows, childPages, this); } protected _onTableRowsInserted(event: TableRowsInsertedEvent) { if (this.leaf) { // when page is a leaf we do nothing at all return; } let rows = arrays.ensure(event.rows); let childPages = rows.map(row => this._createChildPageInternal(row)).filter(Boolean); this.outline.mediator.onTableRowsInserted(rows, childPages, this); } protected _onTableRowsUpdated(event: TableRowsUpdatedEvent) { this.outline.mediator.onTableRowsUpdated(event, this); } protected _onTableRowAction(event: TableRowActionEvent) { this.outline.mediator.onTableRowAction(event, this); } protected _onTableRowOrderChanged(event: TableRowOrderChangedEvent) { this.outline.mediator.onTableRowOrderChanged(event, this); } protected _onTableReload(event: TableReloadEvent) { if (this.expandedLazy) { // If the page is expanded lazily, all child nodes will be gone -> collapse it to prevent showing the "+" icon without any child nodes this.outline.setNodeExpanded(this, false); } this.loadTableData(event.reloadReason); } protected _onTableControlsChange(e: PropertyChangeEvent<TableControl[]>) { // disable search/reset listeners on old search form controls arrays.ensure(e.oldValue) .filter(tableControl => tableControl instanceof SearchFormTableControl) .forEach((searchFormTableControl: SearchFormTableControl) => this._removeSearchFormTableControlListeners(searchFormTableControl)); // enable search/reset listeners on search form control this._addSearchFormTableControlListeners(this.getSearchFormTableControl()); } /** * Adds search and reset listener to the given {@link SearchFormTableControl}. */ protected _addSearchFormTableControlListeners(searchFormTableControl: SearchFormTableControl) { searchFormTableControl?.on('search', this._searchFormTableControlSearchHandler); searchFormTableControl?.on('reset', this._searchFormTableControlResetHandler); } /** * Removes search and reset listener from the given {@link SearchFormTableControl}. */ protected _removeSearchFormTableControlListeners(searchFormTableControl: SearchFormTableControl) { searchFormTableControl?.off('search', this._searchFormTableControlSearchHandler); searchFormTableControl?.off('reset', this._searchFormTableControlResetHandler); } protected _onSearchFormTableControlSearch(e: Event<SearchFormTableControl>) { this.detailTable.reload(Table.ReloadReason.SEARCH); // close search table control after search if outline is compact, otherwise the search form covers the table if (this.outline.compact) { this.getSearchFormTableControl().setSelected(false); } } protected _onSearchFormTableControlReset(e: Event<SearchFormTableControl>) { this.detailTable.reload(Table.ReloadReason.SEARCH); } protected _createChildPageInternal(row: TableRow): Page { // noinspection JSDeprecatedSymbols let childPage = this.createChildPage(row); if (!childPage && this.alwaysCreateChildPage) { childPage = this.createDefaultChildPage(row); } if (childPage) { childPage.linkWithRow(row); childPage = childPage.updatePageFromTableRow(row); } return childPage; } /** * @deprecated use {@link _createChildPage} instead */ createChildPage(row: TableRow): Page { return this._createChildPage(row); } /** * Override this method to create a {@link Page} for the given {@link TableRow}. * * By default, no page is created unless {@link alwaysCreateChildPage} is set to true. * In that case, an {@link AutoLeafPageWithNodes} is created. */ protected _createChildPage(row: TableRow): Page { return null; } createDefaultChildPage(row: TableRow): Page { return scout.create(AutoLeafPageWithNodes, { parent: this.outline, row: row }); } override loadChildren(): JQuery.Promise<any> { this.ensureDetailTable(); // It's allowed to have no table - but we don't have to load data in that case if (!this.detailTable) { return $.resolvedPromise(); } this.childrenLoaded = false; const deferred = $.Deferred(); this.one('load error', e => deferred.resolve()); this.detailTable.reload(); return deferred.promise().then(() => { this.childrenLoaded = true; }); } protected _createSearchFilter(): any { return this.getSearchFilter(); } /** * Returns the {@link SearchFormTableControl} for the given {@link Table}, or `null` if no {@link SearchFormTableControl} is present. */ protected _findSearchFormTableControl(table: Table): SearchFormTableControl { return table?.findTableControl(SearchFormTableControl); } /** * Returns the {@link SearchFormTableControl} for this page, or `null` if no {@link SearchFormTableControl} is present. */ getSearchFormTableControl(): SearchFormTableControl { return this._findSearchFormTableControl(this.detailTable); } /** * Returns the search form for this page, or `null` if no search form is present. */ getSearchForm(): Form { return this.getSearchFormTableControl()?.form || null; } /** * Returns the exported data of the {@link #getSearchForm search form}, or `undefined` if no search form is present. */ getSearchFilter(): any { return this.getSearchForm()?.exportData(); } /** * Imports the given data into the {@link #getSearchForm search form}. If no search form is present, nothing happens. * * @param markAsSaved * If this optional parameter is set to `true`, the form state after the import is marked as the saved state, * i.e. pressing the reset button will revert the form to the new state. Otherwise, the saved state will not be * altered and pressing the reset button will revert the form to whatever was previously the saved state. */ setSearchFilter(searchFilter: any, markAsSaved?: boolean) { let searchForm = this.getSearchForm(); if (!searchForm) { return; } let oldData = searchForm.data; searchForm.setData(searchFilter); searchForm.importData(); if (markAsSaved) { searchForm.markAsSaved(); } else { // Because resetting the form not only resets every field but also loads the form again (see Lifecycle#reset), // to 'data' attribute has to be reverted back to the previous value. searchForm.setData(oldData); } } /** * Resets the {@link #getSearchForm search form} to its saved state. If no search form is present, nothing happens. */ resetSearchFilter() { this.getSearchForm()?.reset(); } /** * Adds a {@link MaxRowCountContributionDo} to the given request. * Typically, this method should be used before sending a request in {@link _loadTableData} to attach the row limit constraints (if existing). * The contribution is only added if there is a row limit. Otherwise, the request remains untouched. * @example * protected override _loadTableData(searchFilter: MyRestrictionDo): JQuery.Promise<MyResponseDo> { * const request = scout.create(MyRequestDo, { * ... * restriction: searchFilter * }); * return ajax.postDataObject(url, this._withMaxRowCountContribution(request)); * } * @param dataObject The {@link DoEntity} to which the contribution should be added. * @returns the resulting request with the added contribution. */ protected _withMaxRowCountContribution<T>(dataObject: T): T { return scout.create(TableMaxResultsHelper).withMaxRowCountContribution(dataObject, this.detailTable); } /** * see Java: AbstractPageWithTable#loadChildren that's where the table is reloaded and the tree is rebuilt, called by AbstractTree#P_UIFacade */ loadTableData(reloadReason?: TableReloadReason): JQuery.Promise<any> { this.ensureDetailTable(); this.detailTable.setLoading(true); const restoreSelectionInfo = this._getRestoreSelectionInfo(); return this._loadTableData(this._createSearchFilter()) .then(data => this._onLoadTableDataDone(data, restoreSelectionInfo)) .catch(error => this._onLoadTableDataFail(error, restoreSelectionInfo)); } /** * Get info needed to restore the selection after table data was loaded. * - {@link RestoreSelectionInfo.restoreSelection} is `true` if a child page of this page is currently selected. * - {@link RestoreSelectionInfo.selectedRowKey} is the row key (see {@link TableRow.getKeyValues}) of the row corresponding to the direct child page of this page that is currently selected or a parent of the currently selected page. */ protected _getRestoreSelectionInfo(): RestoreSelectionInfo { let restoreSelection = false; let selectedRowKey = null; if (this.outline.selectedNode()) { let node = this.outline.selectedNode(); while (node?.parentNode) { if (node.parentNode === this) { restoreSelection = true; selectedRowKey = node.row?.getKeyValues(); break; } node = node.parentNode; } } return {restoreSelection, selectedRowKey}; } /** * Restores the selection by the given {@link RestoreSelectionInfo}. If there is no selected page for the current outline, the following page will be selected: * 1. The page corresponding to the selected row of the detail table of this page. * 2. The page corresponding to the row found by the given former selected row key (@see {@link RestoreSelectionInfo}). * 3. This page. */ protected _restoreSelection(restoreSelectionInfo?: RestoreSelectionInfo) { if (!restoreSelectionInfo) { return; } try { const {restoreSelection, selectedRowKey} = restoreSelectionInfo; if (restoreSelection && !this.outline.selectedNode()) { let selectedNode = this.detailTable.selectedRow()?.page || this.detailTable.getRowByKey(selectedRowKey)?.page || this; this.outline.selectNode(selectedNode); } } catch (e) { $.log.warn('Unable to restore selection.', e); } } /** * Override this method to load table data (rows to be added to table). * * This is an asynchronous operation working with a Promise. If table data load is successful, * {@link _onLoadTableDataDone} will be called. If a failure occurs while loading table data, * {@link _onLoadTableDataFail} will be called. * * To return static data, use a resolved promise: `return $.resolvedPromise({...});` * * @param searchFilter The search filter as exported by the search form or null. */ protected _loadTableData(searchFilter: any): JQuery.Promise<any> { return $.resolvedPromise(); } /** * This method is called when table data load is successful. It should transform the table data * object to table rows and add them to the table. * * @param tableData data loaded by {@link _loadTableData} * @param restoreSelectionInfo information needed to restore the selection after table data was loaded */ protected _onLoadTableDataDone(tableData: any, restoreSelectionInfo?: RestoreSelectionInfo) { let success = false; try { const rows = arrays.ensure(this._transformTableDataToTableRows(tableData)); const limitedResultInfoDo = this._getLimitedResultInfoDo(tableData); this._readLimitedResultInfo(rows.length, limitedResultInfoDo); // apply properties from LimitedResultInfoDo to table (must be before replaceRows as this triggers the TableFooter update which already requires the new values). this.detailTable.replaceRows(rows); this.detailTable.setLimitedResultTableStatus(!!limitedResultInfoDo?.limitedResult); // set table status after replaceRows as the new rows are required success = true; } finally { this._onLoadTableDataAlways(restoreSelectionInfo); } if (success) { this.trigger('load'); } } protected _readLimitedResultInfo(numRows: number, limitedResultInfoDo?: LimitedResultInfoContributionDo) { if (!limitedResultInfoDo) { return; } // update table properties. The footer is automatically updated after the new rows have been created if (scout.create(TableMaxResultsHelper).isLoadMoreDataPossible(numRows, limitedResultInfoDo.estimatedRowCount, limitedResultInfoDo.maxRowCount)) { // only update if the next load would be a ReloadReason.OVERRIDE_ROW_LIMIT so that the new limit is used this.detailTable.setMaxRowCount(limitedResultInfoDo.maxRowCount); } this.detailTable.setEstimatedRowCount(limitedResultInfoDo.estimatedRowCount); } protected _getLimitedResultInfoDo(tableData: any): LimitedResultInfoContributionDo { return dataObjects.getContribution('scout.LimitedResultInfoContribution', tableData) as LimitedResultInfoContributionDo; } protected _onLoadTableDataFail(error: any, restoreSelectionInfo?: RestoreSelectionInfo) { try { this.detailTable.setTableStatus(Status.error({ message: this.session.text('ErrorWhileLoadingData') })); $.log.error('Failed to load tableData. error=', error); this.detailTable.deleteAllRows(); } finally { this._onLoadTableDataAlways(restoreSelectionInfo); this.trigger('error', {error}); } } protected _onLoadTableDataAlways(restoreSelectionInfo?: RestoreSelectionInfo) { this._restoreSelection(restoreSelectionInfo); this.detailTable.setLoading(false); } /** * This method converts the loaded table data, which can be any object, into table rows. * You must override this method unless tableData is already an array of table rows. */ protected _transformTableDataToTableRows(tableData: any): ObjectOrModel<TableRow>[] { return tableData; } override getTableRowIdentifier(row: TableRow, allowObjectFallback = false): BookmarkTableRowIdentifierDo { return BookmarkSupport.get(this.session).createTableRowIdentifier(this, row, allowObjectFallback); } } /** * Object containing the info needed to restore the selection after table data was loaded. */ export type RestoreSelectionInfo = { /** * Whether the selection should be restored or not. */ restoreSelection: boolean; /** * Former selected row key. */ selectedRowKey: any[]; };