Skip to content

Commit

Permalink
Add board custom card order (marcusolsson#586, marcusolsson#652)
Browse files Browse the repository at this point in the history
  • Loading branch information
st4ng committed Feb 13, 2024
1 parent ce48eb1 commit e2d6dbf
Show file tree
Hide file tree
Showing 26 changed files with 567 additions and 230 deletions.
4 changes: 4 additions & 0 deletions src/customViewApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { ProjectDefinition, ViewId } from "./settings/settings";

export interface DataQueryResult {
data: DataFrame;
hasSort: boolean;
hasFilter: boolean;
}

/**
Expand All @@ -18,6 +20,8 @@ export interface ProjectViewProps<T = Record<string, any>> {
viewApi: ViewApi;
readonly: boolean;
getRecordColor: (record: DataRecord) => string | null;
sortRecords: (records: ReadonlyArray<DataRecord>) => DataRecord[];
getRecord: (id: string) => DataRecord | undefined;
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/lib/datasources/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ export function parseRecords(
return records;
}

/**
* Copies a data record and merges values.
*
* @param {Readonly<DataRecord>} record - the original data record
* @param {Readonly<DataRecord["values"]>} values - the values to merge into the original record
* @return {DataRecord} a new data record with the merged values
*/
export function copyRecordWithValues(
record: Readonly<DataRecord>,
values: Readonly<DataRecord["values"]>
): DataRecord {
return { ...record, values: { ...record.values, ...values } };
}

export function detectFields(records: DataRecord[]): DataField[] {
const valuesByField: Record<string, Optional<DataValue>[]> = {};

Expand Down
13 changes: 13 additions & 0 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { get } from "svelte/store";

import { app } from "src/lib/stores/obsidian";
import type { ProjectDefinition, ViewDefinition } from "src/settings/settings";
import { getContext, setContext } from "svelte";
import type { DataField } from "./dataframe/dataframe";

/**
Expand Down Expand Up @@ -88,3 +89,15 @@ export function getNameFromPath(path: string) {
const end: number = path.lastIndexOf(".");
return path.substring(start, end);
}

export type Context<T> = Readonly<{
get: () => T;
set: (value: T) => void;
}>;
export function makeContext<T>(): Context<T> {
const key = Symbol();
return {
get: () => getContext(key),
set: (value: T) => setContext(key, value),
};
}
4 changes: 4 additions & 0 deletions src/lib/stores/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@
"column-width": {
"name": "Column width",
"description": "Width of each column in pixels."
},
"order-sync-field": {
"name": "Sync card order with field",
"description": "Field to store the position of cards in the board."
}
},
"note": {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/viewApi.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { get } from "svelte/store";

import type { DataApi } from "./dataApi";
import type {
DataField,
DataRecord,
DataValue,
Optional,
} from "./dataframe/dataframe";
import type { DataApi } from "./dataApi";
import { dataFrame } from "./stores/dataframe";
import type { DataSource } from "./datasources";
import { dataFrame } from "./stores/dataframe";

/**
* ViewApi provides an write API for views.
Expand Down
36 changes: 33 additions & 3 deletions src/ui/app/View.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import type {
ProjectDefinition,
ProjectId,
SortDefinition,
ViewDefinition,
ViewId,
} from "src/settings/settings";
import { applyFilter, matchesCondition } from "./filterFunctions";
import { useView } from "./useView";
import { applySort } from "./viewSort";
import { useView, type ViewProps } from "./useView";
import { applySort, sortRecords } from "./viewSort";
/**
* Specify the project.
Expand Down Expand Up @@ -74,9 +75,21 @@
$: viewFilter = view.filter ?? { conditions: [] };
$: filteredFrame = applyFilter(frame, viewFilter);
$: viewSort = view.sort ?? { criteria: [] };
$: viewSort =
view.sort.criteria.length > 0
? view.sort
: ({
criteria: [{ field: "path", order: "asc", enabled: true }],
} satisfies SortDefinition);
$: sortedFrame = applySort(filteredFrame, viewSort);
let recordCache: Record<string, DataRecord | undefined>;
$: {
frame;
recordCache = {};
}
function getRecordColor(record: DataRecord): string | null {
const colorFilter = view.colors ?? { conditions: [] };
for (const cond of colorFilter.conditions) {
Expand All @@ -88,6 +101,19 @@
}
return null;
}
const applyViewSortToRecords = (
records: ReadonlyArray<DataRecord>
): Array<DataRecord> => {
return sortRecords([...records], viewSort);
};
const getRecord = (id: string) => {
return (
recordCache[id] ??
(recordCache[id] = frame.records.find((record) => record.id === id))
);
};
</script>

<!--
Expand All @@ -100,13 +126,17 @@
view,
dataProps: {
data: sortedFrame,
hasSort: view.sort.criteria.filter((c) => c.enabled).length > 0,
hasFilter: view.filter.conditions.filter((c) => c.enabled).length > 0,
},
viewApi: api,
project,
readonly,
config: view.config,
onConfigChange: handleConfigChange,
getRecordColor: getRecordColor,
sortRecords: applyViewSortToRecords,
getRecord,
}}
/>

Expand Down
4 changes: 2 additions & 2 deletions src/ui/app/onboarding/demoProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { normalizePath, stringifyYaml, type Vault } from "obsidian";
import { v4 as uuidv4 } from "uuid";

import { settings } from "src/lib/stores/settings";
import { DEFAULT_PROJECT, DEFAULT_VIEW } from "src/settings/settings";
import type { BoardConfig } from "src/ui/views/Board/types";
import type { CalendarConfig } from "src/ui/views/Calendar/types";
import type { GalleryConfig } from "src/ui/views/Gallery/types";
import type { TableConfig } from "src/ui/views/Table/types";
import { DEFAULT_PROJECT, DEFAULT_VIEW } from "src/settings/settings";

export async function createDemoProject(vault: Vault) {
const demoFolder = "Projects - Demo Project";
Expand Down Expand Up @@ -84,7 +84,7 @@ export async function createDemoProject(vault: Vault) {

const boardConfig: BoardConfig = {
groupByField: "status",
priorityField: "weight",
orderSyncField: "weight",
};

const calendarConfig: CalendarConfig = {
Expand Down
2 changes: 1 addition & 1 deletion src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
outline: "none",
borderRadius: "5px",
background: "hsla(var(--interactive-accent-hsl), 0.3)",
transition: "all 150ms easy-in-out",
transition: "all 150ms ease-in-out",
},
}}
on:consider={handleDndConsider}
Expand Down
91 changes: 43 additions & 48 deletions src/ui/app/useView.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { get } from "svelte/store";

import type { DataQueryResult } from "src/customViewApi";
import type {
DataQueryResult,
ProjectView,
ProjectViewProps,
} from "src/customViewApi";
import type { DataRecord } from "src/lib/dataframe/dataframe";
import { customViews } from "src/lib/stores/customViews";
import type { ViewApi } from "src/lib/viewApi";
import type { DataRecord } from "src/lib/dataframe/dataframe";
import type { ProjectDefinition, ViewDefinition } from "src/settings/settings";

export interface ViewProps {
Expand All @@ -15,65 +19,56 @@ export interface ViewProps {
readonly: boolean;
project: ProjectDefinition;
getRecordColor: (record: DataRecord) => string | null;
sortRecords: ProjectViewProps["sortRecords"];
getRecord: ProjectViewProps["getRecord"];
}

export function useView(node: HTMLElement, props: ViewProps) {
// Keep track of previous view id to determine if view should be invalidated.
let viewId = props.view.id;

let viewId: string;
const projectId = props.project.id;
let projectView: ProjectView<Record<string, any>> | undefined;

let projectView = get(customViews)[props.view.type];
const update = (newprops: ViewProps) => {
// User switched to a different view.
const dirty =
newprops.view.id !== viewId || newprops.project.id !== projectId;

if (projectView) {
// Component just mounted, so treat all properties as dirty.
projectView.onOpen({
viewId: props.view.id,
project: props.project,
contentEl: node,
config: props.config,
saveConfig: props.onConfigChange,
viewApi: props.viewApi,
readonly: props.readonly,
getRecordColor: props.getRecordColor,
});
projectView.onData(props.dataProps);
}
if (dirty) {
// Clean up previous view.
projectView?.onClose();

return {
update(newprops: ViewProps) {
// User switched to a different view.
const dirty =
newprops.view.id !== viewId || newprops.project.id !== projectId;
node.empty();

if (dirty) {
// Clean up previous view.
projectView?.onClose();
// Look up the next view.
projectView = get(customViews)[newprops.view.type];

node.empty();
if (projectView) {
projectView.onOpen({
contentEl: node,
viewId: newprops.view.id,
project: newprops.project,
viewApi: newprops.viewApi,
readonly: newprops.readonly,
config: newprops.config,
saveConfig: newprops.onConfigChange,
getRecordColor: newprops.getRecordColor,
sortRecords: newprops.sortRecords,
getRecord: newprops.getRecord,
});
projectView.onData(newprops.dataProps);
}

// Look up the next view.
projectView = get(customViews)[newprops.view.type];
viewId = newprops.view.id;
} else {
projectView?.onData(newprops.dataProps);
}
};

if (projectView) {
projectView.onOpen({
contentEl: node,
viewId: newprops.view.id,
project: newprops.project,
viewApi: newprops.viewApi,
readonly: newprops.readonly,
config: newprops.config,
saveConfig: newprops.onConfigChange,
getRecordColor: newprops.getRecordColor,
});
projectView.onData(newprops.dataProps);
}
update(props);

viewId = newprops.view.id;
} else {
projectView?.onData(newprops.dataProps);
}
},
return {
update,
destroy() {
projectView?.onClose();
},
Expand Down
43 changes: 27 additions & 16 deletions src/ui/app/viewSort.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
import produce from "immer";
import type { SortDefinition, SortingCriteria } from "src/settings/settings";
import type { DataRecord } from "../../lib/dataframe/dataframe";
import {
isNumber,
isDate,
isBoolean,
isDate,
isNumber,
type DataFrame,
} from "../../lib/dataframe/dataframe";
import type { SortDefinition, SortingCriteria } from "src/settings/settings";
import type { DataRecord } from "../../lib/dataframe/dataframe";

export function applySort(frame: DataFrame, sort: SortDefinition): DataFrame {
return produce(frame, (draft) => {
draft.records = draft.records.sort((a, b): number => {
let res = 0;
const records: DataRecord[] = draft.records;
sortRecords(records, sort);
});
}

/**
* Sorts records in place. This method mutates the array and returns a reference to the same array.
*
* @param {DataRecord[]} records - the records to be sorted
* @param {SortDefinition} sort - the definition for sorting the records
*/
export function sortRecords(records: DataRecord[], sort: SortDefinition) {
return records.sort((a, b): number => {
let res = 0;

const enabledCriteria = sort.criteria.filter((c) => c.enabled);
const enabledCriteria = sort.criteria.filter((c) => c.enabled);

for (let i = 0; i < enabledCriteria.length; i++) {
const criteria = enabledCriteria[i];
for (let i = 0; i < enabledCriteria.length; i++) {
const criteria = enabledCriteria[i];

// @ts-ignore
res = sortCriteria(a, b, criteria);
// @ts-ignore
res = sortCriteria(a, b, criteria);

if (res !== 0) {
break;
}
if (res !== 0) {
break;
}
}

return res;
});
return res;
});
}

Expand Down
Loading

0 comments on commit e2d6dbf

Please sign in to comment.