From fb8de2dbe75713c63d10357806198ea4e2bf661f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= <jaroslaw.sadzinski@gmail.com> Date: Wed, 2 Oct 2024 10:00:43 +0200 Subject: [PATCH 1/7] Sorting mappable columns --- app/client/ui/CustomSectionConfig.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 9163bb11..52e0f78c 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -75,11 +75,13 @@ class ColumnPicker extends Disposable { void use(refreshTrigger); const columnsAsOptions: IOption<number|null>[] = use(canBeMapped) - .map((col) => ({ - value: col.getRowId(), - label: col.label.peek(), - icon: 'FieldColumn', - })); + .map((col) => ({ + value: col.getRowId(), + label: col.label.peek() || '', + icon: 'FieldColumn', + })); + // Order it by label. + columnsAsOptions.sort((a, b) => a.label.localeCompare(b.label)); // For optional mappings, add 'Blank' option but only if the value is set. // This option will allow to clear the selection. From 179272e3f26601e4021fd516fa6473107d9b21cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= <jaroslaw.sadzinski@gmail.com> Date: Wed, 2 Oct 2024 13:16:10 +0200 Subject: [PATCH 2/7] Sorting column names --- app/client/models/entities/ColumnRec.ts | 17 +++++++++++++++++ app/client/ui/CustomSectionConfig.ts | 5 +++-- app/client/ui/PageWidgetPicker.ts | 3 ++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/client/models/entities/ColumnRec.ts b/app/client/models/entities/ColumnRec.ts index 3791baa6..8a4b8aa7 100644 --- a/app/client/models/entities/ColumnRec.ts +++ b/app/client/models/entities/ColumnRec.ts @@ -230,6 +230,23 @@ export function formatterForRec( return func(args); } +export function labelsOrder(a: ColumnRec, b: ColumnRec): number { + const left = a.label.peek().toLowerCase(); + const right = b.label.peek().toLowerCase(); + + // Order is as follows: + // - First columns with normal labels starting with a letter. + // - Second all columns starting with '_' (treated as private) + // - Third all columns starting with '#' (treated as private) + // - Rest. + if (left[0] === '_' && right[0] !== '_') { return 1; } + if (left[0] !== '_' && right[0] === '_') { return -1; } + if (left[0] === '#' && right[0] !== '#') { return 1; } + if (left[0] !== '#' && right[0] === '#') { return -1; } + return left.localeCompare(right); +} + + /** * A chat message. Either send by the user or by the AI. */ diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 52e0f78c..4ce4bf77 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -7,6 +7,7 @@ import {makeT} from 'app/client/lib/localization'; import {localStorageBoolObs} from 'app/client/lib/localStorageObs'; import {ColumnToMapImpl} from 'app/client/models/ColumnToMap'; import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel'; +import {labelsOrder} from 'app/client/models/entities/ColumnRec'; import { cssDeveloperLink, cssWidgetMetadata, @@ -75,13 +76,12 @@ class ColumnPicker extends Disposable { void use(refreshTrigger); const columnsAsOptions: IOption<number|null>[] = use(canBeMapped) + .sort(labelsOrder) .map((col) => ({ value: col.getRowId(), label: col.label.peek() || '', icon: 'FieldColumn', })); - // Order it by label. - columnsAsOptions.sort((a, b) => a.label.localeCompare(b.label)); // For optional mappings, add 'Blank' option but only if the value is set. // This option will allow to clear the selection. @@ -205,6 +205,7 @@ class ColumnListPicker extends Disposable { const wrongTypeCount = notMapped.get().length - typedColumns.get().length; return [ ...typedColumns.get() + .sort(labelsOrder) .map((col) => menuItem( () => this._addColumn(col), col.label.peek(), diff --git a/app/client/ui/PageWidgetPicker.ts b/app/client/ui/PageWidgetPicker.ts index dbd7cf84..8c64b736 100644 --- a/app/client/ui/PageWidgetPicker.ts +++ b/app/client/ui/PageWidgetPicker.ts @@ -32,6 +32,7 @@ import { import Popper from 'popper.js'; import {IOpenController, popupOpen, setPopupToCreateDom} from 'popweasel'; import without = require('lodash/without'); +import {labelsOrder} from 'app/client/models/entities/ColumnRec'; const t = makeT('PageWidgetPicker'); @@ -407,7 +408,7 @@ export class PageWidgetSelect extends Disposable { (use) => use(this._columns) .filter((col) => !col.isHiddenCol() && col.parentId() === use(this._value.table)), (cols) => cols ? - dom.forEach(cols, (col) => + dom.forEach([...cols].sort(labelsOrder), (col) => cssEntry(cssIcon('FieldColumn'), cssFieldLabel(dom.text(col.label)), dom.on('click', () => this._toggleColumnId(col.id())), cssEntry.cls('-selected', (use) => use(this._value.columns).includes(col.id())), From 9806e1a4c0ddfdb94fd3cc6e3aecf1388cb1d941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= <jaroslaw.sadzinski@gmail.com> Date: Mon, 7 Oct 2024 02:42:38 +0200 Subject: [PATCH 3/7] inputs --- app/client/models/entities/ViewSectionRec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 181fcd86..2dc36c77 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -16,7 +16,7 @@ import { ViewFieldRec, ViewRec } from 'app/client/models/DocModel'; -import {BEHAVIOR} from 'app/client/models/entities/ColumnRec'; +import {BEHAVIOR, labelsOrder} from 'app/client/models/entities/ColumnRec'; import * as modelUtil from 'app/client/models/modelUtil'; import {removeRule, RuleOwner} from 'app/client/models/RuleOwner'; import {LinkConfig} from 'app/client/ui/selectBy'; @@ -698,7 +698,9 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): // Evaluates to an array of column models, which are not referenced by anything in viewFields. this.hiddenColumns = this.autoDispose(ko.pureComputed(() => { const included = new Set(this.viewFields().all().map((f) => f.column().origColRef())); - return this.columns().filter(c => !included.has(c.getRowId())); + return this.columns() + .filter(c => !included.has(c.getRowId())) + .sort(labelsOrder); })); this.hasFocus = ko.pureComputed({ From 4cde2202b545f4ecb665beb136e2c1df7f5af1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= <jaroslaw.sadzinski@gmail.com> Date: Wed, 9 Oct 2024 11:12:21 +0200 Subject: [PATCH 4/7] Allowing new row on recrod card --- app/client/components/GristDoc.ts | 2 +- app/client/components/RecordCardPopup.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index 9eff9dbc..a3416052 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -701,7 +701,7 @@ export class GristDoc extends DisposableWithEvents { const {recordCard, rowId} = popupOptions.hash; if (recordCard) { - if (!rowId || rowId === 'new') { + if (!rowId) { // Should be unreachable, but just to be sure (and to satisfy type checking)... throw new Error('Unable to open Record Card: undefined row id'); } diff --git a/app/client/components/RecordCardPopup.ts b/app/client/components/RecordCardPopup.ts index 7fc1efac..de30ed10 100644 --- a/app/client/components/RecordCardPopup.ts +++ b/app/client/components/RecordCardPopup.ts @@ -7,13 +7,14 @@ import {ViewSectionRec} from 'app/client/models/DocModel'; import {ChangeType, RowList} from 'app/client/models/rowset'; import {theme} from 'app/client/ui2018/cssVars'; import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; +import {UIRowId} from 'app/plugin/GristAPI'; import {dom, makeTestId, styled} from 'grainjs'; const testId = makeTestId('test-record-card-popup-'); interface RecordCardPopupOptions { gristDoc: GristDoc; - rowId: number; + rowId: UIRowId; viewSection: ViewSectionRec; onClose(): void; } From 1315cc366fa634f750292aeaa4c5ffe470017705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= <jaroslaw.sadzinski@gmail.com> Date: Wed, 9 Oct 2024 11:13:31 +0200 Subject: [PATCH 5/7] Adding sort order for filters --- app/client/models/entities/ColumnRec.ts | 11 ++++++++--- app/client/models/entities/ViewSectionRec.ts | 2 +- app/client/ui/CustomSectionConfig.ts | 7 ++++--- app/client/ui/SortConfig.ts | 3 ++- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/client/models/entities/ColumnRec.ts b/app/client/models/entities/ColumnRec.ts index 8a4b8aa7..2ecdd471 100644 --- a/app/client/models/entities/ColumnRec.ts +++ b/app/client/models/entities/ColumnRec.ts @@ -230,9 +230,14 @@ export function formatterForRec( return func(args); } -export function labelsOrder(a: ColumnRec, b: ColumnRec): number { - const left = a.label.peek().toLowerCase(); - const right = b.label.peek().toLowerCase(); +type ColumnInfo = {label: string}|{label: ko.Observable<string>}; +function peekLabel(info: ColumnInfo): string { + return typeof info.label === 'string' ? info.label : info.label.peek(); +} + +export function labelsOrder(a: ColumnInfo, b: ColumnInfo): number { + const left = peekLabel(a).toLowerCase(); + const right = peekLabel(b).toLowerCase(); // Order is as follows: // - First columns with normal labels starting with a letter. diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 2dc36c77..c99849f0 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -521,7 +521,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): const savedFiltersByColRef = new Map(this._savedFilters().all().map(f => [f.colRef(), f])); const viewFieldsByColRef = new Map(this.viewFields().all().map(f => [f.origCol().getRowId(), f])); - return this.columns().map(column => { + return [...this.columns()].sort(labelsOrder).map(column => { const savedFilter = savedFiltersByColRef.get(column.origColRef()); // Initialize with a saved filter, if one exists. Otherwise, use a blank filter. const filter = modelUtil.customComputed({ diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 4ce4bf77..82245f3e 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -76,12 +76,13 @@ class ColumnPicker extends Disposable { void use(refreshTrigger); const columnsAsOptions: IOption<number|null>[] = use(canBeMapped) - .sort(labelsOrder) .map((col) => ({ value: col.getRowId(), label: col.label.peek() || '', - icon: 'FieldColumn', - })); + icon: 'FieldColumn' as const, + })) + .sort(labelsOrder); + // For optional mappings, add 'Blank' option but only if the value is set. // This option will allow to clear the selection. diff --git a/app/client/ui/SortConfig.ts b/app/client/ui/SortConfig.ts index 44c4f43c..ec675188 100644 --- a/app/client/ui/SortConfig.ts +++ b/app/client/ui/SortConfig.ts @@ -4,6 +4,7 @@ import * as kf from 'app/client/lib/koForm'; import {makeT} from 'app/client/lib/localization'; import {addToSort, updatePositions} from 'app/client/lib/sortUtil'; import {ViewSectionRec} from 'app/client/models/DocModel'; +import {labelsOrder} from 'app/client/models/entities/ColumnRec'; import {ObjObservable} from 'app/client/models/modelUtil'; import {dropdownWithSearch} from 'app/client/ui/searchDropdown'; import {cssIcon, cssRow, cssSortFilterColumn} from 'app/client/ui/RightPanelStyles'; @@ -215,7 +216,7 @@ export class SortConfig extends Disposable { const currentSection = this._section; const currentSortSpec = use(currentSection.activeSortSpec); const specRowIds = new Set(currentSortSpec.map(_sortRef => Sort.getColRef(_sortRef))); - return use(columns).filter(_col => !specRowIds.has(_col.value)); + return use(columns).filter(_col => !specRowIds.has(_col.value)).sort(labelsOrder); }); const {menuOptions} = this._options; return cssButtonRow( From 3718883bc95f3fffc00369473a4da4170e342bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= <jaroslaw.sadzinski@gmail.com> Date: Tue, 15 Oct 2024 13:00:13 +0200 Subject: [PATCH 6/7] Refactoring --- app/client/models/entities/ColumnRec.ts | 18 +++++++----------- app/client/models/entities/ViewSectionRec.ts | 6 +++--- app/client/ui/CustomSectionConfig.ts | 8 ++++---- app/client/ui/PageWidgetPicker.ts | 4 ++-- app/client/ui/SortConfig.ts | 4 ++-- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/app/client/models/entities/ColumnRec.ts b/app/client/models/entities/ColumnRec.ts index 2ecdd471..8f0b4e09 100644 --- a/app/client/models/entities/ColumnRec.ts +++ b/app/client/models/entities/ColumnRec.ts @@ -235,17 +235,13 @@ function peekLabel(info: ColumnInfo): string { return typeof info.label === 'string' ? info.label : info.label.peek(); } -export function labelsOrder(a: ColumnInfo, b: ColumnInfo): number { - const left = peekLabel(a).toLowerCase(); - const right = peekLabel(b).toLowerCase(); - - // Order is as follows: - // - First columns with normal labels starting with a letter. - // - Second all columns starting with '_' (treated as private) - // - Third all columns starting with '#' (treated as private) - // - Rest. - if (left[0] === '_' && right[0] !== '_') { return 1; } - if (left[0] !== '_' && right[0] === '_') { return -1; } +/** + * Helper function to sort columns based on the label. Puts # columns at the end as this is + * treated as private columns. + */ +export function columnsOrder(a: ColumnInfo, b: ColumnInfo): number { + const left = peekLabel(a)?.toLowerCase() || ''; + const right = peekLabel(b)?.toLowerCase() || ''; if (left[0] === '#' && right[0] !== '#') { return 1; } if (left[0] !== '#' && right[0] === '#') { return -1; } return left.localeCompare(right); diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index c99849f0..37e12176 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -16,7 +16,7 @@ import { ViewFieldRec, ViewRec } from 'app/client/models/DocModel'; -import {BEHAVIOR, labelsOrder} from 'app/client/models/entities/ColumnRec'; +import {BEHAVIOR, columnsOrder} from 'app/client/models/entities/ColumnRec'; import * as modelUtil from 'app/client/models/modelUtil'; import {removeRule, RuleOwner} from 'app/client/models/RuleOwner'; import {LinkConfig} from 'app/client/ui/selectBy'; @@ -521,7 +521,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): const savedFiltersByColRef = new Map(this._savedFilters().all().map(f => [f.colRef(), f])); const viewFieldsByColRef = new Map(this.viewFields().all().map(f => [f.origCol().getRowId(), f])); - return [...this.columns()].sort(labelsOrder).map(column => { + return [...this.columns()].sort(columnsOrder).map(column => { const savedFilter = savedFiltersByColRef.get(column.origColRef()); // Initialize with a saved filter, if one exists. Otherwise, use a blank filter. const filter = modelUtil.customComputed({ @@ -700,7 +700,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): const included = new Set(this.viewFields().all().map((f) => f.column().origColRef())); return this.columns() .filter(c => !included.has(c.getRowId())) - .sort(labelsOrder); + .sort(columnsOrder); })); this.hasFocus = ko.pureComputed({ diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 82245f3e..7c60548a 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -7,7 +7,7 @@ import {makeT} from 'app/client/lib/localization'; import {localStorageBoolObs} from 'app/client/lib/localStorageObs'; import {ColumnToMapImpl} from 'app/client/models/ColumnToMap'; import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel'; -import {labelsOrder} from 'app/client/models/entities/ColumnRec'; +import {columnsOrder} from 'app/client/models/entities/ColumnRec'; import { cssDeveloperLink, cssWidgetMetadata, @@ -81,7 +81,7 @@ class ColumnPicker extends Disposable { label: col.label.peek() || '', icon: 'FieldColumn' as const, })) - .sort(labelsOrder); + .sort(columnsOrder); // For optional mappings, add 'Blank' option but only if the value is set. @@ -205,8 +205,8 @@ class ColumnListPicker extends Disposable { menu(() => { const wrongTypeCount = notMapped.get().length - typedColumns.get().length; return [ - ...typedColumns.get() - .sort(labelsOrder) + ...typedColumns.get() // returns a temp table. + .sort(columnsOrder) .map((col) => menuItem( () => this._addColumn(col), col.label.peek(), diff --git a/app/client/ui/PageWidgetPicker.ts b/app/client/ui/PageWidgetPicker.ts index 8c64b736..bf1b5ede 100644 --- a/app/client/ui/PageWidgetPicker.ts +++ b/app/client/ui/PageWidgetPicker.ts @@ -4,6 +4,7 @@ import {FocusLayer} from 'app/client/lib/FocusLayer'; import {makeT} from 'app/client/lib/localization'; import {reportError} from 'app/client/models/AppModel'; import {ColumnRec, TableRec, ViewSectionRec} from 'app/client/models/DocModel'; +import {columnsOrder} from 'app/client/models/entities/ColumnRec'; import {PERMITTED_CUSTOM_WIDGETS} from "app/client/models/features"; import {linkId, NoLink} from 'app/client/ui/selectBy'; import {overflowTooltip, withInfoTooltip} from 'app/client/ui/tooltips'; @@ -32,7 +33,6 @@ import { import Popper from 'popper.js'; import {IOpenController, popupOpen, setPopupToCreateDom} from 'popweasel'; import without = require('lodash/without'); -import {labelsOrder} from 'app/client/models/entities/ColumnRec'; const t = makeT('PageWidgetPicker'); @@ -408,7 +408,7 @@ export class PageWidgetSelect extends Disposable { (use) => use(this._columns) .filter((col) => !col.isHiddenCol() && col.parentId() === use(this._value.table)), (cols) => cols ? - dom.forEach([...cols].sort(labelsOrder), (col) => + dom.forEach([...cols].sort(columnsOrder), (col) => cssEntry(cssIcon('FieldColumn'), cssFieldLabel(dom.text(col.label)), dom.on('click', () => this._toggleColumnId(col.id())), cssEntry.cls('-selected', (use) => use(this._value.columns).includes(col.id())), diff --git a/app/client/ui/SortConfig.ts b/app/client/ui/SortConfig.ts index ec675188..94d1ffc4 100644 --- a/app/client/ui/SortConfig.ts +++ b/app/client/ui/SortConfig.ts @@ -4,7 +4,7 @@ import * as kf from 'app/client/lib/koForm'; import {makeT} from 'app/client/lib/localization'; import {addToSort, updatePositions} from 'app/client/lib/sortUtil'; import {ViewSectionRec} from 'app/client/models/DocModel'; -import {labelsOrder} from 'app/client/models/entities/ColumnRec'; +import {columnsOrder} from 'app/client/models/entities/ColumnRec'; import {ObjObservable} from 'app/client/models/modelUtil'; import {dropdownWithSearch} from 'app/client/ui/searchDropdown'; import {cssIcon, cssRow, cssSortFilterColumn} from 'app/client/ui/RightPanelStyles'; @@ -216,7 +216,7 @@ export class SortConfig extends Disposable { const currentSection = this._section; const currentSortSpec = use(currentSection.activeSortSpec); const specRowIds = new Set(currentSortSpec.map(_sortRef => Sort.getColRef(_sortRef))); - return use(columns).filter(_col => !specRowIds.has(_col.value)).sort(labelsOrder); + return use(columns).filter(_col => !specRowIds.has(_col.value)).sort(columnsOrder); }); const {menuOptions} = this._options; return cssButtonRow( From 9b27e3fd2bde855e3f601087e81c0d21c6173e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= <jaroslaw.sadzinski@gmail.com> Date: Tue, 15 Oct 2024 13:20:08 +0200 Subject: [PATCH 7/7] Updating other places --- app/client/ui/GridViewMenus.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/client/ui/GridViewMenus.ts b/app/client/ui/GridViewMenus.ts index f816f723..6f7d0d8a 100644 --- a/app/client/ui/GridViewMenus.ts +++ b/app/client/ui/GridViewMenus.ts @@ -2,7 +2,7 @@ import {allCommands} from 'app/client/components/commands'; import {GristDoc} from 'app/client/components/GristDoc'; import GridView from 'app/client/components/GridView'; import {makeT} from 'app/client/lib/localization'; -import {ColumnRec} from "app/client/models/entities/ColumnRec"; +import {ColumnRec, columnsOrder} from "app/client/models/entities/ColumnRec"; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {withInfoTooltip} from 'app/client/ui/tooltips'; import {isNarrowScreen, testId, theme, vars} from 'app/client/ui2018/cssVars'; @@ -293,7 +293,7 @@ function buildDetectDuplicatesMenuItems(gridView: GridView, index?: number) { const {viewSection} = gridView; return menuItemSubmenu( () => searchableMenu( - viewSection.columns().map((col) => { + [...viewSection.columns()].sort(columnsOrder).map((col) => { function buildFormula() { if (isListType(col.type())) { return `any([len(${col.table().tableId()}.lookupRecords(${col.colId()}` + @@ -568,7 +568,9 @@ function buildLookupSection(gridView: GridView, index?: number){ return references.map((ref) => menuItemSubmenu( () => searchableMenu( - ref.refTable()?.visibleColumns().map(buildRefColMenu.bind(null, ref)) ?? [], + (ref.refTable()?.visibleColumns() ?? []) + .sort(columnsOrder) + .map(buildRefColMenu.bind(null, ref)), { searchInputPlaceholder: t('Search columns') }