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; } diff --git a/app/client/models/entities/ColumnRec.ts b/app/client/models/entities/ColumnRec.ts index 3791baa6..8f0b4e09 100644 --- a/app/client/models/entities/ColumnRec.ts +++ b/app/client/models/entities/ColumnRec.ts @@ -230,6 +230,24 @@ export function formatterForRec( return func(args); } +type ColumnInfo = {label: string}|{label: ko.Observable}; +function peekLabel(info: ColumnInfo): string { + return typeof info.label === 'string' ? info.label : info.label.peek(); +} + +/** + * 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); +} + + /** * A chat message. Either send by the user or by the AI. */ diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 181fcd86..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} 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().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({ @@ -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(columnsOrder); })); this.hasFocus = ko.pureComputed({ diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 9163bb11..7c60548a 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 {columnsOrder} from 'app/client/models/entities/ColumnRec'; import { cssDeveloperLink, cssWidgetMetadata, @@ -75,11 +76,13 @@ class ColumnPicker extends Disposable { void use(refreshTrigger); const columnsAsOptions: IOption[] = use(canBeMapped) - .map((col) => ({ - value: col.getRowId(), - label: col.label.peek(), - icon: 'FieldColumn', - })); + .map((col) => ({ + value: col.getRowId(), + label: col.label.peek() || '', + icon: 'FieldColumn' as const, + })) + .sort(columnsOrder); + // For optional mappings, add 'Blank' option but only if the value is set. // This option will allow to clear the selection. @@ -202,7 +205,8 @@ class ColumnListPicker extends Disposable { menu(() => { const wrongTypeCount = notMapped.get().length - typedColumns.get().length; return [ - ...typedColumns.get() + ...typedColumns.get() // returns a temp table. + .sort(columnsOrder) .map((col) => menuItem( () => this._addColumn(col), col.label.peek(), 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') } diff --git a/app/client/ui/PageWidgetPicker.ts b/app/client/ui/PageWidgetPicker.ts index dbd7cf84..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'; @@ -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(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 44c4f43c..94d1ffc4 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 {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'; @@ -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(columnsOrder); }); const {menuOptions} = this._options; return cssButtonRow(