diff --git a/src/DragDropApp.tsx b/src/DragDropApp.tsx index 2eb6e366..fffff183 100644 --- a/src/DragDropApp.tsx +++ b/src/DragDropApp.tsx @@ -6,7 +6,7 @@ import { KanbanView } from './KanbanView'; import { DraggableItem } from './components/Item/Item'; import { DraggableLane } from './components/Lane/Lane'; import { KanbanContext } from './components/context'; -import { c, getDateColorFn, getTagColorFn, maybeCompleteForMove } from './components/helpers'; +import { c, maybeCompleteForMove } from './components/helpers'; import { Board, DataTypes, Item, Lane } from './components/types'; import { DndContext } from './dnd/components/DndContext'; import { DragOverlay } from './dnd/components/DragOverlay'; @@ -285,8 +285,6 @@ export function DragDropApp({ win, plugin }: { win: Window; plugin: KanbanPlugin stateManager, boardModifiers, filePath, - getTagColor: getTagColorFn(stateManager), - getDateColor: getDateColorFn(stateManager), }, ]; }, [entity]); diff --git a/src/KanbanView.tsx b/src/KanbanView.tsx index 3d0a4605..e7986b8d 100644 --- a/src/KanbanView.tsx +++ b/src/KanbanView.tsx @@ -227,13 +227,14 @@ export class KanbanView extends TextFileView implements HoverParent { return; } - this.activeEditor = null; - this.previewQueue.clear(); - this.previewCache.clear(); - this.emitter.emit('queueEmpty'); - - Object.values(this.actionButtons).forEach((b) => b.remove()); - this.actionButtons = {}; + if (clear) { + this.activeEditor = null; + this.previewQueue.clear(); + this.previewCache.clear(); + this.emitter.emit('queueEmpty'); + Object.values(this.actionButtons).forEach((b) => b.remove()); + this.actionButtons = {}; + } this.plugin.addView(this, data, !clear && this.isPrimary); } diff --git a/src/components/Item/ItemContent.tsx b/src/components/Item/ItemContent.tsx index 64d1f44e..2700fb4a 100644 --- a/src/components/Item/ItemContent.tsx +++ b/src/components/Item/ItemContent.tsx @@ -20,7 +20,7 @@ import { MarkdownRenderer, } from '../MarkdownRenderer/MarkdownRenderer'; import { KanbanContext, SearchContext } from '../context'; -import { c } from '../helpers'; +import { c, useGetDateColorFn, useGetTagColorFn } from '../helpers'; import { EditState, EditingState, Item, isEditing } from '../types'; import { DateAndTime, RelativeDate } from './DateAndTime'; import { InlineMetadata } from './InlineMetadata'; @@ -123,10 +123,19 @@ function checkCheckbox(stateManager: StateManager, title: string, checkboxIndex: return results.join('\n'); } -export function Tags({ tags, searchQuery }: { tags?: string[]; searchQuery?: string }) { - const { stateManager, getTagColor } = useContext(KanbanContext); +export function Tags({ + tags, + searchQuery, + alwaysShow, +}: { + tags?: string[]; + searchQuery?: string; + alwaysShow?: boolean; +}) { + const { stateManager } = useContext(KanbanContext); + const getTagColor = useGetTagColorFn(stateManager); const search = useContext(SearchContext); - const shouldShow = stateManager.useSetting('move-tags'); + const shouldShow = stateManager.useSetting('move-tags') || alwaysShow; if (!tags.length || !shouldShow) return null; @@ -179,7 +188,8 @@ export const ItemContent = memo(function ItemContent({ showMetadata = true, isStatic, }: ItemContentProps) { - const { stateManager, filePath, boardModifiers, getDateColor } = useContext(KanbanContext); + const { stateManager, filePath, boardModifiers } = useContext(KanbanContext); + const getDateColor = useGetDateColorFn(stateManager); const titleRef = useRef(null); useEffect(() => { diff --git a/src/components/Item/ItemMenu.ts b/src/components/Item/ItemMenu.ts index 7b8019ab..01589099 100644 --- a/src/components/Item/ItemMenu.ts +++ b/src/components/Item/ItemMenu.ts @@ -52,7 +52,7 @@ export function useItemMenu({ i.setIcon('lucide-file-plus-2') .setTitle(t('New note from card')) .onClick(async () => { - const prevTitle = item.data.title.split('\n')[0].trim(); + const prevTitle = item.data.titleRaw.split('\n')[0].trim(); const sanitizedTitle = prevTitle .replace(embedRegEx, '$1') .replace(wikilinkRegEx, '$1') diff --git a/src/components/Item/MetadataTable.tsx b/src/components/Item/MetadataTable.tsx index 50e76513..2a69503e 100644 --- a/src/components/Item/MetadataTable.tsx +++ b/src/components/Item/MetadataTable.tsx @@ -1,5 +1,7 @@ import classcat from 'classcat'; +import { isPlainObject } from 'is-plain-object'; import { TFile, moment } from 'obsidian'; +import { getAPI } from 'obsidian-dataview'; import { ComponentChild } from 'preact'; import { memo, useContext, useMemo } from 'preact/compat'; import { KanbanView } from 'src/KanbanView'; @@ -8,7 +10,7 @@ import { InlineField, taskFields } from 'src/parsers/helpers/inlineMetadata'; import { MarkdownRenderer } from '../MarkdownRenderer/MarkdownRenderer'; import { KanbanContext } from '../context'; -import { c, parseMetadataWithOptions } from '../helpers'; +import { c, parseMetadataWithOptions, useGetDateColorFn } from '../helpers'; import { DataKey, FileMetadata, Item, PageData } from '../types'; import { Tags } from './ItemContent'; @@ -120,22 +122,24 @@ function getDate(v: any) { } } if (moment.isMoment(v)) return v; - if (v.ts) return moment(v.ts); if (v instanceof Date) return moment(v); + const dv = getAPI(); + if (dv?.value.isDate(v)) return moment(v.ts); return null; } export function anyToString(v: any, stateManager: StateManager): string { - if (v.value) v = v.value; + if (isPlainObject(v) && v.value) v = v.value; const date = getDate(v); if (date) return getDateFromObj(date, stateManager); if (typeof v === 'string') return v; if (v instanceof TFile) return v.path; - if (typeof v === 'object' && v.path) return v.display || v.path; if (Array.isArray(v)) { return v.map((v2) => anyToString(v2, stateManager)).join(' '); } if (v.rrule) return v.toText(); + const dv = getAPI(); + if (dv) return dv.value.toString(v); return `${v}`; } @@ -144,7 +148,8 @@ export function pageDataToString(data: PageData, stateManager: StateManager): st } export function MetadataValue({ data, dateLabel, searchQuery }: MetadataValueProps) { - const { view, stateManager, getDateColor } = useContext(KanbanContext); + const { view, stateManager } = useContext(KanbanContext); + const getDateColor = useGetDateColorFn(stateManager); const renderChild = (v: any, sep?: string) => { const link = getLinkFromObj(v, view); @@ -253,7 +258,7 @@ export const MetadataTable = memo(function MetadataTable({ data-value={pageDataToString(data, stateManager)} > {k === 'tags' ? ( - + ) : ( )} diff --git a/src/components/Kanban.tsx b/src/components/Kanban.tsx index 4938802e..8812eb5d 100644 --- a/src/components/Kanban.tsx +++ b/src/components/Kanban.tsx @@ -19,7 +19,7 @@ import { Lanes } from './Lane/Lane'; import { LaneForm } from './Lane/LaneForm'; import { TableView } from './Table/Table'; import { KanbanContext, SearchContext } from './context'; -import { baseClassName, c, getDateColorFn, getTagColorFn, useSearchValue } from './helpers'; +import { baseClassName, c, useSearchValue } from './helpers'; import { DataTypes } from './types'; const boardScrollTiggers = [DataTypes.Item, DataTypes.Lane]; @@ -173,8 +173,6 @@ export const Kanban = ({ view, stateManager }: KanbanProps) => { stateManager, boardModifiers, filePath, - getTagColor: getTagColorFn(stateManager), - getDateColor: getDateColorFn(stateManager), }; }, [view, stateManager, boardModifiers, filePath, dateColors, tagColors]); diff --git a/src/components/MarkdownRenderer/MarkdownRenderer.tsx b/src/components/MarkdownRenderer/MarkdownRenderer.tsx index 5410b085..8a8b9367 100644 --- a/src/components/MarkdownRenderer/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer/MarkdownRenderer.tsx @@ -11,7 +11,7 @@ import { PromiseCapability } from 'src/helpers/util'; import { applyCheckboxIndexes } from '../../helpers/renderMarkdown'; import { IntersectionObserverContext, KanbanContext, SortContext } from '../context'; -import { c } from '../helpers'; +import { c, useGetDateColorFn, useGetTagColorFn } from '../helpers'; import { DateColor, TagColor } from '../types'; interface MarkdownRendererProps extends HTMLAttributes { @@ -225,11 +225,13 @@ export const MarkdownRenderer = memo(function MarkdownPreviewRenderer({ searchQuery, ...divProps }: MarkdownRendererProps) { - const { view, getDateColor, getTagColor } = useContext(KanbanContext); + const { view, stateManager } = useContext(KanbanContext); const entityManager = useContext(EntityManagerContext); const dndManager = useContext(DndManagerContext); const sortContext = useContext(SortContext); const intersectionContext = useContext(IntersectionObserverContext); + const getTagColor = useGetTagColorFn(stateManager); + const getDateColor = useGetDateColorFn(stateManager); const renderer = useRef(); const elRef = useRef(); diff --git a/src/components/Table/Cells.tsx b/src/components/Table/Cells.tsx index a4a168da..96c1e4b0 100644 --- a/src/components/Table/Cells.tsx +++ b/src/components/Table/Cells.tsx @@ -12,7 +12,7 @@ import { ItemContent, useDatePickers } from '../Item/ItemContent'; import { useItemMenu } from '../Item/ItemMenu'; import { MarkdownRenderer } from '../MarkdownRenderer/MarkdownRenderer'; import { KanbanContext, SearchContext } from '../context'; -import { c } from '../helpers'; +import { c, useGetDateColorFn } from '../helpers'; import { EditState, Item, Lane, isEditing } from '../types'; import { TableItem } from './types'; @@ -25,8 +25,9 @@ export const DateCell = memo(function DateCell({ hideDateDisplay: boolean; shouldShowRelativeDate: boolean; }) { - const { stateManager, filePath, getDateColor } = useContext(KanbanContext); + const { stateManager, filePath } = useContext(KanbanContext); const { onEditDate, onEditTime } = useDatePickers(item.item, item.path); + const getDateColor = useGetDateColorFn(stateManager); return ( <> diff --git a/src/components/Table/helpers.tsx b/src/components/Table/helpers.tsx index c8981080..593452da 100644 --- a/src/components/Table/helpers.tsx +++ b/src/components/Table/helpers.tsx @@ -370,7 +370,7 @@ export function useTableColumns(boardData: Board, stateManager: StateManager) { if (!val) return null; const searchQuery = info.table.getState().globalFilter; if (key === 'tags') { - return ; + return ; } return ; }, diff --git a/src/components/context.ts b/src/components/context.ts index bbf77af7..9763e767 100644 --- a/src/components/context.ts +++ b/src/components/context.ts @@ -4,15 +4,13 @@ import { StateManager } from 'src/StateManager'; import { IntersectionObserverHandler } from 'src/dnd/managers/ScrollManager'; import { BoardModifiers } from '../helpers/boardModifiers'; -import { DateColor, Item, Lane, LaneSort, TagColor } from './types'; +import { Item, Lane, LaneSort } from './types'; export interface KanbanContextProps { filePath?: string; stateManager: StateManager; boardModifiers: BoardModifiers; view: KanbanView; - getTagColor: (tag: string) => TagColor; - getDateColor: (date: moment.Moment) => DateColor; } export const KanbanContext = createContext(null); diff --git a/src/components/helpers.ts b/src/components/helpers.ts index 4d7a0094..fb354b2d 100644 --- a/src/components/helpers.ts +++ b/src/components/helpers.ts @@ -224,9 +224,7 @@ export function getTemplatePlugins(app: App) { }; } -export function getTagColorFn(stateManager: StateManager): (tag: string) => TagColor { - const tagColors = stateManager.getSetting('tag-colors'); - +export function getTagColorFn(tagColors: TagColor[]) { const tagMap = (tagColors || []).reduce>((total, current) => { if (!current.tagKey) return total; total[current.tagKey] = current; @@ -239,10 +237,12 @@ export function getTagColorFn(stateManager: StateManager): (tag: string) => TagC }; } -export function getDateColorFn( - stateManager: StateManager -): (date: moment.Moment) => DateColor | null { - const dateColors = stateManager.getSetting('date-colors'); +export function useGetTagColorFn(stateManager: StateManager): (tag: string) => TagColor { + const tagColors = stateManager.useSetting('tag-colors'); + return useMemo(() => getTagColorFn(tagColors), [tagColors]); +} + +export function getDateColorFn(dateColors: DateColor[]) { const orders = (dateColors || []).map<[moment.Moment | 'today' | 'before' | 'after', DateColor]>( (c) => { if (c.isToday) { @@ -312,6 +312,13 @@ export function getDateColorFn( }; } +export function useGetDateColorFn( + stateManager: StateManager +): (date: moment.Moment) => DateColor | null { + const dateColors = stateManager.useSetting('date-colors'); + return useMemo(() => getDateColorFn(dateColors), [dateColors]); +} + export function parseMetadataWithOptions(data: InlineField, metadataKeys: DataKey[]): PageData { const options = metadataKeys.find((opts) => opts.metadataKey === data.key); diff --git a/src/components/types.ts b/src/components/types.ts index 3fbef5ae..15ca1864 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -86,7 +86,6 @@ export interface ItemData { titleSearch: string; titleSearchRaw: string; metadata: ItemMetadata; - dom?: HTMLDivElement; forceEditMode?: boolean; } diff --git a/src/helpers/patch.ts b/src/helpers/patch.ts new file mode 100644 index 00000000..035790d1 --- /dev/null +++ b/src/helpers/patch.ts @@ -0,0 +1,270 @@ +import { isPlainObject } from 'is-plain-object'; +import { getAPI } from 'obsidian-dataview'; + +type Key = string | number; +type Diffable = Record | any[]; +type OpPath = Array; + +const REMOVE = 'remove'; +const REPLACE = 'replace'; +const ADD = 'add'; + +export interface Op { + op: 'remove' | 'replace' | 'add'; + path: OpPath; + value?: any; +} + +interface Diff { + remove: Op[]; + replace: Op[]; + add: Op[]; +} + +type SkipFn = (k: OpPath, val?: any) => boolean; +type ToStringFn = (val: any) => string; + +function isDiffable(obj: any): obj is Diffable { + if (!obj) return false; + if (isPlainObject(obj) || Array.isArray(obj)) return true; + + const dv = getAPI(); + if (dv?.value.isObject(obj)) return true; + + return false; +} + +export function diff( + obj1: Diffable, + obj2: Diffable, + skip: SkipFn = () => false, + toString: ToStringFn = (val) => String(val) +): Op[] { + if (!isDiffable(obj1) || !isDiffable(obj2)) { + throw new Error('both arguments must be objects or arrays'); + } + + const diffs: Diff = getDiff( + obj1, + obj2, + [], + [], + { remove: [], replace: [], add: [] }, + skip, + toString + ); + + // reverse removes since we want to maintain indexes + return diffs.remove.reverse().concat(diffs.replace).concat(diffs.add); +} + +function getDiff( + obj1: Diffable, + obj2: Diffable, + basePath: OpPath, + basePathForRemoves: OpPath, + diffs: Diff, + skip: SkipFn, + toString: ToStringFn +) { + if (!isDiffable(obj1) || !isDiffable(obj2)) return diffs; + + const obj1Keys = Object.keys(obj1); + const obj2Keys = Object.keys(obj2); + const obj2KeysLength = obj2Keys.length; + const lengthDelta = obj1.length - obj2.length; + + let path: OpPath; + + if (trimFromRight(obj1, obj2)) { + for (const k of obj1Keys) { + const key = Array.isArray(obj1) ? Number(k) : k; + if (!(key in obj2)) { + path = basePathForRemoves.concat(key); + if (skip(path)) continue; + diffs.remove.push({ + op: REMOVE, + path, + }); + } + } + + for (const k of obj2Keys) { + const key = Array.isArray(obj2) ? Number(k) : k; + pushReplaces( + key, + obj1, + obj2, + basePath.concat(key), + basePath.concat(key), + diffs, + skip, + toString + ); + } + } else { + // trim from left, objects are both arrays + for (let i = 0; i < lengthDelta; i++) { + path = basePathForRemoves.concat(i); + if (skip(path)) continue; + diffs.remove.push({ + op: REMOVE, + path, + }); + } + + // now make a copy of obj1 with excess elements left trimmed and see if there any replaces + const obj1Trimmed = obj1.slice(lengthDelta); + for (let i = 0; i < obj2KeysLength; i++) { + pushReplaces( + i, + obj1Trimmed, + obj2, + basePath.concat(i), + // since list of removes are reversed before presenting result, + // we need to ignore existing parent removes when doing nested removes + basePath.concat(i + lengthDelta), + diffs, + skip, + toString + ); + } + } + + return diffs; +} + +function pushReplaces( + key: any, + obj1: Diffable, + obj2: Diffable, + path: OpPath, + pathForRemoves: OpPath, + diffs: Diff, + skip: SkipFn, + toString: ToStringFn +) { + const obj1AtKey = obj1[key]; + const obj2AtKey = obj2[key]; + + if (skip(path, obj2AtKey)) return; + + if (!(key in obj1) && key in obj2) { + diffs.add.push({ op: ADD, path, value: obj2AtKey }); + } else if (obj1AtKey !== obj2AtKey) { + if ( + Object(obj1AtKey) !== obj1AtKey || + Object(obj2AtKey) !== obj2AtKey || + differentTypes(obj1AtKey, obj2AtKey) + ) { + diffs.replace.push({ op: REPLACE, path, value: obj2AtKey }); + } else { + if ( + !isDiffable(obj1AtKey) && + !isDiffable(obj2AtKey) && + toString(obj1AtKey) !== toString(obj2AtKey) + ) { + diffs.replace.push({ op: REPLACE, path, value: obj2AtKey }); + } else { + getDiff(obj1[key], obj2[key], path, pathForRemoves, diffs, skip, toString); + } + } + } +} + +function differentTypes(a: any, b: any) { + return Object.prototype.toString.call(a) !== Object.prototype.toString.call(b); +} + +function trimFromRight(obj1: Record, obj2: Record) { + const lengthDelta = obj1.length - obj2.length; + + if (Array.isArray(obj1) && Array.isArray(obj2) && lengthDelta > 0) { + let leftMatches = 0; + let rightMatches = 0; + for (let i = 0; i < obj2.length; i++) { + if (String(obj1[i]) === String(obj2[i])) { + leftMatches++; + } else { + break; + } + } + + for (let j = obj2.length; j > 0; j--) { + if (String(obj1[j + lengthDelta]) === String(obj2[j])) { + rightMatches++; + } else { + break; + } + } + + // bias to trim right becase it requires less index shifting + return leftMatches >= rightMatches; + } + + return true; +} + +export function diffApply(obj: Diffable, diff: Op[]) { + if (!isDiffable(obj)) { + throw new Error('base object must be an object or an array'); + } + + if (!Array.isArray(diff)) { + throw new Error('diff must be an array'); + } + + if (Array.isArray(obj)) obj = obj.slice(); + else obj = { ...obj }; + + for (const thisDiff of diff) { + const thisOp = thisDiff.op; + const thisPath = thisDiff.path; + const pathCopy = thisPath.slice(); + const lastProp: any = pathCopy.pop(); + let subObject = obj; + + prototypeCheck(lastProp); + if (lastProp == null) return false; + + let thisProp: any; + while ((thisProp = pathCopy.shift()) !== null) { + if (thisProp === undefined) break; + + prototypeCheck(thisProp); + if (!(thisProp in subObject)) { + subObject = subObject[thisProp] = {}; + } else if (Array.isArray(subObject[thisProp])) { + subObject = subObject[thisProp] = subObject[thisProp].slice(); + } else if (isPlainObject(subObject[thisProp])) { + subObject = subObject[thisProp] = { ...subObject[thisProp] }; + } else { + subObject = subObject[thisProp]; + } + } + + if (thisOp === REMOVE || thisOp === REPLACE) { + const path = thisDiff.path; + if (!Object.prototype.hasOwnProperty.call(subObject, lastProp)) { + throw new Error(['expected to find property', path, 'in object', obj].join(' ')); + } + } + + if (thisOp === REMOVE && typeof lastProp === 'number') { + Array.isArray(subObject) ? subObject.splice(lastProp, 1) : delete subObject[lastProp]; + } + + if (thisOp === REPLACE || thisOp === ADD) { + subObject[lastProp] = thisDiff.value; + } + } + + return obj; +} + +function prototypeCheck(prop?: string | number) { + // coercion is intentional to catch prop values like `['__proto__']` + if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') { + throw new Error('setting of prototype values not supported'); + } +} diff --git a/src/parsers/List.ts b/src/parsers/List.ts index 8255e260..43e37f37 100644 --- a/src/parsers/List.ts +++ b/src/parsers/List.ts @@ -1,10 +1,10 @@ -import { compare } from 'fast-json-patch'; -import { JSONPatchDocument, immutableJSONPatch } from 'immutable-json-patch'; -import { moment } from 'obsidian'; +import { isPlainObject } from 'is-plain-object'; import { TFile } from 'obsidian'; +import { getAPI } from 'obsidian-dataview'; import { StateManager } from 'src/StateManager'; import { Board, Item } from 'src/components/types'; +import { diff, diffApply } from '../helpers/patch'; import { BaseFormat } from './common'; import { astToUnhydratedBoard, @@ -16,6 +16,15 @@ import { import { hydrateBoard, hydratePostOp } from './helpers/hydrateBoard'; import { parseMarkdown } from './parseMarkdown'; +const generatedKeys: Array = [ + 'id', + 'date', + 'time', + 'titleSearch', + 'titleSearchRaw', + 'file', +]; + export class ListFormat implements BaseFormat { stateManager: StateManager; @@ -39,41 +48,27 @@ export class ListFormat implements BaseFormat { const { ast, settings, frontmatter } = parseMarkdown(this.stateManager, md); const newBoard = astToUnhydratedBoard(this.stateManager, settings, frontmatter, ast, md); const { state } = this.stateManager; + const dv = getAPI(); if (!this.stateManager.hasError() && state) { - const ops = compare(state, newBoard); - const filtered = ops.filter((op) => { - const path = op.path.split('/'); - - if (['id', 'dom', 'date', 'time', 'titleSearch', 'file'].includes(path.last())) { - return false; - } - - let obj: any = state; - for (const p of path) { - if (!p || !obj[p]) continue; - obj = obj[p]; - - if (typeof obj === 'function') return false; - if ( - typeof obj === 'object' && - !Array.isArray(obj) && - !Object.prototype.isPrototypeOf.call(Object.getPrototypeOf(obj), Object) - ) { - return false; - } - if (obj instanceof TFile) return false; - if (obj instanceof Date) return false; - if (moment.isMoment(obj)) return false; - if (obj.toMillis) return false; + const ops = diff( + state, + newBoard, + (path) => { + return generatedKeys.includes(path.last()); + }, + (val: any) => { + if (!val) return String(val); + if (val instanceof TFile) return val.path; + if (isPlainObject(val) || Array.isArray(val)) return String(val); + if (dv && !dv.value.isObject(val)) return dv.value.toString(val); + return String(val); } + ); - return true; - }); - - const patchedBoard = immutableJSONPatch(state, filtered as JSONPatchDocument) as Board; + const patchedBoard = diffApply(state, ops) as Board; - return hydratePostOp(this.stateManager, patchedBoard, filtered); + return hydratePostOp(this.stateManager, patchedBoard, ops); } return hydrateBoard(this.stateManager, newBoard); diff --git a/src/parsers/common.ts b/src/parsers/common.ts index d6c65b1a..eecc33e4 100644 --- a/src/parsers/common.ts +++ b/src/parsers/common.ts @@ -166,7 +166,11 @@ export function getLinkedPageMetadata( const dataviewVal = getPageData(dataviewCache, k.metadataKey); let cacheVal = getPageData(cache?.frontmatter, k.metadataKey); - if (cacheVal !== null && cacheVal !== undefined) { + if ( + cacheVal !== null && + cacheVal !== undefined && + !(Array.isArray(cacheVal) && cacheVal.length === 0) + ) { if (typeof cacheVal === 'string') { if (/^\d{4}-\d{2}-\d{2}/.test(cacheVal)) { cacheVal = moment(cacheVal); @@ -208,23 +212,17 @@ export function getLinkedPageMetadata( value: cacheVal, }; haveData = true; - } else if (dataviewVal) { + } else if ( + dataviewVal !== undefined && + dataviewVal !== null && + !(Array.isArray(dataviewVal) && dataviewVal.length === 0) + ) { const cachedValue = dataviewCache[k.metadataKey]; - let val = Array.isArray(cachedValue) - ? cachedValue - : cachedValue.values || cachedValue.val || cachedValue; - - // Protect against proxy values - if (val === cachedValue && !Array.isArray(cachedValue) && typeof val === 'object') { - val = { ...cachedValue }; - } else if (!Array.isArray(val) && typeof val !== 'string' && typeof val !== 'number') { - return; - } order.push(k.metadataKey); metadata[k.metadataKey] = { ...k, - value: val, + value: cachedValue, }; haveData = true; } diff --git a/src/parsers/formats/list.ts b/src/parsers/formats/list.ts index 7119d5e9..492778ad 100644 --- a/src/parsers/formats/list.ts +++ b/src/parsers/formats/list.ts @@ -29,7 +29,8 @@ import { getPrevSibling, getStringFromBoundary, } from '../helpers/ast'; -import { hydrateItem } from '../helpers/hydrateBoard'; +import { hydrateItem, preprocessTitle } from '../helpers/hydrateBoard'; +import { extractInlineFields, taskFields } from '../helpers/inlineMetadata'; import { dedentNewLines, executeDeletion, @@ -103,7 +104,6 @@ export function listItemToItemData(stateManager: StateManager, md: string, item: fileMetadata: undefined, fileMetadataOrder: undefined, }, - dom: undefined, checked: item.checked, checkChar: item.checked ? item.checkChar || ' ' : ' ', }; @@ -189,7 +189,36 @@ export function listItemToItemData(stateManager: StateManager, md: string, item: } ); - itemData.title = dedentNewLines(executeDeletion(title)); + const firstLineEnd = itemData.title.indexOf('\n'); + const inlineFields = extractInlineFields(itemData.title, true); + + if (inlineFields?.length) { + const inlineMetadata = (itemData.metadata.inlineMetadata = inlineFields.reduce((acc, curr) => { + if (!taskFields.has(curr.key)) acc.push(curr); + else if (firstLineEnd <= 0 || curr.end < firstLineEnd) acc.push(curr); + + return acc; + }, [])); + + const moveTaskData = stateManager.getSetting('move-task-metadata'); + const moveMetadata = stateManager.getSetting('inline-metadata-position') !== 'body'; + + if (moveTaskData || moveMetadata) { + let title = itemData.title; + for (const item of [...inlineMetadata].reverse()) { + const isTask = taskFields.has(item.key); + + if (isTask && !moveTaskData) continue; + if (!isTask && !moveMetadata) continue; + + title = title.slice(0, item.start) + title.slice(item.end); + } + + itemData.title = title; + } + } + + itemData.title = preprocessTitle(stateManager, dedentNewLines(executeDeletion(title))); itemData.metadata.tags?.sort(defaultSort); return itemData; diff --git a/src/parsers/helpers/hydrateBoard.ts b/src/parsers/helpers/hydrateBoard.ts index 150107c2..f31755ec 100644 --- a/src/parsers/helpers/hydrateBoard.ts +++ b/src/parsers/helpers/hydrateBoard.ts @@ -1,20 +1,19 @@ -import { Operation } from 'fast-json-patch'; import { moment } from 'obsidian'; import { StateManager } from 'src/StateManager'; import { c, getDateColorFn } from 'src/components/helpers'; import { Board, DataTypes, DateColor, Item, Lane } from 'src/components/types'; import { Path } from 'src/dnd/types'; import { getEntityFromPath } from 'src/dnd/util/data'; +import { Op } from 'src/helpers/patch'; import { getSearchValue } from '../common'; -import { extractInlineFields, taskFields } from './inlineMetadata'; export function hydrateLane(stateManager: StateManager, lane: Lane) { return lane; } export function preprocessTitle(stateManager: StateManager, title: string) { - const getDateColor = getDateColorFn(stateManager); + const getDateColor = getDateColorFn(stateManager.getSetting('date-colors')); const dateTrigger = stateManager.getSetting('date-trigger'); const dateFormat = stateManager.getSetting('date-format'); const dateDisplayFormat = stateManager.getSetting('date-display-format'); @@ -125,37 +124,7 @@ export function hydrateItem(stateManager: StateManager, item: Item) { } } - const firstLineEnd = item.data.title.indexOf('\n'); - const inlineFields = extractInlineFields(item.data.title, true); - - if (inlineFields?.length) { - const inlineMetadata = (item.data.metadata.inlineMetadata = inlineFields.reduce((acc, curr) => { - if (!taskFields.has(curr.key)) acc.push(curr); - else if (firstLineEnd <= 0 || curr.end < firstLineEnd) acc.push(curr); - - return acc; - }, [])); - - const moveTaskData = stateManager.getSetting('move-task-metadata'); - const moveMetadata = stateManager.getSetting('inline-metadata-position') !== 'body'; - - if (moveTaskData || moveMetadata) { - let title = item.data.title; - for (const item of [...inlineMetadata].reverse()) { - const isTask = taskFields.has(item.key); - - if (isTask && !moveTaskData) continue; - if (!isTask && !moveMetadata) continue; - - title = title.slice(0, item.start) + title.slice(item.end); - } - - item.data.title = title; - } - } - item.data.titleSearch = getSearchValue(item, stateManager); - item.data.title = preprocessTitle(stateManager, item.data.title); return item; } @@ -176,31 +145,29 @@ export function hydrateBoard(stateManager: StateManager, board: Board): Board { return board; } -function opAffectsHydration(op: Operation) { +function opAffectsHydration(op: Op) { return ( (op.op === 'add' || op.op === 'replace') && - ['/title', '/titleRaw', '/dateStr', '/timeStr', /\d$/, /\/fileAccessor\/.+$/].some( - (postFix) => { - if (typeof postFix === 'string') { - return op.path.endsWith(postFix); - } else { - return postFix.test(op.path); - } + ['title', 'titleRaw', 'dateStr', 'timeStr', /\d$/, /\/fileAccessor\/.+$/].some((postFix) => { + if (typeof postFix === 'string') { + return op.path.last().toString().endsWith(postFix); + } else { + return postFix.test(op.path.last().toString()); } - ) + }) ); } -export function hydratePostOp(stateManager: StateManager, board: Board, ops: Operation[]): Board { +export function hydratePostOp(stateManager: StateManager, board: Board, ops: Op[]): Board { const seen: Record = {}; const toHydrate = ops.reduce((paths, op) => { if (!opAffectsHydration(op)) { return paths; } - const path = op.path.split('/').reduce((path, segment) => { - if (/\d+/.test(segment)) { - path.push(Number(segment)); + const path = op.path.reduce((path, segment) => { + if (typeof segment === 'number') { + path.push(segment); } return path;