mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 12:30:24 +00:00
feat: fronttmatter tag by template
This commit is contained in:
parent
3cfd46b848
commit
754d5002d4
16 changed files with 864 additions and 115 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { PluginSettings } from "@/types/pluginSettings";
|
||||
import React from "react";
|
||||
import { SettingItem } from "./SettingItem";
|
||||
import { FrontmatterMappingsSettings } from "./FrontmatterMappingsSettings";
|
||||
|
||||
export type Props = {
|
||||
settings: PluginSettings;
|
||||
|
|
@ -91,6 +92,17 @@ export const BehaviourSettings: React.FC<Props> = ({ settings, onChange }) => {
|
|||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Frontmatter mappings"
|
||||
description="Map templates or individual statuses to specific YAML keys. These mappings only apply to Markdown files with frontmatter."
|
||||
vertical
|
||||
>
|
||||
<FrontmatterMappingsSettings
|
||||
settings={settings}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Vault size limit"
|
||||
description="Disable dashboard and grouped view for vaults with more notes than this limit to improve performance. Set to 0 to disable this feature."
|
||||
|
|
|
|||
315
components/SettingsUI/FrontmatterMappingsSettings.tsx
Normal file
315
components/SettingsUI/FrontmatterMappingsSettings.tsx
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
import React, { useMemo } from "react";
|
||||
import {
|
||||
PluginSettings,
|
||||
StatusFrontmatterMapping,
|
||||
} from "@/types/pluginSettings";
|
||||
|
||||
type Props = {
|
||||
settings: PluginSettings;
|
||||
onChange: (key: keyof PluginSettings, value: unknown) => void;
|
||||
};
|
||||
|
||||
type StatusOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const isStatusMapping = (
|
||||
mapping: StatusFrontmatterMapping,
|
||||
): mapping is StatusFrontmatterMapping & { scope: "status" } =>
|
||||
mapping.scope === "status";
|
||||
|
||||
const generateId = () =>
|
||||
typeof crypto !== "undefined" && crypto.randomUUID
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
const sanitizeKeys = (raw: string): string[] =>
|
||||
raw
|
||||
.split(",")
|
||||
.map((key) => key.trim())
|
||||
.filter((key) => Boolean(key));
|
||||
|
||||
const buildStatusIdentifier = (
|
||||
templateId: string | undefined,
|
||||
statusName: string,
|
||||
): string => {
|
||||
if (!templateId) {
|
||||
return statusName;
|
||||
}
|
||||
return `${templateId}:${statusName}`;
|
||||
};
|
||||
|
||||
export const FrontmatterMappingsSettings: React.FC<Props> = ({
|
||||
settings,
|
||||
onChange,
|
||||
}) => {
|
||||
const mappings = settings.statusFrontmatterMappings || [];
|
||||
|
||||
const templateOptions = useMemo(
|
||||
() =>
|
||||
(settings.templates || []).map((template) => ({
|
||||
value: template.id,
|
||||
label: template.name || template.id,
|
||||
})),
|
||||
[settings.templates],
|
||||
);
|
||||
|
||||
const statusOptions = useMemo<StatusOption[]>(() => {
|
||||
const templateStatuses =
|
||||
settings.templates?.flatMap((template) =>
|
||||
template.statuses.map((status) => ({
|
||||
value: buildStatusIdentifier(template.id, status.name),
|
||||
label: `${status.name} (${template.name || template.id})`,
|
||||
})),
|
||||
) || [];
|
||||
|
||||
const customStatuses =
|
||||
settings.customStatuses?.map((status) => ({
|
||||
value: status.name,
|
||||
label: `${status.name} (Custom)`,
|
||||
})) || [];
|
||||
|
||||
return [...templateStatuses, ...customStatuses];
|
||||
}, [settings.templates, settings.customStatuses]);
|
||||
|
||||
const updateMappings = (nextMappings: StatusFrontmatterMapping[]) => {
|
||||
onChange("statusFrontmatterMappings", nextMappings);
|
||||
};
|
||||
|
||||
const handleMappingChange = (
|
||||
id: string,
|
||||
updater: (
|
||||
mapping: StatusFrontmatterMapping,
|
||||
) => StatusFrontmatterMapping,
|
||||
) => {
|
||||
updateMappings(
|
||||
mappings.map((mapping) =>
|
||||
mapping.id === id ? updater(mapping) : mapping,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleScopeChange = (
|
||||
id: string,
|
||||
scope: StatusFrontmatterMapping["scope"],
|
||||
) => {
|
||||
handleMappingChange(id, (mapping) => {
|
||||
const frontmatterKeys = mapping.frontmatterKeys || [];
|
||||
|
||||
if (scope === "template") {
|
||||
const templateId =
|
||||
mapping.scope === "template"
|
||||
? mapping.templateId
|
||||
: templateOptions[0]?.value || "";
|
||||
|
||||
return {
|
||||
id: mapping.id,
|
||||
scope: "template",
|
||||
templateId,
|
||||
frontmatterKeys,
|
||||
};
|
||||
}
|
||||
|
||||
const defaultStatusOption = statusOptions[0];
|
||||
const templateId = isStatusMapping(mapping)
|
||||
? mapping.templateId
|
||||
: defaultStatusOption?.value.includes(":")
|
||||
? defaultStatusOption.value.split(":", 2)[0]
|
||||
: undefined;
|
||||
const statusName = isStatusMapping(mapping)
|
||||
? mapping.statusName || ""
|
||||
: defaultStatusOption?.value?.split(":")?.pop() || "";
|
||||
|
||||
return {
|
||||
id: mapping.id,
|
||||
scope: "status",
|
||||
templateId,
|
||||
statusName,
|
||||
frontmatterKeys,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleTemplateChange = (id: string, templateId: string) => {
|
||||
handleMappingChange(id, (mapping) => ({
|
||||
...mapping,
|
||||
scope: "template",
|
||||
templateId,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleStatusChange = (id: string, identifier: string) => {
|
||||
const [maybeTemplateId, name] = identifier.includes(":")
|
||||
? identifier.split(":", 2)
|
||||
: [undefined, identifier];
|
||||
handleMappingChange(id, (mapping) => ({
|
||||
...mapping,
|
||||
templateId: maybeTemplateId,
|
||||
statusName: name,
|
||||
scope: "status",
|
||||
}));
|
||||
};
|
||||
|
||||
const handleKeysChange = (id: string, raw: string) => {
|
||||
const keys = sanitizeKeys(raw);
|
||||
handleMappingChange(id, (mapping) => ({
|
||||
...mapping,
|
||||
frontmatterKeys: keys,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemove = (id: string) => {
|
||||
updateMappings(mappings.filter((mapping) => mapping.id !== id));
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
const newMapping: StatusFrontmatterMapping = {
|
||||
id: generateId(),
|
||||
scope: "template",
|
||||
templateId: templateOptions[0]?.value || "",
|
||||
frontmatterKeys: [],
|
||||
};
|
||||
updateMappings([...mappings, newMapping]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="status-mapping-list">
|
||||
{mappings.length === 0 && (
|
||||
<div className="status-mapping-empty">
|
||||
No mappings configured yet.
|
||||
</div>
|
||||
)}
|
||||
{mappings.map((mapping) => {
|
||||
const currentKeys = mapping.frontmatterKeys || [];
|
||||
const keysValue = currentKeys.join(", ");
|
||||
const selectedStatus =
|
||||
mapping.scope === "status"
|
||||
? buildStatusIdentifier(
|
||||
mapping.templateId,
|
||||
mapping.statusName || "",
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="status-mapping-row" key={mapping.id}>
|
||||
<div className="status-mapping-row__fields">
|
||||
<div className="status-mapping-field">
|
||||
<label>Target type</label>
|
||||
<select
|
||||
value={mapping.scope}
|
||||
onChange={(event) =>
|
||||
handleScopeChange(
|
||||
mapping.id,
|
||||
event.target
|
||||
.value as StatusFrontmatterMapping["scope"],
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="template">Template</option>
|
||||
<option value="status">
|
||||
Specific status
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{mapping.scope === "template" ? (
|
||||
<div className="status-mapping-field">
|
||||
<label>Template</label>
|
||||
<select
|
||||
value={mapping.templateId || ""}
|
||||
onChange={(event) =>
|
||||
handleTemplateChange(
|
||||
mapping.id,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
{templateOptions.length === 0 && (
|
||||
<option value="">
|
||||
No templates available
|
||||
</option>
|
||||
)}
|
||||
{templateOptions.map((template) => (
|
||||
<option
|
||||
value={template.value}
|
||||
key={template.value}
|
||||
>
|
||||
{template.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="status-mapping-field">
|
||||
<label>Status</label>
|
||||
<select
|
||||
value={selectedStatus || ""}
|
||||
onChange={(event) =>
|
||||
handleStatusChange(
|
||||
mapping.id,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
{statusOptions.length === 0 && (
|
||||
<option value="">
|
||||
No statuses available
|
||||
</option>
|
||||
)}
|
||||
{statusOptions.map((option) => (
|
||||
<option
|
||||
value={option.value}
|
||||
key={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="status-mapping-field status-mapping-field--keys">
|
||||
<label>Frontmatter keys</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="status, project-status"
|
||||
value={keysValue}
|
||||
onChange={(event) =>
|
||||
handleKeysChange(
|
||||
mapping.id,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<small>
|
||||
Separate multiple keys with commas. Include
|
||||
the default key if you still want to write
|
||||
to it.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="status-mapping-row__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="status-mapping-remove-btn"
|
||||
onClick={() => handleRemove(mapping.id)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="status-mapping-add-btn"
|
||||
onClick={handleAdd}
|
||||
>
|
||||
Add mapping
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -6,6 +6,7 @@ import {
|
|||
NoteStatusService,
|
||||
} from "@/core/noteStatusService";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { getKnownFrontmatterKeys } from "@/utils/frontmatterMappings";
|
||||
|
||||
export interface VaultStats {
|
||||
totalNotes: number;
|
||||
|
|
@ -33,7 +34,9 @@ export const useVaultStats = () => {
|
|||
const files = BaseNoteStatusService.app.vault.getFiles();
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
const statusMetadataKeys = getKnownFrontmatterKeys(
|
||||
settingsService.settings,
|
||||
);
|
||||
|
||||
let notesWithStatus = 0;
|
||||
const statusDistribution: Record<string, number> = {};
|
||||
|
|
|
|||
|
|
@ -51,4 +51,5 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
|
|||
editorToolbarButtonPosition: "right", // Default position on the right
|
||||
editorToolbarButtonDisplay: "all-notes", // Default to show button in all notes
|
||||
applyStatusRecursivelyToSubfolders: false,
|
||||
statusFrontmatterMappings: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import settingsService from "./settingsService";
|
|||
import eventBus from "./eventBus";
|
||||
import { VIEW_TYPE_EXAMPLE } from "../integrations/views/grouped-status-view";
|
||||
import { isExperimentalFeatureEnabled } from "@/utils/experimentalFeatures";
|
||||
import { GroupedStatuses, NoteStatus } from "@/types/noteStatus";
|
||||
import { getKnownFrontmatterKeys } from "@/utils/frontmatterMappings";
|
||||
|
||||
export class CommandsService {
|
||||
private plugin: Plugin;
|
||||
|
|
@ -71,23 +73,20 @@ export class CommandsService {
|
|||
// Get the current file data
|
||||
const statusService = this.createStatusService(file);
|
||||
statusService.populateStatuses();
|
||||
const statuses =
|
||||
statusService.statuses[
|
||||
settingsService.settings.tagPrefix
|
||||
] || [];
|
||||
const currentStatus = statuses.length
|
||||
? statuses?.[0].name
|
||||
: undefined;
|
||||
const flattened = this.flattenStatuses(
|
||||
statusService.statuses,
|
||||
);
|
||||
const currentStatus = flattened[0];
|
||||
let nextIndex = 0;
|
||||
if (currentStatus) {
|
||||
const currentIndex = allStatuses.findIndex(
|
||||
(s) => s.name === currentStatus,
|
||||
(s) =>
|
||||
s.name === currentStatus.name &&
|
||||
(s.templateId || null) ===
|
||||
(currentStatus.templateId || null),
|
||||
);
|
||||
if (currentIndex !== -1) {
|
||||
nextIndex =
|
||||
currentIndex === -1
|
||||
? 0
|
||||
: (currentIndex + 1) % allStatuses.length;
|
||||
nextIndex = (currentIndex + 1) % allStatuses.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,10 +213,17 @@ export class CommandsService {
|
|||
|
||||
if (!statuses || statuses.length === 0) return false;
|
||||
|
||||
const tagPrefix = settingsService.settings.tagPrefix;
|
||||
const queries = statuses.map(
|
||||
(status) => `[${tagPrefix}:"${status}"]`,
|
||||
const keys = getKnownFrontmatterKeys(
|
||||
settingsService.settings,
|
||||
);
|
||||
const queries = Array.from(
|
||||
new Set(
|
||||
statuses.flatMap((status) =>
|
||||
keys.map((key) => `[${key}:"${status}"]`),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!queries.length) return false;
|
||||
const query = queries.join(" OR ");
|
||||
|
||||
// @ts-ignore
|
||||
|
|
@ -340,12 +346,38 @@ export class CommandsService {
|
|||
});
|
||||
}
|
||||
|
||||
private flattenStatuses(
|
||||
groupedStatuses: GroupedStatuses | null | undefined,
|
||||
): NoteStatus[] {
|
||||
if (!groupedStatuses) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const flattened: NoteStatus[] = [];
|
||||
Object.values(groupedStatuses).forEach((list) => {
|
||||
list.forEach((status) => {
|
||||
const identifier = BaseNoteStatusService.formatStatusIdentifier(
|
||||
{
|
||||
templateId: status.templateId,
|
||||
name: status.name,
|
||||
},
|
||||
);
|
||||
if (!seen.has(identifier)) {
|
||||
seen.add(identifier);
|
||||
flattened.push(status);
|
||||
}
|
||||
});
|
||||
});
|
||||
return flattened;
|
||||
}
|
||||
|
||||
private getFileStatuses(file: TFile): string[] {
|
||||
const statusService = this.createStatusService(file);
|
||||
statusService.populateStatuses();
|
||||
const tagPrefix = settingsService.settings.tagPrefix;
|
||||
const statuses = statusService.statuses[tagPrefix] || [];
|
||||
return statuses.length > 0 ? statuses.map((s) => s.name) : ["unknown"];
|
||||
const flattened = this.flattenStatuses(statusService.statuses);
|
||||
return flattened.length > 0
|
||||
? flattened.map((s) => s.name)
|
||||
: ["unknown"];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import settingsService from "@/core/settingsService";
|
|||
import eventBus from "./eventBus";
|
||||
import statusStoreManager from "./statusStoreManager";
|
||||
import type { StatusStore } from "./statusStores/types";
|
||||
import {
|
||||
getKnownFrontmatterKeys,
|
||||
resolveFrontmatterKeysForStatus,
|
||||
} from "@/utils/frontmatterMappings";
|
||||
|
||||
export abstract class BaseNoteStatusService {
|
||||
static app: App;
|
||||
|
|
@ -129,6 +133,39 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
this.statusStore = statusStoreManager.getStoreForFile(file);
|
||||
}
|
||||
|
||||
private isMarkdownFile(): boolean {
|
||||
return this.file.extension === "md";
|
||||
}
|
||||
|
||||
private getKeysForReading(): string[] {
|
||||
const defaultKey = settingsService.settings.tagPrefix;
|
||||
if (!this.isMarkdownFile()) {
|
||||
return [defaultKey];
|
||||
}
|
||||
const knownKeys = getKnownFrontmatterKeys(settingsService.settings);
|
||||
return [defaultKey, ...knownKeys.filter((key) => key !== defaultKey)];
|
||||
}
|
||||
|
||||
private collectIdentifiersForFile(): string[] {
|
||||
const keysToRead = this.getKeysForReading();
|
||||
const seen = new Set<string>();
|
||||
const identifiers: string[] = [];
|
||||
|
||||
keysToRead.forEach((key) => {
|
||||
const values = this.statusStore.getStatuses(this.file, key);
|
||||
values.forEach((value) => {
|
||||
const normalized = value?.toString();
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
seen.add(normalized);
|
||||
identifiers.push(normalized);
|
||||
});
|
||||
});
|
||||
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
private shouldStoreStatusesAsArray(): boolean {
|
||||
if (settingsService.settings.useMultipleStatuses) {
|
||||
return true;
|
||||
|
|
@ -141,48 +178,104 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
return { storeAsArray: this.shouldStoreStatusesAsArray() };
|
||||
}
|
||||
|
||||
private resolveTargetKeys(
|
||||
frontmatterTagName: string,
|
||||
status: StatusIdentifier | NoteStatus,
|
||||
options?: { includeSourceKey?: boolean },
|
||||
): string[] {
|
||||
if (!this.isMarkdownFile()) {
|
||||
return [frontmatterTagName];
|
||||
}
|
||||
|
||||
let resolved = resolveFrontmatterKeysForStatus(
|
||||
status,
|
||||
settingsService.settings,
|
||||
{ isMarkdownFile: true },
|
||||
);
|
||||
|
||||
const defaultKey = settingsService.settings.tagPrefix;
|
||||
if (
|
||||
resolved.length === 1 &&
|
||||
resolved[0] === defaultKey &&
|
||||
frontmatterTagName !== defaultKey
|
||||
) {
|
||||
resolved = [frontmatterTagName];
|
||||
}
|
||||
|
||||
if (options?.includeSourceKey && frontmatterTagName) {
|
||||
resolved = [...resolved, frontmatterTagName];
|
||||
}
|
||||
|
||||
const uniqueKeys = Array.from(
|
||||
new Set(resolved.filter((key) => key && key.trim().length)),
|
||||
);
|
||||
return uniqueKeys.length ? uniqueKeys : [frontmatterTagName];
|
||||
}
|
||||
|
||||
private async runAcrossKeys(
|
||||
keys: string[],
|
||||
action: (key: string) => Promise<boolean>,
|
||||
): Promise<boolean> {
|
||||
let hasChanged = false;
|
||||
for (const key of keys) {
|
||||
const changed = await action(key);
|
||||
if (changed) {
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
private statusesAreEqual(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((value, index) => value === b[index]);
|
||||
}
|
||||
|
||||
public populateStatuses(): void {
|
||||
const STATUS_METADATA_KEYS = this.getStatusMetadataKeys();
|
||||
this.statuses = Object.fromEntries(
|
||||
STATUS_METADATA_KEYS.map((key) => [key, []]),
|
||||
);
|
||||
const defaultKey = settingsService.settings.tagPrefix;
|
||||
this.statuses = { [defaultKey]: [] };
|
||||
|
||||
if (!this.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
STATUS_METADATA_KEYS.forEach((key) => {
|
||||
const identifiers = this.statusStore.getStatuses(this.file, key);
|
||||
if (!identifiers.length) {
|
||||
return;
|
||||
}
|
||||
const identifiers = this.collectIdentifiersForFile();
|
||||
if (!identifiers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let statuses = identifiers
|
||||
.map((identifier) =>
|
||||
this.statusNameToObject(identifier.toString()),
|
||||
)
|
||||
.filter(
|
||||
(status): status is NoteStatusType => status !== undefined,
|
||||
);
|
||||
let statuses = identifiers
|
||||
.map((identifier) => this.statusNameToObject(identifier))
|
||||
.filter((status): status is NoteStatusType => status !== undefined);
|
||||
|
||||
if (
|
||||
statuses.length &&
|
||||
!settingsService.settings.useMultipleStatuses
|
||||
) {
|
||||
statuses = statuses.slice(0, 1);
|
||||
}
|
||||
this.statuses[key] = statuses;
|
||||
});
|
||||
if (statuses.length && !settingsService.settings.useMultipleStatuses) {
|
||||
statuses = statuses.slice(0, 1);
|
||||
}
|
||||
this.statuses[defaultKey] = statuses;
|
||||
}
|
||||
|
||||
async removeStatus(
|
||||
frontmatterTagName: string,
|
||||
status: NoteStatus,
|
||||
): Promise<boolean> {
|
||||
const targetKeys = this.resolveTargetKeys(frontmatterTagName, status, {
|
||||
includeSourceKey: true,
|
||||
});
|
||||
const removed = await this.runAcrossKeys(targetKeys, (key) =>
|
||||
this.removeStatusFromKey(key, status),
|
||||
);
|
||||
|
||||
if (removed) {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private async removeStatusFromKey(
|
||||
frontmatterTagName: string,
|
||||
status: NoteStatus,
|
||||
): Promise<boolean> {
|
||||
const targetIdentifier = status.templateId
|
||||
? BaseNoteStatusService.formatStatusIdentifier({
|
||||
|
|
@ -191,7 +284,7 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
})
|
||||
: status.name;
|
||||
|
||||
const removed = await this.statusStore.mutateStatuses(
|
||||
return this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
|
|
@ -219,17 +312,29 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
async clearStatus(frontmatterTagName: string): Promise<boolean> {
|
||||
const keys = new Set<string>([frontmatterTagName]);
|
||||
if (this.isMarkdownFile()) {
|
||||
this.getKeysForReading().forEach((key) => keys.add(key));
|
||||
}
|
||||
|
||||
const cleared = await this.runAcrossKeys([...keys], (key) =>
|
||||
this.clearStatusForKey(key),
|
||||
);
|
||||
if (cleared) {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
return removed;
|
||||
return cleared;
|
||||
}
|
||||
|
||||
async clearStatus(frontmatterTagName: string): Promise<boolean> {
|
||||
const cleared = await this.statusStore.mutateStatuses(
|
||||
private async clearStatusForKey(
|
||||
frontmatterTagName: string,
|
||||
): Promise<boolean> {
|
||||
return this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
|
|
@ -240,37 +345,50 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
if (cleared) {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
return cleared;
|
||||
}
|
||||
|
||||
async overrideStatuses(
|
||||
frontmatterTagName: string,
|
||||
statusIdentifiers: StatusIdentifier[],
|
||||
): Promise<boolean> {
|
||||
const formattedIdentifiers = statusIdentifiers.map((id) =>
|
||||
typeof id === "string"
|
||||
? id
|
||||
: BaseNoteStatusService.formatStatusIdentifier(id),
|
||||
);
|
||||
const statusesToStore = settingsService.settings.useMultipleStatuses
|
||||
? formattedIdentifiers
|
||||
: formattedIdentifiers.slice(0, 1);
|
||||
const limitedIdentifiers = settingsService.settings.useMultipleStatuses
|
||||
? statusIdentifiers
|
||||
: statusIdentifiers.slice(0, 1);
|
||||
|
||||
const overridden = await this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
if (this.statusesAreEqual(current, statusesToStore)) {
|
||||
return { hasChanged: false };
|
||||
if (!limitedIdentifiers.length) {
|
||||
return this.clearStatus(frontmatterTagName);
|
||||
}
|
||||
|
||||
const keyMap = new Map<string, StatusIdentifier[]>();
|
||||
if (!this.isMarkdownFile()) {
|
||||
keyMap.set(frontmatterTagName, limitedIdentifiers);
|
||||
} else {
|
||||
limitedIdentifiers.forEach((identifier) => {
|
||||
const keys = this.resolveTargetKeys(
|
||||
frontmatterTagName,
|
||||
identifier,
|
||||
);
|
||||
keys.forEach((key) => {
|
||||
const currentList = keyMap.get(key) ?? [];
|
||||
currentList.push(identifier);
|
||||
keyMap.set(key, currentList);
|
||||
});
|
||||
});
|
||||
|
||||
const knownKeys = new Set([
|
||||
frontmatterTagName,
|
||||
...this.getStatusMetadataKeys(),
|
||||
]);
|
||||
knownKeys.forEach((key) => {
|
||||
if (!keyMap.has(key)) {
|
||||
keyMap.set(key, []);
|
||||
}
|
||||
return { hasChanged: true, nextStatuses: statusesToStore };
|
||||
},
|
||||
this.getStoreOptions(),
|
||||
});
|
||||
}
|
||||
|
||||
const overridden = await this.runAcrossKeys(
|
||||
Array.from(keyMap.keys()),
|
||||
(key) => this.overrideStatusesForKey(key, keyMap.get(key) ?? []),
|
||||
);
|
||||
|
||||
if (overridden) {
|
||||
|
|
@ -281,9 +399,55 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
return overridden;
|
||||
}
|
||||
|
||||
private async overrideStatusesForKey(
|
||||
frontmatterTagName: string,
|
||||
statusIdentifiers: StatusIdentifier[],
|
||||
): Promise<boolean> {
|
||||
const formattedIdentifiers = statusIdentifiers.map((id) =>
|
||||
typeof id === "string"
|
||||
? id
|
||||
: BaseNoteStatusService.formatStatusIdentifier(id),
|
||||
);
|
||||
|
||||
return this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
if (this.statusesAreEqual(current, formattedIdentifiers)) {
|
||||
return { hasChanged: false };
|
||||
}
|
||||
return {
|
||||
hasChanged: true,
|
||||
nextStatuses: formattedIdentifiers,
|
||||
};
|
||||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
}
|
||||
|
||||
async addStatus(
|
||||
frontmatterTagName: string,
|
||||
statusIdentifier: StatusIdentifier,
|
||||
): Promise<boolean> {
|
||||
const targetKeys = this.resolveTargetKeys(
|
||||
frontmatterTagName,
|
||||
statusIdentifier,
|
||||
);
|
||||
const added = await this.runAcrossKeys(targetKeys, (key) =>
|
||||
this.addStatusToKey(key, statusIdentifier),
|
||||
);
|
||||
|
||||
if (added) {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
private async addStatusToKey(
|
||||
frontmatterTagName: string,
|
||||
statusIdentifier: StatusIdentifier,
|
||||
): Promise<boolean> {
|
||||
const formattedIdentifier =
|
||||
typeof statusIdentifier === "string"
|
||||
|
|
@ -292,7 +456,7 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
statusIdentifier,
|
||||
);
|
||||
|
||||
const added = await this.statusStore.mutateStatuses(
|
||||
return this.statusStore.mutateStatuses(
|
||||
this.file,
|
||||
frontmatterTagName,
|
||||
(current) => {
|
||||
|
|
@ -334,13 +498,6 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
},
|
||||
this.getStoreOptions(),
|
||||
);
|
||||
|
||||
if (added) {
|
||||
eventBus.publish("status-changed", {
|
||||
file: this.file,
|
||||
});
|
||||
}
|
||||
return added;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -356,14 +513,20 @@ export class MultipleNoteStatusService extends BaseNoteStatusService {
|
|||
return this.files.length;
|
||||
}
|
||||
|
||||
private getKeysForReading(file: TFile): string[] {
|
||||
const defaultKey = settingsService.settings.tagPrefix;
|
||||
if (file.extension !== "md") {
|
||||
return [defaultKey];
|
||||
}
|
||||
const knownKeys = getKnownFrontmatterKeys(settingsService.settings);
|
||||
return [defaultKey, ...knownKeys.filter((key) => key !== defaultKey)];
|
||||
}
|
||||
|
||||
public populateStatuses(): void {
|
||||
const STATUS_METADATA_KEYS = this.getStatusMetadataKeys();
|
||||
const defaultKey = settingsService.settings.tagPrefix;
|
||||
this.statuses = { [defaultKey]: [] };
|
||||
|
||||
this.statuses = Object.fromEntries(
|
||||
STATUS_METADATA_KEYS.map((key) => [key, []]),
|
||||
);
|
||||
|
||||
const allStatuses = new Map<string, Set<string>>();
|
||||
const aggregated = new Set<string>();
|
||||
|
||||
this.files.forEach((file) => {
|
||||
let store: StatusStore;
|
||||
|
|
@ -373,30 +536,27 @@ export class MultipleNoteStatusService extends BaseNoteStatusService {
|
|||
return;
|
||||
}
|
||||
|
||||
STATUS_METADATA_KEYS.forEach((key) => {
|
||||
const keys = this.getKeysForReading(file);
|
||||
keys.forEach((key) => {
|
||||
const identifiers = store.getStatuses(file, key);
|
||||
if (!identifiers.length) return;
|
||||
|
||||
if (!allStatuses.has(key)) {
|
||||
allStatuses.set(key, new Set());
|
||||
}
|
||||
|
||||
identifiers.forEach((name) =>
|
||||
allStatuses.get(key)!.add(name.toString()),
|
||||
);
|
||||
identifiers.forEach((identifier) => {
|
||||
const normalized = identifier?.toString();
|
||||
if (normalized && !aggregated.has(normalized)) {
|
||||
aggregated.add(normalized);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
STATUS_METADATA_KEYS.forEach((key) => {
|
||||
const statusNames = allStatuses.get(key);
|
||||
if (statusNames) {
|
||||
const statuses = Array.from(statusNames)
|
||||
.map((name) => this.statusNameToObject(name))
|
||||
.filter((s) => s !== undefined) as NoteStatusType[];
|
||||
if (!aggregated.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.statuses[key] = statuses;
|
||||
}
|
||||
});
|
||||
const statuses = Array.from(aggregated)
|
||||
.map((identifier) => this.statusNameToObject(identifier))
|
||||
.filter((status): status is NoteStatusType => status !== undefined);
|
||||
|
||||
this.statuses[defaultKey] = statuses;
|
||||
}
|
||||
|
||||
async removeStatus(
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
key === "customStatuses" ||
|
||||
key === "useMultipleStatuses" ||
|
||||
key === "tagPrefix" ||
|
||||
key === "statusFrontmatterMappings" ||
|
||||
key === "strictStatuses" ||
|
||||
key === "fileExplorerIconPosition" ||
|
||||
key === "fileExplorerIconFrame" ||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ export class StatusBarIntegration {
|
|||
if (key === "tagPrefix") {
|
||||
this.handleActiveFileChange().catch(console.error); // INFO: Force a re-read of the statuses and render
|
||||
}
|
||||
if (key === "statusFrontmatterMappings") {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
}
|
||||
if (key === "useCustomStatusesOnly") {
|
||||
this.handleActiveFileChange().catch(console.error); // INFO: Force a re-read of the statuses and render
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ export class EditorToolbarIntegration {
|
|||
if (key === "tagPrefix") {
|
||||
this.refreshAllButtons();
|
||||
}
|
||||
if (key === "statusFrontmatterMappings") {
|
||||
this.refreshAllButtons();
|
||||
}
|
||||
if (key === "useCustomStatusesOnly") {
|
||||
this.refreshAllButtons();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import eventBus from "@/core/eventBus";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { getKnownFrontmatterKeys } from "@/utils/frontmatterMappings";
|
||||
|
||||
export const VIEW_TYPE_GROUPED_DASHBOARD = "grouped-dashboard-view";
|
||||
|
||||
|
|
@ -71,7 +72,9 @@ export class GroupedDashboardView extends ItemView {
|
|||
|
||||
private processFiles = (files: FileItem[]): GroupedByStatus => {
|
||||
const result: GroupedByStatus = {};
|
||||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
const statusMetadataKeys = getKnownFrontmatterKeys(
|
||||
settingsService.settings,
|
||||
);
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
|
||||
|
|
@ -147,6 +150,7 @@ export class GroupedDashboardView extends ItemView {
|
|||
key === "useCustomStatusesOnly" ||
|
||||
key === "customStatuses" ||
|
||||
key === "useMultipleStatuses" ||
|
||||
key === "statusFrontmatterMappings" ||
|
||||
key === "strictStatuses"
|
||||
) {
|
||||
onDataChange();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import eventBus from "@/core/eventBus";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { getKnownFrontmatterKeys } from "@/utils/frontmatterMappings";
|
||||
|
||||
export const VIEW_TYPE_EXAMPLE = "grouped-status-view";
|
||||
|
||||
|
|
@ -55,7 +56,9 @@ export class GroupedStatusView extends ItemView {
|
|||
|
||||
private processFiles = (files: FileItem[]): GroupedByStatus => {
|
||||
const result: GroupedByStatus = {};
|
||||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
const statusMetadataKeys = getKnownFrontmatterKeys(
|
||||
settingsService.settings,
|
||||
);
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
|
||||
|
|
@ -131,6 +134,7 @@ export class GroupedStatusView extends ItemView {
|
|||
key === "useCustomStatusesOnly" ||
|
||||
key === "customStatuses" ||
|
||||
key === "useMultipleStatuses" ||
|
||||
key === "statusFrontmatterMappings" ||
|
||||
key === "strictStatuses" ||
|
||||
key === "vaultSizeLimit"
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import settingsService from "@/core/settingsService";
|
|||
import eventBus from "@/core/eventBus";
|
||||
import { VaultStats } from "@/components/StatusDashboard/useVaultStats";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { getKnownFrontmatterKeys } from "@/utils/frontmatterMappings";
|
||||
|
||||
interface AppWithCommands extends App {
|
||||
commands: {
|
||||
|
|
@ -61,7 +62,9 @@ export class StatusDashboardView extends ItemView {
|
|||
const files = this.app.vault.getFiles();
|
||||
const availableStatuses =
|
||||
BaseNoteStatusService.getAllAvailableStatuses();
|
||||
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
||||
const statusMetadataKeys = getKnownFrontmatterKeys(
|
||||
settingsService.settings,
|
||||
);
|
||||
|
||||
let notesWithStatus = 0;
|
||||
const statusDistribution: Record<string, number> = {};
|
||||
|
|
@ -224,12 +227,19 @@ export class StatusDashboardView extends ItemView {
|
|||
|
||||
private findUnassignedNotes() {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
const statusKeys = getKnownFrontmatterKeys(settingsService.settings);
|
||||
const filesWithoutStatus = files.filter((file) => {
|
||||
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
return (
|
||||
!frontmatter || !frontmatter[settingsService.settings.tagPrefix]
|
||||
);
|
||||
if (!frontmatter) return true;
|
||||
|
||||
return !statusKeys.some((key) => {
|
||||
const value = frontmatter[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0;
|
||||
}
|
||||
return Boolean(value);
|
||||
});
|
||||
});
|
||||
|
||||
if (filesWithoutStatus.length === 0) {
|
||||
|
|
@ -242,8 +252,7 @@ export class StatusDashboardView extends ItemView {
|
|||
);
|
||||
|
||||
// Create a search query to find files without the status tag
|
||||
const tagPrefix = settingsService.settings.tagPrefix;
|
||||
const query = `-[${tagPrefix}:]`;
|
||||
const query = statusKeys.map((key) => `-[${key}:]`).join(" ");
|
||||
|
||||
// @ts-ignore
|
||||
this.app.internalPlugins.plugins[
|
||||
|
|
@ -252,8 +261,10 @@ export class StatusDashboardView extends ItemView {
|
|||
}
|
||||
|
||||
private searchBySpecificStatus(statusName: string) {
|
||||
const tagPrefix = settingsService.settings.tagPrefix;
|
||||
const query = `[${tagPrefix}:"${statusName}"]`;
|
||||
const keys = getKnownFrontmatterKeys(settingsService.settings);
|
||||
const query = keys
|
||||
.map((key) => `[${key}:"${statusName}"]`)
|
||||
.join(" OR ");
|
||||
|
||||
// @ts-ignore
|
||||
this.app.internalPlugins.plugins[
|
||||
|
|
@ -325,6 +336,7 @@ export class StatusDashboardView extends ItemView {
|
|||
key === "customStatuses" ||
|
||||
key === "useMultipleStatuses" ||
|
||||
key === "strictStatuses" ||
|
||||
key === "statusFrontmatterMappings" ||
|
||||
key === "vaultSizeLimit"
|
||||
) {
|
||||
handleVaultChange();
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -350,6 +350,78 @@
|
|||
border-radius: var(--radius-s);
|
||||
background: var(--background-primary);
|
||||
}
|
||||
.status-mapping-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-2);
|
||||
width: 100%;
|
||||
}
|
||||
.status-mapping-row {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--size-4-3);
|
||||
background: var(--background-primary-alt);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
.status-mapping-empty {
|
||||
padding: var(--size-4-3);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
border: 1px dashed var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
.status-mapping-row__fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-4-2);
|
||||
width: 100%;
|
||||
}
|
||||
.status-mapping-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
.status-mapping-field label {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.status-mapping-field small {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
.status-mapping-field select,
|
||||
.status-mapping-field input {
|
||||
width: 100%;
|
||||
}
|
||||
.status-mapping-row__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.status-mapping-remove-btn {
|
||||
background: var(--background-modifier-error);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-mapping-remove-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.status-mapping-add-btn {
|
||||
align-self: flex-start;
|
||||
padding: var(--size-2-2) var(--size-4-2);
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-mapping-add-btn:hover {
|
||||
background: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.note-status-option {
|
||||
/* Uses SelectableListItem styles */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
import { NoteStatus } from "./noteStatus";
|
||||
|
||||
export type StatusFrontmatterMapping =
|
||||
| {
|
||||
id: string;
|
||||
scope: "template";
|
||||
templateId: string;
|
||||
frontmatterKeys: string[];
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
scope: "status";
|
||||
templateId?: string;
|
||||
statusName: string;
|
||||
frontmatterKeys: string[];
|
||||
};
|
||||
|
||||
export interface StatusTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -51,5 +66,6 @@ export type PluginSettings = {
|
|||
editorToolbarButtonPosition: "left" | "right" | "right-before"; // Position of the toolbar button
|
||||
editorToolbarButtonDisplay: "all-notes" | "active-only"; // Whether to show button in all notes or only active one
|
||||
applyStatusRecursivelyToSubfolders: boolean; // Whether to show recursive folder context option
|
||||
statusFrontmatterMappings: StatusFrontmatterMapping[]; // Custom mappings between templates/statuses and frontmatter keys
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
|
|
|||
111
utils/frontmatterMappings.ts
Normal file
111
utils/frontmatterMappings.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
PluginSettings,
|
||||
StatusFrontmatterMapping,
|
||||
} from "@/types/pluginSettings";
|
||||
import {
|
||||
NoteStatus,
|
||||
ScopedStatusName,
|
||||
StatusIdentifier,
|
||||
} from "@/types/noteStatus";
|
||||
|
||||
type StatusLike = NoteStatus | ScopedStatusName | StatusIdentifier;
|
||||
|
||||
const normalizeKeys = (keys: string[]): string[] => {
|
||||
const unique = new Set(
|
||||
keys
|
||||
.map((key) => key?.trim())
|
||||
.filter((key): key is string => Boolean(key)),
|
||||
);
|
||||
return Array.from(unique);
|
||||
};
|
||||
|
||||
const toScopedStatusName = (status: StatusLike): ScopedStatusName => {
|
||||
if (typeof status === "string") {
|
||||
if (status.includes(":")) {
|
||||
const [templateId, name] = status.split(":", 2);
|
||||
return { templateId, name };
|
||||
}
|
||||
return { name: status };
|
||||
}
|
||||
|
||||
if ("name" in status) {
|
||||
return {
|
||||
name: status.name,
|
||||
templateId: "templateId" in status ? status.templateId : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
const matchesStatusMapping = (
|
||||
mapping: StatusFrontmatterMapping,
|
||||
status: ScopedStatusName,
|
||||
) => {
|
||||
if (mapping.scope === "status") {
|
||||
const templateMatches =
|
||||
(mapping.templateId || null) === (status.templateId || null);
|
||||
return templateMatches && mapping.statusName === status.name;
|
||||
}
|
||||
|
||||
if (mapping.scope === "template") {
|
||||
return Boolean(status.templateId) &&
|
||||
status.templateId === mapping.templateId
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getMappingKeys = (mapping: StatusFrontmatterMapping): string[] => {
|
||||
return normalizeKeys(mapping.frontmatterKeys || []);
|
||||
};
|
||||
|
||||
export const getKnownFrontmatterKeys = (settings: PluginSettings): string[] => {
|
||||
const keys = [
|
||||
settings.tagPrefix,
|
||||
...(settings.statusFrontmatterMappings ?? []).flatMap((mapping) =>
|
||||
getMappingKeys(mapping),
|
||||
),
|
||||
];
|
||||
return normalizeKeys(keys);
|
||||
};
|
||||
|
||||
export const resolveFrontmatterKeysForStatus = (
|
||||
status: StatusLike,
|
||||
settings: PluginSettings,
|
||||
options?: { isMarkdownFile?: boolean },
|
||||
): string[] => {
|
||||
if (options?.isMarkdownFile === false) {
|
||||
return [settings.tagPrefix];
|
||||
}
|
||||
|
||||
const scoped = toScopedStatusName(status);
|
||||
const mappings = settings.statusFrontmatterMappings ?? [];
|
||||
|
||||
const statusMapping = mappings.find(
|
||||
(mapping) =>
|
||||
mapping.scope === "status" && matchesStatusMapping(mapping, scoped),
|
||||
);
|
||||
if (statusMapping) {
|
||||
const keys = getMappingKeys(statusMapping);
|
||||
if (keys.length) {
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
const templateMapping = mappings.find(
|
||||
(mapping) =>
|
||||
mapping.scope === "template" &&
|
||||
matchesStatusMapping(mapping, scoped),
|
||||
);
|
||||
if (templateMapping) {
|
||||
const keys = getMappingKeys(templateMapping);
|
||||
if (keys.length) {
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
return [settings.tagPrefix];
|
||||
};
|
||||
Loading…
Reference in a new issue