Match when property doesn't exist (#50)

* feat: add matchNotesWithoutProperty setting

* feat: add match without property checkbox

* feat: add business logic for handling property DNE checkbox

* refactor: rename to property type
This commit is contained in:
DecafDev 2024-06-03 17:28:01 -06:00 committed by GitHub
parent 29c4af256b
commit 06d8aa63c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 282 additions and 59 deletions

View file

@ -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",

View file

@ -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": {

View file

@ -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<string, unknown>;
}
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<string, unknown>;
}
}
}

View file

@ -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;

View file

@ -39,10 +39,11 @@
filters={selectedGroup.filters}
on:filterConditionChange
on:filterTypeChange
on:filterNameChange
on:filterPropertyNameChange
on:filterValueChange
on:filterToggle
on:filterDeleteClick
on:filterMatchWhenPropertyDNEChange
/>
<Spacer direction="vertical" size="sm" />
{/if}

View file

@ -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}
</Stack>

View file

@ -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 @@
</script>
<div class="vault-explorer-property-filter">
<Wrap spacingX="sm" spacingY="sm">
<select value={type} on:change={handlePropertyTypeChange}>
{#each Object.values(PropertyFilterType) as type}
<Wrap spacingX="sm" spacingY="sm" align="center">
<select value={type} on:change={handleTypeChange}>
{#each Object.values(PropertyType) as type}
<option value={type}>{type}</option>
{/each}
</select>
<select value={name} on:change={handlePropertyNameChange}>
<select value={propertyName} on:change={handlePropertyNameChange}>
<option value="">Select a property</option>
{#each filteredObsidianProperties as prop (prop.name)}
<option value={prop.name}>{prop.name}</option>
@ -118,6 +127,14 @@
{#if condition !== TextFilterCondition.EXISTS && condition !== TextFilterCondition.DOES_NOT_EXIST}
<input type="text" {value} on:change={handleValueChange} />
{/if}
{#if condition !== TextFilterCondition.EXISTS && condition !== TextFilterCondition.DOES_NOT_EXIST}
<input
aria-label="Match when property doesn't exist"
type="checkbox"
checked={matchWhenPropertyDNE}
on:change={handleMatchWhenDNEChange}
/>
{/if}
<Stack spacing="sm" align="center">
<Switch value={isEnabled} on:change={() => handleToggle()} />
<IconButton

View file

@ -18,6 +18,7 @@
import GroupList from "./components/group-list.svelte";
import Flex from "../shared/components/flex.svelte";
import Divider from "../shared/components/divider.svelte";
import { match } from "assert";
let selectedGroupId: string = "";
let groups: PropertyFilterGroup[] = [];
@ -187,7 +188,7 @@
groups = newGroups;
}
function handleFilterNameChange(e: CustomEvent) {
function handleFilterPropertyNameChange(e: CustomEvent) {
const { id, name } = e.detail;
const newGroups = groups.map((group) =>
@ -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;
}
</script>
<div class="vault-explorer-property-filter-app">
@ -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}
</Flex>

View file

@ -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,
};
}

View file

@ -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;
}

139
src/types/types-1.2.1.ts Normal file
View file

@ -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";

View file

@ -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"
}