diff --git a/manifest.json b/manifest.json index c53cba7..423397d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "vault-explorer", "name": "Vault Explorer", - "version": "1.2.1", + "version": "1.3.0", "minAppVersion": "1.4.13", "description": "Explore your vault in visual format", "author": "DecafDev", diff --git a/package.json b/package.json index e0376f5..092f1c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-vault-explorer", - "version": "1.2.1", + "version": "1.3.0", "description": "Explore your vault in visual format", "main": "main.js", "scripts": { diff --git a/src/main.ts b/src/main.ts index 2d6fa8a..269cb73 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ import { Plugin, TAbstractFile, TFile, TFolder } from 'obsidian'; import VaultExplorerView from './obsidian/vault-explorer-view'; import VaultExplorerSettingsTab from './obsidian/vault-explorer-settings-tab'; -import { VaultExplorerPluginSettings, ViewType } from './types'; +import { PropertyFilter, PropertyFilterGroup, PropertyType, VaultExplorerPluginSettings, ViewType } from './types'; import { DEFAULT_SETTINGS, VAULT_EXPLORER_VIEW } from './constants'; import _ from 'lodash'; import EventManager from './event/event-manager'; @@ -16,6 +16,7 @@ import { LOG_LEVEL_WARN } from './logger/constants'; import { VaultExplorerPluginSettings_1_0_1 } from './types/types-1.0.1'; import { moveFocus } from './focus-utils'; import { VaultExplorerPluginSettings_1_2_0 } from './types/types-1.2.0'; +import { VaultExplorerPluginSettings_1_2_1 } from './types/types-1.2.1'; export default class VaultExplorerPlugin extends Plugin { settings: VaultExplorerPluginSettings = DEFAULT_SETTINGS; @@ -189,7 +190,7 @@ export default class VaultExplorerPlugin extends Plugin { if (isVersionLessThan(settingsVersion, "1.2.1")) { console.log("Upgrading settings from version 1.2.0 to 1.2.1"); const typedData = (data as unknown) as VaultExplorerPluginSettings_1_2_0; - const newData: VaultExplorerPluginSettings = { + const newData: VaultExplorerPluginSettings_1_2_1 = { ...typedData, views: { ...typedData.views, @@ -198,6 +199,38 @@ export default class VaultExplorerPlugin extends Plugin { } data = newData as unknown as Record; } + + if (isVersionLessThan(settingsVersion, "1.3.0")) { + console.log("Upgrading settings from version 1.2.1 to 1.3.0"); + const typedData = (data as unknown) as VaultExplorerPluginSettings_1_2_1; + const groups = typedData.filters.properties.groups; + + const updatedGroups: PropertyFilterGroup[] = groups.map(group => { + const updatedFilters: PropertyFilter[] = group.filters.map(filter => { + return { + ...filter, + type: filter.type as any, + matchWhenPropertyDNE: false + } + }); + return { + ...group, + filters: updatedFilters + } + }); + + const newData: VaultExplorerPluginSettings = { + ...typedData, + filters: { + ...typedData.filters, + properties: { + ...typedData.filters.properties, + groups: updatedGroups, + } + } + } + data = newData as unknown as Record; + } } } diff --git a/src/svelte/app/services/filters/property-filter.ts b/src/svelte/app/services/filters/property-filter.ts index 2ba7368..86da230 100644 --- a/src/svelte/app/services/filters/property-filter.ts +++ b/src/svelte/app/services/filters/property-filter.ts @@ -23,7 +23,7 @@ export const filterByProperty = (frontmatter: FrontMatterCache | undefined, grou return group.filters.every((filter) => { if (!filter.isEnabled) return true; - const { propertyName, condition, value, type } = filter; + const { propertyName, condition, value, type, matchWhenPropertyDNE } = filter; if (propertyName === "") return true; let propertyValue: (string | string[] | boolean | number | null) = frontmatter?.[propertyName] ?? null; @@ -34,7 +34,7 @@ export const filterByProperty = (frontmatter: FrontMatterCache | undefined, grou Logger.warn(`Property value is not a string: ${propertyValue}`); return true; } - const doesMatch = doesTextMatchFilter(condition, propertyValue, value); + const doesMatch = doesTextMatchFilter(condition, propertyValue, value, matchWhenPropertyDNE); return doesMatch; } else if (type === "list") { if (propertyValue !== null && !Array.isArray(propertyValue)) { @@ -42,7 +42,7 @@ export const filterByProperty = (frontmatter: FrontMatterCache | undefined, grou return true; } const compare = value.split(",").map((v) => v.trim()); - const doesMatch = doesListMatchFilter(condition, propertyValue, compare); + const doesMatch = doesListMatchFilter(condition, propertyValue, compare, matchWhenPropertyDNE); return doesMatch; } else if (type === "number") { if (propertyValue !== null && typeof propertyValue !== "number") { @@ -50,7 +50,7 @@ export const filterByProperty = (frontmatter: FrontMatterCache | undefined, grou return true; } const compare = parseFloat(value); - const doesMatch = doesNumberMatchFilter(condition, propertyValue, compare); + const doesMatch = doesNumberMatchFilter(condition, propertyValue, compare, matchWhenPropertyDNE); return doesMatch; } else if (type === "checkbox") { if (propertyValue !== null && typeof propertyValue !== "boolean") { @@ -59,7 +59,7 @@ export const filterByProperty = (frontmatter: FrontMatterCache | undefined, grou } const compare = value === "true"; - const doesMatch = doesCheckboxMatchFilter(condition, propertyValue, compare); + const doesMatch = doesCheckboxMatchFilter(condition, propertyValue, compare, matchWhenPropertyDNE); return doesMatch; } else if (type === "date" || type === "datetime") { @@ -68,7 +68,7 @@ export const filterByProperty = (frontmatter: FrontMatterCache | undefined, grou return true; } - const doesMatch = doesDateMatchFilter(condition, propertyValue, value); + const doesMatch = doesDateMatchFilter(condition, propertyValue, value, matchWhenPropertyDNE); return doesMatch; } else { @@ -82,6 +82,7 @@ const doesTextMatchFilter = ( condition: FilterCondition, propertyValue: string | null, compare: string, + matchIfNull: boolean ): boolean => { if (propertyValue) propertyValue = propertyValue.toLowerCase().trim(); @@ -90,22 +91,22 @@ const doesTextMatchFilter = ( switch (condition) { case TextFilterCondition.IS: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue === compare; case TextFilterCondition.IS_NOT: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue !== compare; case TextFilterCondition.CONTAINS: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue.includes(compare); case TextFilterCondition.DOES_NOT_CONTAIN: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return !propertyValue.includes(compare); case TextFilterCondition.STARTS_WITH: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue.startsWith(compare); case TextFilterCondition.ENDS_WITH: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue.endsWith(compare); case TextFilterCondition.EXISTS: return propertyValue !== null; @@ -116,16 +117,16 @@ const doesTextMatchFilter = ( } }; -const doesListMatchFilter = (condition: ListFilterCondition, propertyValue: string[] | null, compare: string[]) => { +const doesListMatchFilter = (condition: ListFilterCondition, propertyValue: string[] | null, compare: string[], matchIfNull: boolean) => { switch (condition) { case ListFilterCondition.CONTAINS: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue.some((value) => //Union compare.some((c) => c === value) ); case ListFilterCondition.DOES_NOT_CONTAIN: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue.every((value) => //Complement compare.every((c) => c !== value) @@ -141,11 +142,11 @@ const doesListMatchFilter = (condition: ListFilterCondition, propertyValue: stri const doesDateMatchFilter = (condition: DateFilterCondition, propertyValue: string | null, - compare: string | null) => { + compare: string | null, matchIfNull: boolean) => { switch (condition) { case DateFilterCondition.IS: { - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; if (compare === null) return false; const propertyValueTime = getMillis(propertyValue); @@ -158,7 +159,7 @@ const doesDateMatchFilter = (condition: DateFilterCondition, ); } case DateFilterCondition.IS_AFTER: { - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; if (compare === null) return false; const propertyValueTime = getMillis(propertyValue); @@ -166,7 +167,7 @@ const doesDateMatchFilter = (condition: DateFilterCondition, return propertyValueTime > dayEndTime; } case DateFilterCondition.IS_BEFORE: { - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; if (compare === null) return false; const propertyValueTime = getMillis(propertyValue); @@ -186,25 +187,26 @@ export const doesNumberMatchFilter = ( condition: NumberFilterCondition, propertyValue: number | null, compare: number, + matchIfNull: boolean ) => { switch (condition) { case NumberFilterCondition.IS_EQUAL: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue === compare; case NumberFilterCondition.IS_GREATER: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue > compare; case NumberFilterCondition.IS_GREATER_OR_EQUAL: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue >= compare; case NumberFilterCondition.IS_LESS: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue < compare; case NumberFilterCondition.IS_LESS_OR_EQUAL: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue <= compare; case NumberFilterCondition.IS_NOT_EQUAL: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue !== compare; case NumberFilterCondition.EXISTS: return propertyValue !== null; @@ -219,13 +221,14 @@ export const doesCheckboxMatchFilter = ( condition: CheckboxFilterCondition, propertyValue: boolean | null, compare: boolean, + matchIfNull: boolean ) => { switch (condition) { case CheckboxFilterCondition.IS: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue === compare; case CheckboxFilterCondition.IS_NOT: - if (propertyValue === null) return false; + if (propertyValue === null) return matchIfNull; return propertyValue !== compare; case CheckboxFilterCondition.EXISTS: return propertyValue !== null; diff --git a/src/svelte/properties-filter-app/components/group-edit-view.svelte b/src/svelte/properties-filter-app/components/group-edit-view.svelte index 363f7a2..841067a 100644 --- a/src/svelte/properties-filter-app/components/group-edit-view.svelte +++ b/src/svelte/properties-filter-app/components/group-edit-view.svelte @@ -39,10 +39,11 @@ filters={selectedGroup.filters} on:filterConditionChange on:filterTypeChange - on:filterNameChange + on:filterPropertyNameChange on:filterValueChange on:filterToggle on:filterDeleteClick + on:filterMatchWhenPropertyDNEChange /> {/if} diff --git a/src/svelte/properties-filter-app/components/property-filter-list.svelte b/src/svelte/properties-filter-app/components/property-filter-list.svelte index 9a0f63b..d158167 100644 --- a/src/svelte/properties-filter-app/components/property-filter-list.svelte +++ b/src/svelte/properties-filter-app/components/property-filter-list.svelte @@ -12,16 +12,18 @@ id={filter.id} value={filter.value} condition={filter.condition} + matchWhenPropertyDNE={filter.matchWhenPropertyDNE} type={filter.type} - name={filter.propertyName} + propertyName={filter.propertyName} isEnabled={filter.isEnabled} on:groupChange on:filterConditionChange on:filterTypeChange - on:filterNameChange + on:filterPropertyNameChange on:filterValueChange on:filterToggle on:filterDeleteClick + on:filterMatchWhenPropertyDNEChange /> {/each} diff --git a/src/svelte/properties-filter-app/components/property-filter.svelte b/src/svelte/properties-filter-app/components/property-filter.svelte index e4394be..a1a2db4 100644 --- a/src/svelte/properties-filter-app/components/property-filter.svelte +++ b/src/svelte/properties-filter-app/components/property-filter.svelte @@ -8,18 +8,19 @@ FilterCondition, ListFilterCondition, NumberFilterCondition, - PropertyFilterType, + PropertyType, TextFilterCondition, } from "src/types"; import { getDisplayNameForFilterCondition } from "./utils"; import { getAllObsidianProperties } from "src/obsidian/utils"; export let id: string; - export let name: string; - export let type: PropertyFilterType; + export let propertyName: string; + export let type: PropertyType; export let value: string; export let condition: FilterCondition; export let isEnabled: boolean; + export let matchWhenPropertyDNE: boolean; let plugin: VaultExplorerPlugin; let obsidianProperties: ObsidianProperty[] = []; @@ -43,10 +44,10 @@ function handlePropertyNameChange(e: Event) { const value = (e.target as HTMLInputElement).value; - dispatch("filterNameChange", { id, name: value }); + dispatch("filterPropertyNameChange", { id, name: value }); } - function handlePropertyTypeChange(e: Event) { + function handleTypeChange(e: Event) { const value = (e.target as HTMLInputElement).value; dispatch("filterTypeChange", { id, type: value }); } @@ -61,6 +62,14 @@ dispatch("filterValueChange", { id, value }); } + function handleMatchWhenDNEChange(e: Event) { + const value = (e.target as HTMLInputElement).checked; + dispatch("filterMatchWhenPropertyDNEChange", { + id, + matchWhenDNE: value, + }); + } + function handleToggle() { dispatch("filterToggle", { id }); } @@ -78,7 +87,7 @@ return prop.type === type; }); - function findFilterConditions(type: PropertyFilterType): FilterCondition[] { + function findFilterConditions(type: PropertyType): FilterCondition[] { if (type === "text") { return Object.values(TextFilterCondition); } else if (type === "number") { @@ -96,13 +105,13 @@
- - + {#each Object.values(PropertyType) as type} {/each} - {#each filteredObsidianProperties as prop (prop.name)} @@ -118,6 +127,14 @@ {#if condition !== TextFilterCondition.EXISTS && condition !== TextFilterCondition.DOES_NOT_EXIST} {/if} + {#if condition !== TextFilterCondition.EXISTS && condition !== TextFilterCondition.DOES_NOT_EXIST} + + {/if} handleToggle()} /> @@ -243,7 +244,7 @@ throw new Error(`Unhandled filter type: ${type}`); } - const newGroups = groups.map((group) => + const newGroups: PropertyFilterGroup[] = groups.map((group) => group.id === selectedGroupId ? { ...group, @@ -252,7 +253,7 @@ ? { ...filter, type, - propertyName: "", + name: "", condition, value: "", } @@ -281,6 +282,28 @@ groups = newGroups; } + + function handleFilterMatchWhenPropertyDNEChange(e: CustomEvent) { + const { id, matchWhenDNE } = e.detail; + + const newGroups = groups.map((group) => + group.id === selectedGroupId + ? { + ...group, + filters: group.filters.map((filter) => + filter.id === id + ? { + ...filter, + matchWhenPropertyDNE: matchWhenDNE, + } + : filter, + ), + } + : group, + ); + + groups = newGroups; + }
@@ -304,9 +327,10 @@ on:filterTypeChange={handleFilterTypeChange} on:filterConditionChange={handleFilterConditionChange} on:filterDeleteClick={handleFilterDeleteClick} - on:filterNameChange={handleFilterNameChange} + on:filterPropertyNameChange={handleFilterPropertyNameChange} on:filterToggle={handleFilterToggle} on:filterValueChange={handleFilterValueChange} + on:filterMatchWhenPropertyDNEChange={handleFilterMatchWhenPropertyDNEChange} /> {/if} diff --git a/src/svelte/properties-filter-app/utils.ts b/src/svelte/properties-filter-app/utils.ts index b52808f..01d0c91 100644 --- a/src/svelte/properties-filter-app/utils.ts +++ b/src/svelte/properties-filter-app/utils.ts @@ -1,14 +1,15 @@ -import { PropertyFilterType, TextFilterCondition, TextPropertyFilter } from "src/types"; +import { PropertyType, TextFilterCondition, TextPropertyFilter } from "src/types"; import { generateUUID } from "../shared/services/uuid"; export const createPropertyFilter = (): TextPropertyFilter => { return { id: generateUUID(), - type: PropertyFilterType.TEXT, + type: PropertyType.TEXT, propertyName: "", operator: "and", isEnabled: true, condition: TextFilterCondition.IS, value: "", + matchWhenPropertyDNE: false, }; } diff --git a/src/types/index.ts b/src/types/index.ts index 4757500..9b04e4f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -87,12 +87,14 @@ interface BasePropertyFilter { id: string; propertyName: string; operator: FilterOperator; - type: PropertyFilterType; + type: PropertyType; isEnabled: boolean; value: string; + matchWhenPropertyDNE: boolean; } -export enum PropertyFilterType { +//Matches Obsidian property types +export enum PropertyType { TEXT = "text", NUMBER = "number", LIST = "list", @@ -102,27 +104,27 @@ export enum PropertyFilterType { } export interface TextPropertyFilter extends BasePropertyFilter { - type: PropertyFilterType.TEXT; + type: PropertyType.TEXT; condition: TextFilterCondition; } export interface NumberPropertyFilter extends BasePropertyFilter { - type: PropertyFilterType.NUMBER; + type: PropertyType.NUMBER; condition: NumberFilterCondition; } export interface ListPropertyFilter extends BasePropertyFilter { - type: PropertyFilterType.LIST + type: PropertyType.LIST condition: ListFilterCondition; } export interface CheckboxPropertyFilter extends BasePropertyFilter { - type: PropertyFilterType.CHECKBOX + type: PropertyType.CHECKBOX condition: CheckboxFilterCondition; } export interface DatePropertyFilter extends BasePropertyFilter { - type: PropertyFilterType.DATE | PropertyFilterType.DATETIME; + type: PropertyType.DATE | PropertyType.DATETIME; condition: DateFilterCondition; } diff --git a/src/types/types-1.2.1.ts b/src/types/types-1.2.1.ts new file mode 100644 index 0000000..b247a7d --- /dev/null +++ b/src/types/types-1.2.1.ts @@ -0,0 +1,139 @@ +export interface VaultExplorerPluginSettings_1_2_1 { + logLevel: string; + properties: { + favorite: string; + url: string; + custom1: string; + custom2: string; + custom3: string; + }, + filters: { + folder: string; + search: string; + onlyFavorites: boolean; + sort: SortFilter; + timestamp: TimestampFilter; + properties: { + selectedGroupId: string; + groups: PropertyFilterGroup[]; + } + }, + views: { + currentView: ViewType; + order: ViewType[]; + titleWrapping: WordBreak; + } + pageSize: number; + pluginVersion: string | null; +} + +type WordBreak = "normal" | "break-word"; + +enum ViewType { + GRID = "grid", + LIST = "list", +} + +type FilterOperator = "and" | "or"; + +enum TextFilterCondition { + IS = "is", + IS_NOT = "is-not", + CONTAINS = "contains", + DOES_NOT_CONTAIN = "does-not-contain", + STARTS_WITH = "starts-with", + ENDS_WITH = "ends-with", + EXISTS = "exists", + DOES_NOT_EXIST = "does-not-exist", +} + +enum ListFilterCondition { + CONTAINS = "contains", + DOES_NOT_CONTAIN = "does-not-contain", + EXISTS = "exists", + DOES_NOT_EXIST = "does-not-exist", +} + +enum NumberFilterCondition { + IS_EQUAL = "is-equal", + IS_NOT_EQUAL = "is-not-equal", + IS_GREATER = "is-greater", + IS_LESS = "is-less", + IS_GREATER_OR_EQUAL = "is-greater-or-equal", + IS_LESS_OR_EQUAL = "is-less-or-equal", + EXISTS = "exists", + DOES_NOT_EXIST = "does-not-exist", +} + +enum CheckboxFilterCondition { + IS = "is", + IS_NOT = "is-not", + EXISTS = "exists", + DOES_NOT_EXIST = "does-not-exist", +} + +enum DateFilterCondition { + IS = "is", + IS_BEFORE = "is-before", + IS_AFTER = "is-after", + EXISTS = "exists", + DOES_NOT_EXIST = "does-not-exist", +} + +type FilterCondition = TextFilterCondition | NumberFilterCondition | DateFilterCondition | CheckboxFilterCondition | ListFilterCondition; + +interface BasePropertyFilter { + id: string; + propertyName: string; + operator: FilterOperator; + type: PropertyFilterType; + isEnabled: boolean; + value: string; +} + +enum PropertyFilterType { + TEXT = "text", + NUMBER = "number", + LIST = "list", + CHECKBOX = "checkbox", + DATE = "date", + DATETIME = "datetime", +} + +interface TextPropertyFilter extends BasePropertyFilter { + type: PropertyFilterType.TEXT; + condition: TextFilterCondition; +} + +interface NumberPropertyFilter extends BasePropertyFilter { + type: PropertyFilterType.NUMBER; + condition: NumberFilterCondition; +} + +interface ListPropertyFilter extends BasePropertyFilter { + type: PropertyFilterType.LIST + condition: ListFilterCondition; +} + +interface CheckboxPropertyFilter extends BasePropertyFilter { + type: PropertyFilterType.CHECKBOX + condition: CheckboxFilterCondition; +} + +interface DatePropertyFilter extends BasePropertyFilter { + type: PropertyFilterType.DATE | PropertyFilterType.DATETIME; + condition: DateFilterCondition; +} + +type PropertyFilter = TextPropertyFilter | NumberPropertyFilter | ListPropertyFilter | CheckboxPropertyFilter | DatePropertyFilter; + +interface PropertyFilterGroup { + id: string; + name: string; + filters: PropertyFilter[]; + isEnabled: boolean; +} + +type SortFilter = "file-name-asc" | "file-name-desc" | "modified-asc" | "modified-desc"; + +type TimestampFilter = "created-today" | "modified-today" | "created-this-week" | "modified-this-week" | "created-2-weeks" | "modified-2-weeks" | "all"; diff --git a/versions.json b/versions.json index a40075d..bdca347 100644 --- a/versions.json +++ b/versions.json @@ -47,5 +47,6 @@ "1.0.1": "1.4.13", "1.1.0": "1.4.13", "1.2.0": "1.4.13", - "1.2.1": "1.4.13" - + "1.2.1": "1.4.13", + "1.3.0": "1.4.13" +}