diff --git a/mathesar_ui/src/App.d.ts b/mathesar_ui/src/App.d.ts
index bc466325b8..c2ecc90cbe 100644
--- a/mathesar_ui/src/App.d.ts
+++ b/mathesar_ui/src/App.d.ts
@@ -33,12 +33,6 @@ export interface SchemaResponse extends SchemaEntry, TreeItem {
tables: DBObjectEntry[];
}
-// TODO: Come up with a better name for representing both tables and views
-export enum TabularType {
- Table = 1,
- View = 2,
-}
-
export type DbType = string;
export interface FilterConfiguration {
diff --git a/mathesar_ui/src/App.svelte b/mathesar_ui/src/App.svelte
index 7214fd2697..cf23f44695 100644
--- a/mathesar_ui/src/App.svelte
+++ b/mathesar_ui/src/App.svelte
@@ -6,6 +6,15 @@
import Header from '@mathesar/header/Header.svelte';
import { toast } from '@mathesar/stores/toast';
import { confirmationController } from '@mathesar/stores/confirmation';
+ import { currentSchemaId } from '@mathesar/stores/schemas';
+ import { beginUpdatingUrlWhenSchemaChanges } from './utils/routing';
+
+ // This is a bit of a hack to deal with our routing still being a patchwork of
+ // declarative and imperative logic. Without this call, the URL will not
+ // reliably set the query params when the schema changes. It actually _will_
+ // set the query params _sometimes_, but we weren't able to figure out why the
+ // behavior is inconsistent.
+ beginUpdatingUrlWhenSchemaChanges(currentSchemaId);
diff --git a/mathesar_ui/src/api/tables/records.ts b/mathesar_ui/src/api/tables/records.ts
index 1d7bc94003..19451aae99 100644
--- a/mathesar_ui/src/api/tables/records.ts
+++ b/mathesar_ui/src/api/tables/records.ts
@@ -1,6 +1,51 @@
+export interface Grouping {
+ /** Each string is a column name */
+ columns: string[];
+ mode: GroupingMode;
+ /**
+ * When `mode` === 'distinct', `num_groups` will always be `null`.
+ *
+ * When `mode` === 'percentile', `num_groups` will give the number of groups,
+ * as specified in the request params.
+ */
+ num_groups: number | null;
+ ranged: boolean;
+ groups: Group[];
+}
+
+export type SortDirection = 'asc' | 'desc';
+export interface SortingEntry {
+ /** column name */
+ field: string;
+ direction: SortDirection;
+}
+export type FilterCombination = 'and' | 'or';
+export type FilterOperation = 'eq' | 'ne' | 'get_duplicates';
+export interface FilterCondition {
+ /** column name */
+ field: string;
+ op: FilterOperation;
+ value: unknown;
+}
+type MakeFilteringOption = U extends string
+ ? { [k in U]: FilterCondition[] }
+ : never;
+export type Filtering =
+ | FilterCondition[]
+ | MakeFilteringOption;
+
+export interface GetRequestParams {
+ limit?: number;
+ offset?: number;
+ order_by?: SortingEntry[];
+ grouping?: Pick;
+ filters?: Filtering;
+}
+
export type ResultValue = string | number | boolean | null;
export interface Result {
+ /** keys are column names */
[k: string]: ResultValue;
}
@@ -29,21 +74,6 @@ export interface Group {
result_indices: number[];
}
-export interface Grouping {
- /** Each string is a column name */
- columns: string[];
- mode: GroupingMode;
- /**
- * When `mode` === 'distinct', `num_groups` will always be `null`.
- *
- * When `mode` === 'percentile', `num_groups` will give the number of groups,
- * as specified in the request params.
- */
- num_groups: number | null;
- ranged: boolean;
- groups: Group[];
-}
-
export interface Response {
count: number;
grouping: Grouping | null;
diff --git a/mathesar_ui/src/component-library/common/utils/ImmutableMap.ts b/mathesar_ui/src/component-library/common/utils/ImmutableMap.ts
new file mode 100644
index 0000000000..bc264093d4
--- /dev/null
+++ b/mathesar_ui/src/component-library/common/utils/ImmutableMap.ts
@@ -0,0 +1,105 @@
+export default class ImmutableMap<
+ Key extends string | number | boolean | null,
+ Value,
+> {
+ private map: Map;
+
+ constructor(i: Iterable<[Key, Value]> = []) {
+ this.map = new Map(i);
+ }
+
+ /**
+ * This method exists to allow us to subclass this class and call the
+ * constructor of the subclass from within this base class.
+ *
+ * If there's a way we can use generics to avoid `any` here, we'd love to
+ * know.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private getNewInstance(...args: any[]): this {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ return new (this.constructor as any)(...args) as this;
+ }
+
+ /**
+ * The value supplied here will overwrite any value that is already associated
+ * with `key`.
+ */
+ with(key: Key, value: Value): this {
+ const map = new Map(this.map);
+ map.set(key, value);
+ return this.getNewInstance(map);
+ }
+
+ /**
+ * When the same keys exist in within the entries of this instance and the
+ * entries supplied, the values from the entries supplied will be used instead
+ * of the values in this instance. This behavior is consistent with the `with`
+ * method.
+ */
+ withEntries(entries: Iterable<[Key, Value]>): this {
+ const map = new Map(this.map);
+ [...entries].forEach(([key, value]) => {
+ map.set(key, value);
+ });
+ return this.getNewInstance(map);
+ }
+
+ /**
+ * If `key` already exists, its corresponding value will remain. If `key` does
+ * not exist, then the value supplied here will be used.
+ */
+ coalesce(key: Key, value: Value): this {
+ return this.has(key) ? this : this.with(key, value);
+ }
+
+ /**
+ * When the same keys exist in within the entries of this instance and the
+ * entries supplied, the values from this instance will be used instead of the
+ * values from the supplied entries. This behavior is consistent with the
+ * `coalesce` method.
+ */
+ coalesceEntries(other: Iterable<[Key, Value]>): this {
+ const map = new Map(this.map);
+ [...other].forEach(([key, value]) => {
+ if (!this.has(key)) {
+ map.set(key, value);
+ }
+ });
+ return this.getNewInstance(map);
+ }
+
+ without(key: Key): this {
+ const map = new Map(this.map);
+ map.delete(key);
+ return this.getNewInstance(map);
+ }
+
+ has(key: Key): boolean {
+ return this.map.has(key);
+ }
+
+ get(key: Key): Value | undefined {
+ return this.map.get(key);
+ }
+
+ get size(): number {
+ return this.map.size;
+ }
+
+ keys(): IterableIterator {
+ return this.map.keys();
+ }
+
+ values(): IterableIterator {
+ return this.map.values();
+ }
+
+ entries(): IterableIterator<[Key, Value]> {
+ return this.map.entries();
+ }
+
+ [Symbol.iterator](): IterableIterator<[Key, Value]> {
+ return this.entries();
+ }
+}
diff --git a/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts b/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts
index f11cefd7cf..e3ecde4b22 100644
--- a/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts
+++ b/mathesar_ui/src/component-library/common/utils/ImmutableSet.ts
@@ -5,24 +5,38 @@ export default class ImmutableSet {
this.set = new Set(i);
}
- with(item: T): ImmutableSet {
+ /**
+ * This method exists to allow us to subclass this class and call the
+ * constructor of the subclass from within this base class.
+ *
+ * If there's a way we can use generics to avoid `any` here, we'd love to
+ * know.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private getNewInstance(...args: any[]): this {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ return new (this.constructor as any)(...args) as this;
+ }
+
+ with(item: T): this {
const set = new Set(this.set);
set.add(item);
- return new ImmutableSet(set);
+ return this.getNewInstance(set);
}
- union(other: ImmutableSet): ImmutableSet {
+ union(other: ImmutableSet): this {
const set = new Set(this.set);
[...other.values()].forEach((value) => {
set.add(value);
});
- return new ImmutableSet(set);
+ return this.getNewInstance(set);
}
- without(item: T): ImmutableSet {
+ without(item: T): this {
const set = new Set(this.set);
set.delete(item);
- return new ImmutableSet(set);
+ //
+ return this.getNewInstance(set);
}
has(item: T): boolean {
@@ -40,4 +54,8 @@ export default class ImmutableSet {
valuesArray(): T[] {
return [...this.set.values()];
}
+
+ [Symbol.iterator](): IterableIterator {
+ return this.values();
+ }
}
diff --git a/mathesar_ui/src/component-library/common/utils/formatUtils.ts b/mathesar_ui/src/component-library/common/utils/formatUtils.ts
index a36a511674..8c7a163edf 100644
--- a/mathesar_ui/src/component-library/common/utils/formatUtils.ts
+++ b/mathesar_ui/src/component-library/common/utils/formatUtils.ts
@@ -1,3 +1,5 @@
+import { hasStringLabelProperty } from './typeUtils';
+
export function formatSize(sizeInBytes: number): string {
if (sizeInBytes === 0) {
return '0 B';
@@ -13,3 +15,14 @@ export function formatSize(sizeInBytes: number): string {
return `${repValue.toFixed(2)} ${repUnit}B`;
}
+
+/**
+ * If the given value has a label property and it is a string, return it.
+ * Otherwise, return the value itself, converted to a string.
+ */
+export function getLabel(v: unknown): string {
+ if (hasStringLabelProperty(v)) {
+ return v.label;
+ }
+ return String(v);
+}
diff --git a/mathesar_ui/src/component-library/common/utils/index.ts b/mathesar_ui/src/component-library/common/utils/index.ts
index 6053f86655..9f4857aca3 100644
--- a/mathesar_ui/src/component-library/common/utils/index.ts
+++ b/mathesar_ui/src/component-library/common/utils/index.ts
@@ -2,6 +2,7 @@
export { default as CancellablePromise } from './CancellablePromise';
export { default as EventHandler } from './EventHandler';
export { default as ImmutableSet } from './ImmutableSet';
+export { default as ImmutableMap } from './ImmutableMap';
// Utility Functions
export * from './domUtils';
diff --git a/mathesar_ui/src/component-library/common/utils/typeUtils.ts b/mathesar_ui/src/component-library/common/utils/typeUtils.ts
new file mode 100644
index 0000000000..f85f293ec0
--- /dev/null
+++ b/mathesar_ui/src/component-library/common/utils/typeUtils.ts
@@ -0,0 +1,7 @@
+function hasLabelProperty(v: unknown): v is { label: unknown } {
+ return typeof v === 'object' && v !== null && 'label' in v;
+}
+
+export function hasStringLabelProperty(v: unknown): v is { label: string } {
+ return hasLabelProperty(v) && typeof v.label === 'string';
+}
diff --git a/mathesar_ui/src/component-library/index.ts b/mathesar_ui/src/component-library/index.ts
index 88b9349e94..9f3e1a7a9e 100644
--- a/mathesar_ui/src/component-library/index.ts
+++ b/mathesar_ui/src/component-library/index.ts
@@ -33,6 +33,7 @@ export { default as Dropdown } from './dropdown/Dropdown.svelte';
export { default as FileUpload } from './file-upload/FileUpload.svelte';
export { default as Notification } from './notification/Notification.svelte';
export { default as Pagination } from './pagination/Pagination.svelte';
+export { default as SimpleSelect } from './simple-select/SimpleSelect.svelte';
export { default as Select } from './select/Select.svelte';
export { default as TabContainer } from './tabs/TabContainer.svelte';
export { default as Tree } from './tree/Tree.svelte';
diff --git a/mathesar_ui/src/component-library/pagination/Pagination.svelte b/mathesar_ui/src/component-library/pagination/Pagination.svelte
index 114ae1c892..c0eb4576a8 100644
--- a/mathesar_ui/src/component-library/pagination/Pagination.svelte
+++ b/mathesar_ui/src/component-library/pagination/Pagination.svelte
@@ -29,6 +29,12 @@
undefined;
// Total number of pages.
+ //
+ // TODO: @seancolsen says:
+ // > Refactor `pageCount` to no longer be an exported prop. We're exporting it
+ // > just so the parent component can access the calculation done within this
+ // > component. That's an unconventional flow of data. I'd rather do the
+ // > calculation in the parent and pass it down to this component.
export let pageCount = 0;
// ARIA Label for component
diff --git a/mathesar_ui/src/component-library/select/Select.svelte b/mathesar_ui/src/component-library/select/Select.svelte
index bcdc5eb0da..a46a0ec0ae 100644
--- a/mathesar_ui/src/component-library/select/Select.svelte
+++ b/mathesar_ui/src/component-library/select/Select.svelte
@@ -1,3 +1,11 @@
+
+
+
diff --git a/mathesar_ui/src/component-library/simple-select/__meta__/SimpleSelect.stories.svelte b/mathesar_ui/src/component-library/simple-select/__meta__/SimpleSelect.stories.svelte
new file mode 100644
index 0000000000..4ef4aec15a
--- /dev/null
+++ b/mathesar_ui/src/component-library/simple-select/__meta__/SimpleSelect.stories.svelte
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+ Value: {a}
+
+
+
+
+
+
+
+
+
+
+
+ foo: {b?.foo}, bar: {b?.bar}
+
+
+
diff --git a/mathesar_ui/src/components/SelectColumn.svelte b/mathesar_ui/src/components/SelectColumn.svelte
new file mode 100644
index 0000000000..21f20780dd
--- /dev/null
+++ b/mathesar_ui/src/components/SelectColumn.svelte
@@ -0,0 +1,16 @@
+
+
+ c.name}
+ valuesAreEqual={(a, b) => a.id === b.id}
+ bind:value={column}
+ {onChange}
+/>
diff --git a/mathesar_ui/src/components/SelectSortDirection.svelte b/mathesar_ui/src/components/SelectSortDirection.svelte
new file mode 100644
index 0000000000..bf9e4758a0
--- /dev/null
+++ b/mathesar_ui/src/components/SelectSortDirection.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/mathesar_ui/src/header/Header.svelte b/mathesar_ui/src/header/Header.svelte
index c00f370f5d..66c799873c 100644
--- a/mathesar_ui/src/header/Header.svelte
+++ b/mathesar_ui/src/header/Header.svelte
@@ -18,7 +18,7 @@
} from '@mathesar/stores/tabs';
import { Icon, Button, Dropdown } from '@mathesar-component-library';
- import { TabularType } from '@mathesar/App.d';
+ import { TabularType } from '@mathesar/stores/table-data';
import SchemaSelector from './schema-selector/SchemaSelector.svelte';
import ImportIndicator from './import-indicator/ImportIndicator.svelte';
diff --git a/mathesar_ui/src/sections/Base.svelte b/mathesar_ui/src/sections/Base.svelte
index 039d2e9cb5..0d36100542 100644
--- a/mathesar_ui/src/sections/Base.svelte
+++ b/mathesar_ui/src/sections/Base.svelte
@@ -3,12 +3,14 @@
import { TabContainer, Icon } from '@mathesar-component-library';
import { currentDBName } from '@mathesar/stores/databases';
import { currentSchemaId } from '@mathesar/stores/schemas';
+ import type { MathesarTab, TabList } from '@mathesar/stores/tabs/types';
import {
getTabsForSchema,
- constructTabularTabLink,
+ tabIsTabular,
+ TabType,
} from '@mathesar/stores/tabs';
- import type { MathesarTab, TabList } from '@mathesar/stores/tabs/types';
- import { TabularType } from '@mathesar/App.d';
+ import { constructTabularTabLink } from '@mathesar/stores/tabs/tabDataSaver';
+ import { TabularType } from '@mathesar/stores/table-data';
import type { TableEntry } from '@mathesar/App.d';
import ImportData from './import-data/ImportData.svelte';
@@ -35,15 +37,15 @@
// TODO: Move this entire logic to data layer without involving view layer
$: changeCurrentSchema(database, schemaId);
- function getTabLink(entry: MathesarTab): string | undefined {
- if (entry.isNew || !entry.tabularData) {
+ function getTabLink(tab: MathesarTab): string | undefined {
+ if (!tabIsTabular(tab)) {
return undefined;
}
return constructTabularTabLink(
database,
schemaId,
- entry.tabularData.type,
- entry.tabularData.id,
+ tab.tabularData.type,
+ tab.tabularData.id,
);
}
@@ -92,9 +94,9 @@
{#if $activeTab}
- {#if $activeTab.isNew}
+ {#if $activeTab.type === TabType.Import}
- {:else if $activeTab.tabularData}
+ {:else if $activeTab.type === TabType.Tabular}
{/if}
{/if}
diff --git a/mathesar_ui/src/sections/import-data/importUtils.ts b/mathesar_ui/src/sections/import-data/importUtils.ts
index 243664294a..8cf3c51ae7 100644
--- a/mathesar_ui/src/sections/import-data/importUtils.ts
+++ b/mathesar_ui/src/sections/import-data/importUtils.ts
@@ -31,7 +31,7 @@ import type {
FileUploadAddDetail,
FileUploadProgress,
} from '@mathesar-component-library/types';
-import { TabularType } from '@mathesar/App.d';
+import { TabularType } from '@mathesar/stores/table-data';
function completionCallback(
fileImportStore: FileImport,
diff --git a/mathesar_ui/src/sections/left-pane/LeftPane.svelte b/mathesar_ui/src/sections/left-pane/LeftPane.svelte
index 2e4caa0e1b..fb132b9c0c 100644
--- a/mathesar_ui/src/sections/left-pane/LeftPane.svelte
+++ b/mathesar_ui/src/sections/left-pane/LeftPane.svelte
@@ -6,6 +6,7 @@
getTabsForSchema,
constructTabularTab,
constructImportTab,
+ TabType,
} from '@mathesar/stores/tabs';
import { tables } from '@mathesar/stores/tables';
import { loadIncompleteImport } from '@mathesar/stores/fileImports';
@@ -13,7 +14,7 @@
import type { DBTablesStoreData } from '@mathesar/stores/tables';
import type { MathesarTab } from '@mathesar/stores/tabs/types';
import type { SchemaEntry, TableEntry } from '@mathesar/App.d';
- import { TabularType } from '@mathesar/App.d';
+ import { TabularType } from '@mathesar/stores/table-data';
import type { TreeItem } from '@mathesar-component-library/types';
export let database: string;
@@ -55,7 +56,7 @@
$: tree = generateTree($tables);
function onActiveTabChange(_activeTab: MathesarTab | undefined) {
- if (_activeTab?.tabularData) {
+ if (_activeTab && _activeTab.type === TabType.Tabular) {
activeOptionSet = new Set([_activeTab.tabularData.id]);
} else {
activeOptionSet = new Set();
diff --git a/mathesar_ui/src/sections/table-view/actions-pane/ActionsPane.svelte b/mathesar_ui/src/sections/table-view/actions-pane/ActionsPane.svelte
index bc74a10f49..6aaacf249c 100644
--- a/mathesar_ui/src/sections/table-view/actions-pane/ActionsPane.svelte
+++ b/mathesar_ui/src/sections/table-view/actions-pane/ActionsPane.svelte
@@ -12,11 +12,7 @@
} from '@fortawesome/free-solid-svg-icons';
import { States } from '@mathesar/utils/api';
import { Button, Icon, Dropdown } from '@mathesar-component-library';
- import type {
- TabularDataStore,
- ColumnsData,
- } from '@mathesar/stores/table-data/types';
- import type { SelectOption } from '@mathesar-component-library/types';
+ import type { TabularDataStore } from '@mathesar/stores/table-data/types';
import { refetchTablesForSchema, deleteTable } from '@mathesar/stores/tables';
import { currentSchemaId } from '@mathesar/stores/schemas';
import { currentDBName } from '@mathesar/stores/databases';
@@ -31,22 +27,19 @@
const tabularData = getContext('tabularData');
- function getColumnOptions(columnsData: ColumnsData): SelectOption[] {
- return (
- columnsData?.columns?.map((column) => ({
- id: column.name,
- label: column.name,
- })) || []
- );
- }
-
const tableConstraintsModal = modal.spawnModalController();
const tableRenameModal = modal.spawnModalController();
$: ({ columnsDataStore, recordsData, meta, constraintsDataStore } =
$tabularData);
- $: ({ filter, sort, group, selectedRecords, combinedModificationState } =
- meta);
+ $: ({ columns } = $columnsDataStore);
+ $: ({
+ filtering,
+ sorting,
+ grouping,
+ selectedRecords,
+ combinedModificationState,
+ } = meta);
$: recordState = recordsData.state;
$: isLoading =
@@ -57,7 +50,6 @@
$columnsDataStore.state === States.Error ||
$recordState === States.Error ||
$constraintsDataStore.state === States.Error;
- $: columnOptions = getColumnOptions($columnsDataStore);
function refresh() {
void $tabularData.refresh();
@@ -121,13 +113,13 @@
Filters
- {#if $filter?.filters?.length > 0}
- ({$filter?.filters?.length})
+ {#if $filtering.entries.length > 0}
+ ({$filtering.entries.length})
{/if}
-
+
@@ -136,13 +128,13 @@
Sort
- {#if $sort?.size > 0}
- ({$sort?.size})
+ {#if $sorting.size > 0}
+ ({$sorting.size})
{/if}
-
+
@@ -151,13 +143,13 @@
Group
- {#if $group?.size > 0}
- ({$group?.size})
+ {#if $grouping.size > 0}
+ ({$grouping.size})
{/if}
-
+
diff --git a/mathesar_ui/src/sections/table-view/actions-pane/RenameTableModal.svelte b/mathesar_ui/src/sections/table-view/actions-pane/RenameTableModal.svelte
index f4d0db64db..eb4501184c 100644
--- a/mathesar_ui/src/sections/table-view/actions-pane/RenameTableModal.svelte
+++ b/mathesar_ui/src/sections/table-view/actions-pane/RenameTableModal.svelte
@@ -10,7 +10,8 @@
import { currentSchemaId } from '@mathesar/stores/schemas';
import { constructTabularTab, getTabsForSchema } from '@mathesar/stores/tabs';
import { currentDBName } from '@mathesar/stores/databases';
- import { TabularType } from '@mathesar/App.d';
+ import { TabularType } from '@mathesar/stores/table-data';
+
import ModalTextInputForm from '@mathesar/components/ModalTextInputForm.svelte';
import Identifier from '@mathesar/components/Identifier.svelte';
diff --git a/mathesar_ui/src/sections/table-view/display-options/DisplayFilter.svelte b/mathesar_ui/src/sections/table-view/display-options/DisplayFilter.svelte
index 760195d275..66908bcd2f 100644
--- a/mathesar_ui/src/sections/table-view/display-options/DisplayFilter.svelte
+++ b/mathesar_ui/src/sections/table-view/display-options/DisplayFilter.svelte
@@ -1,90 +1,90 @@