mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Enable Record Cards
Summary: Adds remaining functionality, fixes, and polish to Record Cards and removes their feature flag, enabling them by default. Test Plan: Tests deferred; will be included in a follow-up diff. Reviewers: jarek, paulfitz Reviewed By: jarek Subscribers: paulfitz, jarek Differential Revision: https://phab.getgrist.com/D4121
This commit is contained in:
parent
84329404a4
commit
707a8c7b32
@ -274,7 +274,9 @@ BaseView.prototype.deleteRecords = function(source) {
|
|||||||
buildConfirmDelete(selectedCell, onSave, rowIds.length <= 1);
|
buildConfirmDelete(selectedCell, onSave, rowIds.length <= 1);
|
||||||
} else {
|
} else {
|
||||||
onSave().then(() => {
|
onSave().then(() => {
|
||||||
reportUndo(this.gristDoc, `You deleted ${rowIds.length} row${rowIds.length > 1 ? 's' : ''}.`);
|
if (!this.isDisposed()) {
|
||||||
|
reportUndo(this.gristDoc, `You deleted ${rowIds.length} row${rowIds.length > 1 ? 's' : ''}.`);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import {GristDoc} from 'app/client/components/GristDoc';
|
|||||||
import {copyToClipboard} from 'app/client/lib/clipboardUtils';
|
import {copyToClipboard} from 'app/client/lib/clipboardUtils';
|
||||||
import {setTestState} from 'app/client/lib/testState';
|
import {setTestState} from 'app/client/lib/testState';
|
||||||
import {TableRec} from 'app/client/models/DocModel';
|
import {TableRec} from 'app/client/models/DocModel';
|
||||||
import {RECORD_CARDS} from 'app/client/models/features';
|
|
||||||
import {docListHeader, docMenuTrigger} from 'app/client/ui/DocMenuCss';
|
import {docListHeader, docMenuTrigger} from 'app/client/ui/DocMenuCss';
|
||||||
import {duplicateTable, DuplicateTableResponse} from 'app/client/ui/DuplicateTable';
|
import {duplicateTable, DuplicateTableResponse} from 'app/client/ui/DuplicateTable';
|
||||||
import {hoverTooltip, showTransientTooltip} from 'app/client/ui/tooltips';
|
import {hoverTooltip, showTransientTooltip} from 'app/client/ui/tooltips';
|
||||||
@ -99,13 +98,14 @@ export class DataTables extends Disposable {
|
|||||||
hoverTooltip(
|
hoverTooltip(
|
||||||
dom.domComputed(use => use(use(tableRec.recordCardViewSection).disabled)
|
dom.domComputed(use => use(use(tableRec.recordCardViewSection).disabled)
|
||||||
? t('Record Card Disabled')
|
? t('Record Card Disabled')
|
||||||
: t('Record Card')),
|
: t('Edit Record Card')),
|
||||||
{key: DATA_TABLES_TOOLTIP_KEY, closeOnClick: false}
|
{key: DATA_TABLES_TOOLTIP_KEY, closeOnClick: false}
|
||||||
),
|
),
|
||||||
dom.hide(!RECORD_CARDS()),
|
dom.hide(this._gristDoc.isReadonly),
|
||||||
// Make the button invisible to maintain consistent alignment with non-summary tables.
|
// Make the button invisible to maintain consistent alignment with non-summary tables.
|
||||||
dom.style('visibility', u => u(tableRec.summarySourceTable) === 0 ? 'visible' : 'hidden'),
|
dom.style('visibility', u => u(tableRec.summarySourceTable) === 0 ? 'visible' : 'hidden'),
|
||||||
cssRecordCardButton.cls('-disabled', use => use(use(tableRec.recordCardViewSection).disabled)),
|
cssRecordCardButton.cls('-disabled', use => use(use(tableRec.recordCardViewSection).disabled)),
|
||||||
|
testId('table-record-card'),
|
||||||
),
|
),
|
||||||
cssDotsButton(
|
cssDotsButton(
|
||||||
testId('table-menu'),
|
testId('table-menu'),
|
||||||
@ -120,7 +120,8 @@ export class DataTables extends Disposable {
|
|||||||
throw new Error(`Table ${tableRec.tableId.peek()} doesn't have a raw view section.`);
|
throw new Error(`Table ${tableRec.tableId.peek()} doesn't have a raw view section.`);
|
||||||
}
|
}
|
||||||
this._gristDoc.viewModel.activeSectionId(sectionId);
|
this._gristDoc.viewModel.activeSectionId(sectionId);
|
||||||
})
|
}),
|
||||||
|
cssTable.cls('-readonly', this._gristDoc.isReadonly),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
@ -132,7 +133,8 @@ export class DataTables extends Disposable {
|
|||||||
return dom.domComputed((use) => {
|
return dom.domComputed((use) => {
|
||||||
const rawViewSectionRef = use(fromKo(table.rawViewSectionRef));
|
const rawViewSectionRef = use(fromKo(table.rawViewSectionRef));
|
||||||
const isSummaryTable = use(table.summarySourceTable) !== 0;
|
const isSummaryTable = use(table.summarySourceTable) !== 0;
|
||||||
if (!rawViewSectionRef || isSummaryTable) {
|
const isReadonly = use(this._gristDoc.isReadonly);
|
||||||
|
if (!rawViewSectionRef || isSummaryTable || isReadonly) {
|
||||||
// Some very old documents might not have a rawViewSection, and raw summary
|
// Some very old documents might not have a rawViewSection, and raw summary
|
||||||
// tables can't currently be renamed.
|
// tables can't currently be renamed.
|
||||||
const tableName = [
|
const tableName = [
|
||||||
@ -185,7 +187,7 @@ export class DataTables extends Disposable {
|
|||||||
)),
|
)),
|
||||||
testId('menu-remove-table'),
|
testId('menu-remove-table'),
|
||||||
),
|
),
|
||||||
dom.maybe(use => RECORD_CARDS() && use(table.summarySourceTable) === 0, () => [
|
dom.maybe(use => use(table.summarySourceTable) === 0, () => [
|
||||||
menuDivider(),
|
menuDivider(),
|
||||||
menuItem(
|
menuItem(
|
||||||
() => this._editRecordCard(table),
|
() => this._editRecordCard(table),
|
||||||
@ -308,6 +310,10 @@ const cssTable = styled('div', `
|
|||||||
&:hover {
|
&:hover {
|
||||||
border-color: ${css.theme.rawDataTableBorderHover};
|
border-color: ${css.theme.rawDataTableBorderHover};
|
||||||
}
|
}
|
||||||
|
&-readonly {
|
||||||
|
/* Row count column is hidden when document is read-only. */
|
||||||
|
grid-template-columns: 16px auto 56px;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssTableIcon = styled('div', `
|
const cssTableIcon = styled('div', `
|
||||||
|
@ -33,6 +33,7 @@ function DetailView(gristDoc, viewSectionModel) {
|
|||||||
|
|
||||||
this.viewFields = gristDoc.docModel.viewFields;
|
this.viewFields = gristDoc.docModel.viewFields;
|
||||||
this._isSingle = (this.viewSection.parentKey.peek() === 'single');
|
this._isSingle = (this.viewSection.parentKey.peek() === 'single');
|
||||||
|
this._isExternalSectionPopup = gristDoc.externalSectionId.get() === this.viewSection.id();
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// Create and attach the DOM for the view.
|
// Create and attach the DOM for the view.
|
||||||
@ -191,7 +192,9 @@ DetailView.prototype.deleteRows = async function(rowIds) {
|
|||||||
try {
|
try {
|
||||||
await BaseView.prototype.deleteRows.call(this, rowIds);
|
await BaseView.prototype.deleteRows.call(this, rowIds);
|
||||||
} finally {
|
} finally {
|
||||||
this.cursor.rowIndex(index);
|
if (!this.isDisposed()) {
|
||||||
|
this.cursor.rowIndex(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -365,7 +368,13 @@ DetailView.prototype.buildTitleControls = function() {
|
|||||||
// the controls can be confusing in this case.
|
// the controls can be confusing in this case.
|
||||||
// Note that the controls should still be visible with a filter link.
|
// Note that the controls should still be visible with a filter link.
|
||||||
const showControls = ko.computed(() => {
|
const showControls = ko.computed(() => {
|
||||||
if (!this._isSingle || this.recordLayout.layoutEditor()) { return false; }
|
if (
|
||||||
|
!this._isSingle||
|
||||||
|
this.recordLayout.layoutEditor() ||
|
||||||
|
this._isExternalSectionPopup
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const linkingState = this.viewSection.linkingState();
|
const linkingState = this.viewSection.linkingState();
|
||||||
return !(linkingState && Boolean(linkingState.cursorPos));
|
return !(linkingState && Boolean(linkingState.cursorPos));
|
||||||
});
|
});
|
||||||
|
@ -56,7 +56,6 @@ const {CombinedStyle} = require("app/client/models/Styles");
|
|||||||
const {buildRenameColumn} = require('app/client/ui/ColumnTitle');
|
const {buildRenameColumn} = require('app/client/ui/ColumnTitle');
|
||||||
const {makeT} = require('app/client/lib/localization');
|
const {makeT} = require('app/client/lib/localization');
|
||||||
const {reportError} = require('app/client/models/AppModel');
|
const {reportError} = require('app/client/models/AppModel');
|
||||||
const {RECORD_CARDS} = require('app/client/models/features');
|
|
||||||
const {urlState} = require('app/client/models/gristUrlState');
|
const {urlState} = require('app/client/models/gristUrlState');
|
||||||
|
|
||||||
const t = makeT('GridView');
|
const t = makeT('GridView');
|
||||||
@ -375,13 +374,15 @@ GridView.gridCommands = {
|
|||||||
this.viewSection.rawNumFrozen.setAndSave(action.numFrozen);
|
this.viewSection.rawNumFrozen.setAndSave(action.numFrozen);
|
||||||
},
|
},
|
||||||
viewAsCard() {
|
viewAsCard() {
|
||||||
if (!RECORD_CARDS()) { return; }
|
|
||||||
if (this._isRecordCardDisabled()) { return; }
|
if (this._isRecordCardDisabled()) { return; }
|
||||||
|
|
||||||
const selectedRows = this.selectedRows();
|
const selectedRows = this.selectedRows();
|
||||||
|
if (selectedRows.length !== 1) { return; }
|
||||||
|
|
||||||
|
const colRef = this.viewSection.viewFields().at(this.cursor.fieldIndex()).column().id();
|
||||||
const rowId = selectedRows[0];
|
const rowId = selectedRows[0];
|
||||||
const sectionId = this.viewSection.tableRecordCard().id();
|
const sectionId = this.viewSection.tableRecordCard().id();
|
||||||
const anchorUrlState = {hash: {rowId, sectionId, recordCard: true}};
|
const anchorUrlState = {hash: {colRef, rowId, sectionId, recordCard: true}};
|
||||||
urlState().pushUrl(anchorUrlState, {replace: true}).catch(reportError);
|
urlState().pushUrl(anchorUrlState, {replace: true}).catch(reportError);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -209,6 +209,8 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
private _showBackgroundVideoPlayer: Observable<boolean> = Observable.create(this, false);
|
private _showBackgroundVideoPlayer: Observable<boolean> = Observable.create(this, false);
|
||||||
private _backgroundVideoPlayerHolder: Holder<YouTubePlayer> = Holder.create(this);
|
private _backgroundVideoPlayerHolder: Holder<YouTubePlayer> = Holder.create(this);
|
||||||
private _disableAutoStartingTours: boolean = false;
|
private _disableAutoStartingTours: boolean = false;
|
||||||
|
private _isShowingPopupSection = false;
|
||||||
|
private _prevSectionId: number | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly app: App,
|
public readonly app: App,
|
||||||
@ -565,6 +567,13 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
commands.allCommands.viewTabFocus.run();
|
commands.allCommands.viewTabFocus.run();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.autoDispose(this._popupSectionOptions.addListener((popupOptions) => {
|
||||||
|
if (!popupOptions) {
|
||||||
|
this._isShowingPopupSection = false;
|
||||||
|
this._prevSectionId = null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -616,10 +625,16 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
// In case the section is removed, close the popup.
|
// In case the section is removed, close the popup.
|
||||||
content.viewSection.autoDispose({dispose: content.close});
|
content.viewSection.autoDispose({dispose: content.close});
|
||||||
|
|
||||||
const {recordCard} = content.hash;
|
const {recordCard, rowId} = content.hash;
|
||||||
if (recordCard) {
|
if (recordCard) {
|
||||||
|
if (!rowId || rowId === 'new') {
|
||||||
|
// Should be unreachable, but just to be sure (and to satisfy type checking)...
|
||||||
|
throw new Error('Unable to open Record Card: undefined row id');
|
||||||
|
}
|
||||||
|
|
||||||
return dom.create(RecordCardPopup, {
|
return dom.create(RecordCardPopup, {
|
||||||
gristDoc: this,
|
gristDoc: this,
|
||||||
|
rowId,
|
||||||
viewSection: content.viewSection,
|
viewSection: content.viewSection,
|
||||||
onClose: content.close,
|
onClose: content.close,
|
||||||
});
|
});
|
||||||
@ -629,7 +644,15 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
}) :
|
}) :
|
||||||
dom.create((owner) => {
|
dom.create((owner) => {
|
||||||
this.viewLayout = ViewLayout.create(owner, this, content);
|
this.viewLayout = ViewLayout.create(owner, this, content);
|
||||||
this.viewLayout.maximized.addListener(n => this.maximizedSectionId.set(n));
|
this.viewLayout.maximized.addListener(sectionId => {
|
||||||
|
this.maximizedSectionId.set(sectionId);
|
||||||
|
|
||||||
|
if (sectionId === null && !this._isShowingPopupSection) {
|
||||||
|
// If we didn't navigate to another section in the popup, move focus
|
||||||
|
// back to the previous section.
|
||||||
|
this._focusPreviousSection();
|
||||||
|
}
|
||||||
|
});
|
||||||
owner.onDispose(() => this.viewLayout = null);
|
owner.onDispose(() => this.viewLayout = null);
|
||||||
return this.viewLayout;
|
return this.viewLayout;
|
||||||
})
|
})
|
||||||
@ -1290,11 +1313,11 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
if (!hash.sectionId) {
|
if (!hash.sectionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this._prevSectionId) {
|
||||||
|
this._prevSectionId = this.viewModel.activeSection.peek().id();
|
||||||
|
}
|
||||||
// We might open popup either for a section in this view or some other section (like Raw Data Page).
|
// We might open popup either for a section in this view or some other section (like Raw Data Page).
|
||||||
if (this.viewModel.viewSections.peek().peek().some(s => s.id.peek() === hash.sectionId)) {
|
if (this.viewModel.viewSections.peek().peek().some(s => s.id.peek() === hash.sectionId)) {
|
||||||
if (this.viewLayout) {
|
|
||||||
this.viewLayout.previousSectionId = this.viewModel.activeSectionId.peek();
|
|
||||||
}
|
|
||||||
this.viewModel.activeSectionId(hash.sectionId);
|
this.viewModel.activeSectionId(hash.sectionId);
|
||||||
// If the anchor link is valid, set the cursor.
|
// If the anchor link is valid, set the cursor.
|
||||||
if (hash.colRef && hash.rowId) {
|
if (hash.colRef && hash.rowId) {
|
||||||
@ -1308,10 +1331,10 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
this.viewLayout?.maximized.set(hash.sectionId);
|
this.viewLayout?.maximized.set(hash.sectionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._isShowingPopupSection = true;
|
||||||
// We will borrow active viewModel and will trick him into believing that
|
// We will borrow active viewModel and will trick him into believing that
|
||||||
// the section from the link is his viewSection and it is active. Fortunately
|
// the section from the link is his viewSection and it is active. Fortunately
|
||||||
// he doesn't care. After popup is closed, we will restore the original.
|
// he doesn't care. After popup is closed, we will restore the original.
|
||||||
const prevSection = this.viewModel.activeSection.peek();
|
|
||||||
this.viewModel.activeSectionId(hash.sectionId);
|
this.viewModel.activeSectionId(hash.sectionId);
|
||||||
// Now we have view section we want to show in the popup.
|
// Now we have view section we want to show in the popup.
|
||||||
const popupSection = this.viewModel.activeSection.peek();
|
const popupSection = this.viewModel.activeSection.peek();
|
||||||
@ -1329,20 +1352,17 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
if (!this._popupSectionOptions.get()) {
|
if (!this._popupSectionOptions.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (popupSection !== prevSection) {
|
if (popupSection.id() !== this._prevSectionId) {
|
||||||
// We need to blur the popup section. Otherwise it will automatically be opened
|
// We need to blur the popup section. Otherwise it will automatically be opened
|
||||||
// on raw data view. Note: raw data and record card sections don't have parent views;
|
// on raw data view. Note: raw data and record card sections don't have parent views;
|
||||||
// they use the empty row model as a parent (which feels like a hack).
|
// they use the empty row model as a parent (which feels like a hack).
|
||||||
if (!popupSection.isDisposed()) {
|
if (!popupSection.isDisposed()) {
|
||||||
popupSection.hasFocus(false);
|
popupSection.hasFocus(false);
|
||||||
}
|
}
|
||||||
// We need to restore active viewSection for a view that we borrowed.
|
|
||||||
// When this popup was opened we tricked active view by setting its activeViewSection
|
// When this popup was opened we tricked active view by setting its activeViewSection
|
||||||
// to our viewSection (which might be a completely diffrent section or a raw data section) not
|
// to our viewSection (which might be a completely different section or a raw data section) not
|
||||||
// connected to this view.
|
// connected to this view. We need to return focus back to the previous section.
|
||||||
if (!prevSection.isDisposed()) {
|
this._focusPreviousSection();
|
||||||
prevSection.hasFocus(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Clearing popup section data will close this popup.
|
// Clearing popup section data will close this popup.
|
||||||
this._popupSectionOptions.set(null);
|
this._popupSectionOptions.set(null);
|
||||||
@ -1401,6 +1421,19 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
this._showBackgroundVideoPlayer.set(false);
|
this._showBackgroundVideoPlayer.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _focusPreviousSection() {
|
||||||
|
const prevSectionId = this._prevSectionId;
|
||||||
|
if (!prevSectionId) { return; }
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.viewModel.viewSections.peek().all().some(s =>
|
||||||
|
!s.isDisposed() && s.id.peek() === prevSectionId)
|
||||||
|
) {
|
||||||
|
this.viewModel.activeSectionId(prevSectionId);
|
||||||
|
}
|
||||||
|
this._prevSectionId = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for a view to be ready
|
* Waits for a view to be ready
|
||||||
*/
|
*/
|
||||||
|
@ -2,22 +2,27 @@ import {buildViewSectionDom} from 'app/client/components/buildViewSectionDom';
|
|||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {cssCloseButton, cssOverlay} from 'app/client/components/RawDataPage';
|
import {cssCloseButton, cssOverlay} from 'app/client/components/RawDataPage';
|
||||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
|
||||||
import {ViewSectionHelper} from 'app/client/components/ViewLayout';
|
import {ViewSectionHelper} from 'app/client/components/ViewLayout';
|
||||||
|
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
|
import {ChangeType, RowList} from 'app/client/models/rowset';
|
||||||
import {theme} from 'app/client/ui2018/cssVars';
|
import {theme} from 'app/client/ui2018/cssVars';
|
||||||
import {Disposable, dom, makeTestId, styled} from 'grainjs';
|
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||||
|
import {dom, makeTestId, styled} from 'grainjs';
|
||||||
|
|
||||||
const testId = makeTestId('test-record-card-popup-');
|
const testId = makeTestId('test-record-card-popup-');
|
||||||
|
|
||||||
interface RecordCardPopupOptions {
|
interface RecordCardPopupOptions {
|
||||||
gristDoc: GristDoc;
|
gristDoc: GristDoc;
|
||||||
|
rowId: number;
|
||||||
viewSection: ViewSectionRec;
|
viewSection: ViewSectionRec;
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecordCardPopup extends Disposable {
|
export class RecordCardPopup extends DisposableWithEvents {
|
||||||
private _gristDoc = this._options.gristDoc;
|
private _gristDoc = this._options.gristDoc;
|
||||||
|
private _rowId = this._options.rowId;
|
||||||
private _viewSection = this._options.viewSection;
|
private _viewSection = this._options.viewSection;
|
||||||
|
private _tableModel = this._gristDoc.getTableModel(this._viewSection.table().tableId());
|
||||||
private _handleClose = this._options.onClose;
|
private _handleClose = this._options.onClose;
|
||||||
|
|
||||||
constructor(private _options: RecordCardPopupOptions) {
|
constructor(private _options: RecordCardPopupOptions) {
|
||||||
@ -26,6 +31,11 @@ export class RecordCardPopup extends Disposable {
|
|||||||
cancel: () => { this._handleClose(); },
|
cancel: () => { this._handleClose(); },
|
||||||
};
|
};
|
||||||
this.autoDispose(commands.createGroup(commandGroup, this, true));
|
this.autoDispose(commands.createGroup(commandGroup, this, true));
|
||||||
|
|
||||||
|
// Close the popup if the underlying row is removed.
|
||||||
|
const onRowChange = this._onRowChange.bind(this);
|
||||||
|
this._tableModel.on('rowChange', onRowChange);
|
||||||
|
this.onDispose(() => this._tableModel.off('rowChange', onRowChange));
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
@ -39,7 +49,6 @@ export class RecordCardPopup extends Disposable {
|
|||||||
draggable: false,
|
draggable: false,
|
||||||
focusable: false,
|
focusable: false,
|
||||||
renamable: false,
|
renamable: false,
|
||||||
hideTitleControls: true,
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
cssCloseButton('CrossBig',
|
cssCloseButton('CrossBig',
|
||||||
@ -49,6 +58,12 @@ export class RecordCardPopup extends Disposable {
|
|||||||
dom.on('click', (ev, elem) => void (ev.target === elem ? this._handleClose() : null)),
|
dom.on('click', (ev, elem) => void (ev.target === elem ? this._handleClose() : null)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _onRowChange(type: ChangeType, rows: RowList) {
|
||||||
|
if (type === 'remove' && [...rows].includes(this._rowId)) {
|
||||||
|
this._handleClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssSectionWrapper = styled('div', `
|
const cssSectionWrapper = styled('div', `
|
||||||
|
@ -84,7 +84,6 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
|
|||||||
public viewModel: ViewRec;
|
public viewModel: ViewRec;
|
||||||
public layoutSpec: ko.Computed<BoxSpec>;
|
public layoutSpec: ko.Computed<BoxSpec>;
|
||||||
public maximized: Observable<number|null>;
|
public maximized: Observable<number|null>;
|
||||||
public previousSectionId = 0; // Used to restore focus after a maximized section is closed.
|
|
||||||
public isResizing = Observable.create(this, false);
|
public isResizing = Observable.create(this, false);
|
||||||
public layout: Layout;
|
public layout: Layout;
|
||||||
public layoutEditor: LayoutEditor;
|
public layoutEditor: LayoutEditor;
|
||||||
@ -203,16 +202,6 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
|
|||||||
// If we are closing popup, resize all sections.
|
// If we are closing popup, resize all sections.
|
||||||
if (!sectionId) {
|
if (!sectionId) {
|
||||||
this._onResize();
|
this._onResize();
|
||||||
// Reset active section to the first one if the section is popup is collapsed.
|
|
||||||
if (prev
|
|
||||||
&& this.viewModel.activeCollapsedSections.peek().includes(prev)
|
|
||||||
&& this.previousSectionId) {
|
|
||||||
// Make sure that previous section exists still.
|
|
||||||
if (this.viewModel.viewSections.peek().all()
|
|
||||||
.some(s => !s.isDisposed() && s.id.peek() === this.previousSectionId)) {
|
|
||||||
this.viewModel.activeSectionId(this.previousSectionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise resize only active one (the one in popup).
|
// Otherwise resize only active one (the one in popup).
|
||||||
const section = this.viewModel.activeSection.peek();
|
const section = this.viewModel.activeSection.peek();
|
||||||
|
@ -78,7 +78,6 @@ export function buildViewSectionDom(options: {
|
|||||||
tableNameHidden,
|
tableNameHidden,
|
||||||
widgetNameHidden,
|
widgetNameHidden,
|
||||||
renamable = true,
|
renamable = true,
|
||||||
hideTitleControls = false,
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Creating normal section dom
|
// Creating normal section dom
|
||||||
@ -110,7 +109,7 @@ export function buildViewSectionDom(options: {
|
|||||||
testId('viewsection-title'),
|
testId('viewsection-title'),
|
||||||
cssTestClick(testId("viewsection-blank")),
|
cssTestClick(testId("viewsection-blank")),
|
||||||
),
|
),
|
||||||
hideTitleControls ? null : viewInstance.buildTitleControls(),
|
viewInstance.buildTitleControls(),
|
||||||
dom('div.viewsection_buttons',
|
dom('div.viewsection_buttons',
|
||||||
dom.create(viewSectionMenu, gristDoc, vs)
|
dom.create(viewSectionMenu, gristDoc, vs)
|
||||||
)
|
)
|
||||||
|
@ -33,7 +33,3 @@ export function PERMITTED_CUSTOM_WIDGETS(): Observable<string[]> {
|
|||||||
}
|
}
|
||||||
return G.window.PERMITTED_CUSTOM_WIDGETS;
|
return G.window.PERMITTED_CUSTOM_WIDGETS;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RECORD_CARDS() {
|
|
||||||
return Boolean(getGristConfig().experimentalPlugins);
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { allCommands } from 'app/client/components/commands';
|
import { allCommands } from 'app/client/components/commands';
|
||||||
import { makeT } from 'app/client/lib/localization';
|
import { makeT } from 'app/client/lib/localization';
|
||||||
import { RECORD_CARDS } from 'app/client/models/features';
|
|
||||||
import { menuDivider, menuIcon, menuItemCmd, menuItemCmdLabel } from 'app/client/ui2018/menus';
|
import { menuDivider, menuIcon, menuItemCmd, menuItemCmdLabel } from 'app/client/ui2018/menus';
|
||||||
import { dom } from 'grainjs';
|
import { dom } from 'grainjs';
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ export function RowContextMenu({
|
|||||||
numRows
|
numRows
|
||||||
}: IRowContextMenu) {
|
}: IRowContextMenu) {
|
||||||
const result: Element[] = [];
|
const result: Element[] = [];
|
||||||
if (RECORD_CARDS() && numRows === 1) {
|
if (numRows === 1) {
|
||||||
result.push(
|
result.push(
|
||||||
menuItemCmd(
|
menuItemCmd(
|
||||||
allCommands.viewAsCard,
|
allCommands.viewAsCard,
|
||||||
|
@ -2,7 +2,6 @@ import {makeT} from 'app/client/lib/localization';
|
|||||||
import {DataRowModel} from 'app/client/models/DataRowModel';
|
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||||
import {TableRec} from 'app/client/models/DocModel';
|
import {TableRec} from 'app/client/models/DocModel';
|
||||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||||
import {RECORD_CARDS} from 'app/client/models/features';
|
|
||||||
import {urlState} from 'app/client/models/gristUrlState';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||||
import {hideInPrintView, testId, theme} from 'app/client/ui2018/cssVars';
|
import {hideInPrintView, testId, theme} from 'app/client/ui2018/cssVars';
|
||||||
@ -20,7 +19,7 @@ const t = makeT('Reference');
|
|||||||
* Reference - The widget for displaying references to another table's records.
|
* Reference - The widget for displaying references to another table's records.
|
||||||
*/
|
*/
|
||||||
export class Reference extends NTextBox {
|
export class Reference extends NTextBox {
|
||||||
private _refTable: Computed<TableRec | null>;
|
protected _refTable: Computed<TableRec | null>;
|
||||||
private _visibleColRef: Computed<number>;
|
private _visibleColRef: Computed<number>;
|
||||||
private _validCols: Computed<Array<IOptionFull<number>>>;
|
private _validCols: Computed<Array<IOptionFull<number>>>;
|
||||||
|
|
||||||
@ -120,23 +119,20 @@ export class Reference extends NTextBox {
|
|||||||
dom.cls('text_wrapping', this.wrapping),
|
dom.cls('text_wrapping', this.wrapping),
|
||||||
cssRefIcon('FieldReference',
|
cssRefIcon('FieldReference',
|
||||||
cssRefIcon.cls('-view-as-card', use =>
|
cssRefIcon.cls('-view-as-card', use =>
|
||||||
RECORD_CARDS() && use(referenceId) !== 0 && use(formattedValue).hasRecordCard),
|
use(referenceId) !== 0 && use(formattedValue).hasRecordCard),
|
||||||
dom.on('click', async () => {
|
dom.on('click', async () => {
|
||||||
if (!RECORD_CARDS()) { return; }
|
|
||||||
if (referenceId.get() === 0 || !formattedValue.get().hasRecordCard) { return; }
|
if (referenceId.get() === 0 || !formattedValue.get().hasRecordCard) { return; }
|
||||||
|
|
||||||
const rowId = referenceId.get() as UIRowId;
|
const rowId = referenceId.get() as UIRowId;
|
||||||
const sectionId = this._refTable.get()?.recordCardViewSectionRef();
|
const sectionId = this._refTable.get()?.recordCardViewSectionRef();
|
||||||
if (sectionId === undefined) {
|
if (sectionId === undefined) {
|
||||||
throw new Error('Unable to find Record Card section');
|
throw new Error('Unable to open Record Card: undefined section id');
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchorUrlState = {hash: {rowId, sectionId, recordCard: true}};
|
const anchorUrlState = {hash: {rowId, sectionId, recordCard: true}};
|
||||||
await urlState().pushUrl(anchorUrlState, {replace: true});
|
await urlState().pushUrl(anchorUrlState, {replace: true});
|
||||||
}),
|
}),
|
||||||
dom.on('mousedown', (ev) => {
|
dom.on('mousedown', (ev) => {
|
||||||
if (!RECORD_CARDS()) { return; }
|
|
||||||
|
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}),
|
}),
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import {DataRowModel} from 'app/client/models/DataRowModel';
|
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||||
import {testId} from 'app/client/ui2018/cssVars';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
|
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {isList} from 'app/common/gristTypes';
|
import {isList} from 'app/common/gristTypes';
|
||||||
import {dom} from 'grainjs';
|
import {Computed, dom, styled} from 'grainjs';
|
||||||
import {cssChoiceList, cssToken} from "app/client/widgets/ChoiceListCell";
|
import {cssChoiceList, cssToken} from "app/client/widgets/ChoiceListCell";
|
||||||
import {Reference} from "app/client/widgets/Reference";
|
import {Reference} from "app/client/widgets/Reference";
|
||||||
import {choiceToken} from "app/client/widgets/ChoiceToken";
|
import {choiceToken} from "app/client/widgets/ChoiceToken";
|
||||||
@ -10,24 +12,33 @@ import {choiceToken} from "app/client/widgets/ChoiceToken";
|
|||||||
* ReferenceList - The widget for displaying lists of references to another table's records.
|
* ReferenceList - The widget for displaying lists of references to another table's records.
|
||||||
*/
|
*/
|
||||||
export class ReferenceList extends Reference {
|
export class ReferenceList extends Reference {
|
||||||
|
private _hasRecordCard = Computed.create(this, (use) => {
|
||||||
|
const table = use(this._refTable);
|
||||||
|
if (!table) { return false; }
|
||||||
|
|
||||||
|
return !use(use(table.recordCardViewSection).disabled);
|
||||||
|
});
|
||||||
|
|
||||||
public buildDom(row: DataRowModel) {
|
public buildDom(row: DataRowModel) {
|
||||||
return cssChoiceList(
|
return cssChoiceList(
|
||||||
dom.cls('field_clip'),
|
dom.cls('field_clip'),
|
||||||
cssChoiceList.cls('-wrap', this.wrapping),
|
cssChoiceList.cls('-wrap', this.wrapping),
|
||||||
dom.style('justify-content', use => use(this.alignment) === 'right' ? 'flex-end' : use(this.alignment)),
|
dom.style('justify-content', use => use(this.alignment) === 'right' ? 'flex-end' : use(this.alignment)),
|
||||||
dom.domComputed((use) => {
|
dom.domComputed((use) => {
|
||||||
|
|
||||||
if (use(row._isAddRow) || this.isDisposed() || use(this.field.displayColModel).isDisposed()) {
|
if (use(row._isAddRow) || this.isDisposed() || use(this.field.displayColModel).isDisposed()) {
|
||||||
// Work around JS errors during certain changes (noticed when visibleCol field gets removed
|
// Work around JS errors during certain changes (noticed when visibleCol field gets removed
|
||||||
// for a column using per-field settings).
|
// for a column using per-field settings).
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const value = row.cells[use(use(this.field.displayColModel).colId)];
|
|
||||||
if (!value) {
|
const valueObs = row.cells[use(this.field.colId)];
|
||||||
return null;
|
const value = valueObs && use(valueObs);
|
||||||
}
|
if (!value) { return null; }
|
||||||
const content = use(value);
|
|
||||||
if (!content) { return null; }
|
const displayValueObs = row.cells[use(use(this.field.displayColModel).colId)];
|
||||||
|
const displayValue = displayValueObs && use(displayValueObs);
|
||||||
|
if (!displayValue) { return null; }
|
||||||
|
|
||||||
// TODO: Figure out what the implications of this block are for ReferenceList.
|
// TODO: Figure out what the implications of this block are for ReferenceList.
|
||||||
// if (isVersions(content)) {
|
// if (isVersions(content)) {
|
||||||
// // We can arrive here if the reference value is unchanged (viewed as a foreign key)
|
// // We can arrive here if the reference value is unchanged (viewed as a foreign key)
|
||||||
@ -36,20 +47,51 @@ export class ReferenceList extends Reference {
|
|||||||
// // just showing one version of the cell. TODO: elaborate.
|
// // just showing one version of the cell. TODO: elaborate.
|
||||||
// return use(this._formatValue)(content[1].local || content[1].parent);
|
// return use(this._formatValue)(content[1].local || content[1].parent);
|
||||||
// }
|
// }
|
||||||
const items = isList(content) ? content.slice(1) : [content];
|
const values = isList(value) ? value.slice(1) : [value];
|
||||||
|
const displayValues = isList(displayValue) ? displayValue.slice(1) : [displayValue];
|
||||||
// Use field.visibleColFormatter instead of field.formatter
|
// Use field.visibleColFormatter instead of field.formatter
|
||||||
// because we're formatting each list element to render tokens, not the whole list.
|
// because we're formatting each list element to render tokens, not the whole list.
|
||||||
const formatter = use(this.field.visibleColFormatter);
|
const formatter = use(this.field.visibleColFormatter);
|
||||||
return items.map(item => formatter.formatAny(item));
|
return values.map((referenceId, i) => {
|
||||||
|
return {
|
||||||
|
referenceId,
|
||||||
|
formattedValue: formatter.formatAny(displayValues[i]),
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
(input) => {
|
(values) => {
|
||||||
if (!input) {
|
if (!values) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return input.map(token => {
|
return values.map(({referenceId, formattedValue}) => {
|
||||||
const isBlankReference = token.trim() === '';
|
const isBlankReference = formattedValue.trim() === '';
|
||||||
return choiceToken(
|
return choiceToken(
|
||||||
isBlankReference ? '[Blank]' : token,
|
[
|
||||||
|
cssRefIcon('FieldReference',
|
||||||
|
cssRefIcon.cls('-view-as-card', use =>
|
||||||
|
referenceId !== 0 && use(this._hasRecordCard)),
|
||||||
|
dom.on('click', async () => {
|
||||||
|
if (referenceId === 0 || !this._hasRecordCard.get()) { return; }
|
||||||
|
|
||||||
|
const rowId = referenceId as number;
|
||||||
|
const sectionId = this._refTable.get()?.recordCardViewSectionRef();
|
||||||
|
if (sectionId === undefined) {
|
||||||
|
throw new Error('Unable to open Record Card: undefined section id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorUrlState = {hash: {rowId, sectionId, recordCard: true}};
|
||||||
|
await urlState().pushUrl(anchorUrlState, {replace: true});
|
||||||
|
}),
|
||||||
|
dom.on('mousedown', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
cssLabel(isBlankReference ? '[Blank]' : formattedValue,
|
||||||
|
testId('ref-list-cell-token-label'),
|
||||||
|
),
|
||||||
|
dom.cls(cssRefIconAndLabel.className),
|
||||||
|
],
|
||||||
{
|
{
|
||||||
blank: isBlankReference,
|
blank: isBlankReference,
|
||||||
},
|
},
|
||||||
@ -61,3 +103,26 @@ export class ReferenceList extends Reference {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cssRefIcon = styled(icon, `
|
||||||
|
--icon-color: ${theme.lightText};
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&-view-as-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&-view-as-card:hover {
|
||||||
|
--icon-color: ${theme.controlFg};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssRefIconAndLabel = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssLabel = styled('div', `
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`);
|
||||||
|
@ -5,7 +5,6 @@ import {Session} from 'test/nbrowser/gristUtils';
|
|||||||
|
|
||||||
describe('ReferenceList', function() {
|
describe('ReferenceList', function() {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
setupTestSuite();
|
|
||||||
let session: Session;
|
let session: Session;
|
||||||
const cleanup = setupTestSuite({team: true});
|
const cleanup = setupTestSuite({team: true});
|
||||||
|
|
||||||
@ -79,6 +78,7 @@ describe('ReferenceList', function() {
|
|||||||
await gu.sendKeys(Key.ARROW_DOWN, Key.ENTER, 'The Avengers', Key.ENTER, Key.ENTER);
|
await gu.sendKeys(Key.ARROW_DOWN, Key.ENTER, 'The Avengers', Key.ENTER, Key.ENTER);
|
||||||
|
|
||||||
// Check that the cells are rendered correctly.
|
// Check that the cells are rendered correctly.
|
||||||
|
await gu.resizeColumn({col: 'Favorite Film'}, 100);
|
||||||
assert.deepEqual(await gu.getVisibleGridCells('Favorite Film', [1, 2, 3, 4, 5, 6]),
|
assert.deepEqual(await gu.getVisibleGridCells('Favorite Film', [1, 2, 3, 4, 5, 6]),
|
||||||
[
|
[
|
||||||
'Forrest Gump\nAlien',
|
'Forrest Gump\nAlien',
|
||||||
@ -273,6 +273,7 @@ describe('ReferenceList', function() {
|
|||||||
await driver.find('.test-fbuilder-ref-col-select').click();
|
await driver.find('.test-fbuilder-ref-col-select').click();
|
||||||
await driver.findContent('.test-select-row', /Name/).click();
|
await driver.findContent('.test-select-row', /Name/).click();
|
||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
|
await gu.resizeColumn({col: 'A'}, 100);
|
||||||
assert.deepEqual(await gu.getVisibleGridCells(3, [1, 2, 3, 4, 5, 6]),
|
assert.deepEqual(await gu.getVisibleGridCells(3, [1, 2, 3, 4, 5, 6]),
|
||||||
['Roger\nTom', 'Tom', 'Sydney\nBill\nEvan', '', '', '']);
|
['Roger\nTom', 'Tom', 'Sydney\nBill\nEvan', '', '', '']);
|
||||||
|
|
||||||
@ -311,7 +312,7 @@ describe('ReferenceList', function() {
|
|||||||
const cell = gu.getCell({col: 'A', rowNum: 1});
|
const cell = gu.getCell({col: 'A', rowNum: 1});
|
||||||
await server.pauseUntil(async () => {
|
await server.pauseUntil(async () => {
|
||||||
assert.equal(await cell.getText(), 'Friends[1]\nFriends[2]');
|
assert.equal(await cell.getText(), 'Friends[1]\nFriends[2]');
|
||||||
await cell.click();
|
await gu.clickReferenceListCell(cell);
|
||||||
await gu.sendKeys('5');
|
await gu.sendKeys('5');
|
||||||
// Check that the autocomplete has no items yet.
|
// Check that the autocomplete has no items yet.
|
||||||
assert.isEmpty(await driver.findAll('.test-autocomplete .test-ref-editor-new-item'));
|
assert.isEmpty(await driver.findAll('.test-autocomplete .test-ref-editor-new-item'));
|
||||||
@ -324,7 +325,7 @@ describe('ReferenceList', function() {
|
|||||||
assert.equal(await cell.getText(), 'Friends[1]\nFriends[2]');
|
assert.equal(await cell.getText(), 'Friends[1]\nFriends[2]');
|
||||||
|
|
||||||
// Once server is responsive, a valid value should not offer a "new item".
|
// Once server is responsive, a valid value should not offer a "new item".
|
||||||
await cell.click();
|
await gu.clickReferenceListCell(cell);
|
||||||
await gu.sendKeys('5');
|
await gu.sendKeys('5');
|
||||||
await driver.findWait('.test-ref-editor-item', 500);
|
await driver.findWait('.test-ref-editor-item', 500);
|
||||||
assert.isFalse(await driver.find('.test-ref-editor-new-item').isPresent());
|
assert.isFalse(await driver.find('.test-ref-editor-new-item').isPresent());
|
||||||
@ -750,7 +751,8 @@ describe('ReferenceList', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update choices as user types into textbox', async function() {
|
it('should update choices as user types into textbox', async function() {
|
||||||
let cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1}).doClick();
|
let cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1});
|
||||||
|
await gu.clickReferenceListCell(cell);
|
||||||
assert.equal(await cell.getText(), 'TECHNOLOGY, ARTS AND SCIENCES STUDIO');
|
assert.equal(await cell.getText(), 'TECHNOLOGY, ARTS AND SCIENCES STUDIO');
|
||||||
await driver.sendKeys('TECHNOLOGY, ARTS AND SCIENCES STUDIO');
|
await driver.sendKeys('TECHNOLOGY, ARTS AND SCIENCES STUDIO');
|
||||||
assert.deepEqual(await getACOptions(3), [
|
assert.deepEqual(await getACOptions(3), [
|
||||||
@ -759,7 +761,8 @@ describe('ReferenceList', function() {
|
|||||||
'SCHOOL OF SCIENCE AND TECHNOLOGY',
|
'SCHOOL OF SCIENCE AND TECHNOLOGY',
|
||||||
]);
|
]);
|
||||||
await driver.sendKeys(Key.ESCAPE);
|
await driver.sendKeys(Key.ESCAPE);
|
||||||
cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 2}).doClick();
|
cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 2});
|
||||||
|
await gu.clickReferenceListCell(cell);
|
||||||
await driver.sendKeys('stuy');
|
await driver.sendKeys('stuy');
|
||||||
assert.deepEqual(await getACOptions(3), [
|
assert.deepEqual(await getACOptions(3), [
|
||||||
'STUYVESANT HIGH SCHOOL',
|
'STUYVESANT HIGH SCHOOL',
|
||||||
@ -790,7 +793,8 @@ describe('ReferenceList', function() {
|
|||||||
it('should highlight matching parts of items', async function() {
|
it('should highlight matching parts of items', async function() {
|
||||||
await driver.sendKeys(Key.HOME);
|
await driver.sendKeys(Key.HOME);
|
||||||
|
|
||||||
let cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 2}).doClick();
|
let cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 2});
|
||||||
|
await gu.clickReferenceListCell(cell);
|
||||||
assert.equal(await cell.getText(), 'Red');
|
assert.equal(await cell.getText(), 'Red');
|
||||||
await driver.sendKeys(Key.ENTER, 'Red');
|
await driver.sendKeys(Key.ENTER, 'Red');
|
||||||
await driver.findWait('.test-ref-editor-item', 1000);
|
await driver.findWait('.test-ref-editor-item', 1000);
|
||||||
@ -802,7 +806,8 @@ describe('ReferenceList', function() {
|
|||||||
['Re']);
|
['Re']);
|
||||||
await driver.sendKeys(Key.ESCAPE);
|
await driver.sendKeys(Key.ESCAPE);
|
||||||
|
|
||||||
cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1}).doClick();
|
cell = await gu.getCell({section: 'References', col: 'Schools', rowNum: 1});
|
||||||
|
await gu.clickReferenceListCell(cell);
|
||||||
await driver.sendKeys('br tech');
|
await driver.sendKeys('br tech');
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await driver.findContentWait('.test-ref-editor-item', /BROOKLYN TECH/, 1000).findAll('span', e => e.getText()),
|
await driver.findContentWait('.test-ref-editor-item', /BROOKLYN TECH/, 1000).findAll('span', e => e.getText()),
|
||||||
@ -819,19 +824,20 @@ describe('ReferenceList', function() {
|
|||||||
it('should reflect changes to the target column', async function() {
|
it('should reflect changes to the target column', async function() {
|
||||||
await driver.sendKeys(Key.HOME);
|
await driver.sendKeys(Key.HOME);
|
||||||
|
|
||||||
const cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 4}).doClick();
|
const cell = await gu.getCell({section: 'References', col: 'Colors', rowNum: 4});
|
||||||
|
await gu.clickReferenceListCell(cell);
|
||||||
assert.equal(await cell.getText(), '');
|
assert.equal(await cell.getText(), '');
|
||||||
await driver.sendKeys(Key.ENTER);
|
await driver.sendKeys(Key.ENTER);
|
||||||
assert.deepEqual(await getACOptions(2), ['Alice Blue', 'Añil']);
|
assert.deepEqual(await getACOptions(2), ['Alice Blue', 'Añil']);
|
||||||
await driver.sendKeys(Key.ESCAPE);
|
await driver.sendKeys(Key.ESCAPE);
|
||||||
|
|
||||||
// Change a color
|
// Change a color
|
||||||
await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1}).doClick();
|
await gu.clickReferenceListCell(await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1}));
|
||||||
await driver.sendKeys('HAZELNUT', Key.ENTER);
|
await driver.sendKeys('HAZELNUT', Key.ENTER, Key.ENTER);
|
||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
|
|
||||||
// See that the old value is gone from the autocomplete, and the new one is present.
|
// See that the old value is gone from the autocomplete, and the new one is present.
|
||||||
await cell.click();
|
await gu.clickReferenceListCell(cell);
|
||||||
await driver.sendKeys(Key.ENTER);
|
await driver.sendKeys(Key.ENTER);
|
||||||
assert.deepEqual(await getACOptions(2), ['Añil', 'Aqua']);
|
assert.deepEqual(await getACOptions(2), ['Añil', 'Aqua']);
|
||||||
await driver.sendKeys('H');
|
await driver.sendKeys('H');
|
||||||
@ -839,11 +845,11 @@ describe('ReferenceList', function() {
|
|||||||
await driver.sendKeys(Key.ESCAPE);
|
await driver.sendKeys(Key.ESCAPE);
|
||||||
|
|
||||||
// Delete a row.
|
// Delete a row.
|
||||||
await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1}).doClick();
|
await gu.clickReferenceListCell(await gu.getCell({section: 'Colors', col: 'Color Name', rowNum: 1}));
|
||||||
await gu.removeRow(1);
|
await gu.removeRow(1);
|
||||||
|
|
||||||
// See that the value is gone from the autocomplete.
|
// See that the value is gone from the autocomplete.
|
||||||
await cell.click();
|
await gu.clickReferenceListCell(cell);
|
||||||
await driver.sendKeys('H');
|
await driver.sendKeys('H');
|
||||||
assert.deepEqual(await getACOptions(2), ['Honeydew', 'Hot Pink']);
|
assert.deepEqual(await getACOptions(2), ['Honeydew', 'Hot Pink']);
|
||||||
await driver.sendKeys(Key.ESCAPE);
|
await driver.sendKeys(Key.ESCAPE);
|
||||||
@ -856,7 +862,7 @@ describe('ReferenceList', function() {
|
|||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
|
|
||||||
// See that the new value is visible in the autocomplete.
|
// See that the new value is visible in the autocomplete.
|
||||||
await cell.click();
|
await gu.clickReferenceListCell(cell);
|
||||||
await driver.sendKeys('H');
|
await driver.sendKeys('H');
|
||||||
assert.deepEqual(await getACOptions(2), ['HELIOTROPE', 'Honeydew']);
|
assert.deepEqual(await getACOptions(2), ['HELIOTROPE', 'Honeydew']);
|
||||||
await driver.sendKeys(Key.BACK_SPACE);
|
await driver.sendKeys(Key.BACK_SPACE);
|
||||||
@ -866,7 +872,7 @@ describe('ReferenceList', function() {
|
|||||||
// Undo all the changes.
|
// Undo all the changes.
|
||||||
await gu.undo(4);
|
await gu.undo(4);
|
||||||
|
|
||||||
await cell.click();
|
await gu.clickReferenceListCell(cell);
|
||||||
await driver.sendKeys('H');
|
await driver.sendKeys('H');
|
||||||
assert.deepEqual(await getACOptions(2), ['Honeydew', 'Hot Pink']);
|
assert.deepEqual(await getACOptions(2), ['Honeydew', 'Hot Pink']);
|
||||||
await driver.sendKeys(Key.BACK_SPACE);
|
await driver.sendKeys(Key.BACK_SPACE);
|
||||||
|
@ -183,7 +183,12 @@ async function checkSelectingRecords(selectBy: string, sourceData: string[][], n
|
|||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
|
|
||||||
const selectByTable = selectBy.split(' ')[0];
|
const selectByTable = selectBy.split(' ')[0];
|
||||||
await gu.getCell({section: selectByTable, col: 0, rowNum: 3}).click();
|
const cell = await gu.getCell({section: selectByTable, col: 0, rowNum: 3});
|
||||||
|
if (selectByTable === 'REFLISTS') {
|
||||||
|
await gu.clickReferenceListCell(cell);
|
||||||
|
} else {
|
||||||
|
await cell.click();
|
||||||
|
}
|
||||||
|
|
||||||
let numSourceRows = 0;
|
let numSourceRows = 0;
|
||||||
|
|
||||||
@ -207,7 +212,12 @@ async function checkSelectingRecords(selectBy: string, sourceData: string[][], n
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < sourceData.length; i++) {
|
for (let i = 0; i < sourceData.length; i++) {
|
||||||
await gu.getCell({section: selectByTable, col: 0, rowNum: i + 1}).click();
|
const cell = await gu.getCell({section: selectByTable, col: 0, rowNum: i + 1});
|
||||||
|
if (selectByTable === 'REFLISTS') {
|
||||||
|
await gu.clickReferenceListCell(cell);
|
||||||
|
} else {
|
||||||
|
await cell.click();
|
||||||
|
}
|
||||||
await checkSourceGroup(i);
|
await checkSourceGroup(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,6 +572,19 @@ export async function rightClick(cell: WebElement) {
|
|||||||
await driver.withActions((actions) => actions.contextClick(cell));
|
await driver.withActions((actions) => actions.contextClick(cell));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks a Reference List cell, taking care not to click the icon (which can
|
||||||
|
* cause an unexpected Record Card popup to appear).
|
||||||
|
*/
|
||||||
|
export async function clickReferenceListCell(cell: WebElement) {
|
||||||
|
const tokens = await cell.findAll('.test-ref-list-cell-token-label');
|
||||||
|
if (tokens.length > 0) {
|
||||||
|
await tokens[0].click();
|
||||||
|
} else {
|
||||||
|
await cell.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the selector position in the Grid view section (or null if not present).
|
* Gets the selector position in the Grid view section (or null if not present).
|
||||||
* Selector is the black box around the row number.
|
* Selector is the black box around the row number.
|
||||||
|
Loading…
Reference in New Issue
Block a user