Skip to content

Commit

Permalink
Improvement/create new metadata modal (#22)
Browse files Browse the repository at this point in the history
* Enhanced metadata file creation logic

Refined `evaluateForm` method to support editing existing metadata files or creating new ones. Introduced logic to handle file linkage, and to populate new DocumentModel instances with form result data. Now, if an existing file is passed, it's updated with the synchronized metadata; otherwise, a new file is created using a specified template if available.

* Renamed and updated file link methods

Refactored `DocumentModel` methods for clarity and added return value to `setLinkedFile`. The `getFile` method is renamed to `getLinkedFile` to better represent its functionality. The `setLinkedFile` method now returns the constructed wikilink string for potential further use, while previously it returned nothing. Calls to the old `getFile` have been updated accordingly in `StaticDocumentModel` to maintain consistency.

* Enhanced data setting logic in FileModel

Refined the setter logic for `_data` to preserve existing values when an `undefined` is passed, and to explicitly clear them when a `null` is provided. This change improves data handling by ensuring that `undefined` values do not overwrite current data, allowing for more precise updates to the FileModel’s state.

* Refactor transaction handling and model initialization

Refactored the FileModel and TransactionModel classes to streamline the transaction handling process. In FileModel, simplified setting of the file property and consolidated the creation of an empty object with default values directly into the transaction changes object, removing the need for iteratively updating key-value pairs.

Enhanced the TransactionModel by immediately starting a transaction if the `writeChanges` callback is not provided during instantiation, emphasizing the intent to always be in a transactional state. Further, introduced a clear separation of concerns by adding methods to properly finish or abort ongoing transactions based on the presence or emptiness of changes.

Improved error handling in the asynchronous `writeChanges` callback, now catching errors and logging them properly, as well as clarifying the function's execution flow with a boolean return value. This adjustment allows for better debugging and maintenance.

These enhancements aim to improve code clarity, reduce complexity, and enforce a more predictable and reliable transaction flow throughout the system.

* Ensure cache consistency on file update

Added a call to invalidateMetadataCacheArray after updating a file entry in MetadataCache to ensure that the cache remains consistent. This change addresses potential stale data when the cache is accessed following a file update.

Version bump to V0.0.33
  • Loading branch information
PxaMMaxP authored Jan 17, 2024
1 parent aaa17d8 commit 07cf1b2
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 74 deletions.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "prj",
"name": "Prj Plugin",
"version": "0.0.32",
"version": "0.0.33",
"minAppVersion": "0.15.0",
"description": "Prj Plugin - Project, Document, and Task Management",
"author": "M. Passarello",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-sample-plugin",
"version": "0.0.32",
"version": "0.0.33",
"description": "Prj Plugin - Project, Document, and Task Management",
"main": "main.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions src/libs/MetadataCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export default class MetadataCache {
if (this.metadataCache) {
this.metadataCache.delete(oldPath);
this.addEntry(newFile);
this.invalidateMetadataCacheArray();
} else {
this.logger.error("Metadata cache not initialized");
}
Expand Down
59 changes: 31 additions & 28 deletions src/libs/Modals/CreateNewMetadataModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import DocumentData from "src/types/DocumentData";
import { Field, FormConfiguration, IFormResult, IResultData } from "src/types/ModalFormType";
import BaseModalForm from "./BaseModalForm";
import Helper from "../Helper";
import PrjTypes from "src/types/PrjTypes";
import PrjTypes, { FileSubType } from "src/types/PrjTypes";
import API from "src/classes/API";
import { TFile } from "obsidian";

/**
* Modal to create a new metadata file
Expand Down Expand Up @@ -78,42 +79,44 @@ export default class CreateNewMetadataModal extends BaseModalForm {
* Evaluates the form result and creates a new metadata file
* @param {IFormResult} result Result of the form
* @returns {Promise<DocumentModel | undefined>} The created metadata file
* @remarks - This function checks if the API is available.
* - This function checks if the form result is valid,
* - checks if the file exists and is a PDF file,
* - fills the metadata file with the form data,
* - creates the metadata file and
* - returns the metadata file if created successfully else undefined.
* @remarks 1. Creates a new Document model with the give file or no file.
* 2. Sets the data of the document model to the form result.
* 3. Creates a new file with the metadata filename or uses the existing file and rename it.
*/
public async evaluateForm(result: IFormResult): Promise<DocumentModel | undefined> {
public async evaluateForm(result: IFormResult, existingFile?: TFile): Promise<DocumentModel | undefined> {
if (!this.isApiAvailable()) return;
if (result.status !== "ok" || !result.data) return;

const folder = this.settings.documentSettings.defaultFolder;
const document = new DocumentModel(existingFile ? existingFile : undefined);

const document = new DocumentModel(undefined);
document.data.type = "Metadata";
const folder = existingFile?.parent?.path ? existingFile.parent?.path : this.settings.documentSettings.defaultFolder;

document.data.subType = PrjTypes.isValidFileSubType(result.data.subType);
(result.data.subType as FileSubType | undefined) = PrjTypes.isValidFileSubType(result.data.subType);
const linkedFile = this.fileCache.findFileByLinkText(result.data.file as string, "");
(result.data.file as string | undefined) = result.data.file ? document.setLinkedFile(linkedFile, folder) : undefined;

if (!document.data.subType && result.data.file) {
const linkedFile = this.fileCache.findFileByLinkText(result.data.file as string, "");
document.setLinkedFile(linkedFile, folder);
}

document.data.date = (result.data.date as string) ?? undefined;
document.data.dateOfDelivery = (result.data.dateOfDelivery as string) ?? undefined;
document.data.title = result.data.title as string ?? undefined;
document.data.description = result.data.description as string ?? undefined;
document.data.sender = result.data.sender as string ?? undefined;
document.data.recipient = result.data.recipient as string ?? undefined;
document.data.hide = result.data.hide as boolean ?? undefined;
document.data.tags = result.data.tags as string[] ?? undefined;
document.data = result.data as Partial<DocumentData>;

const newFileName = API.documentModel.generateMetadataFilename(document);
if (!existingFile) {
// No existing file, create a new one
let template = "";
// If a template is set, use it
const templateFile = this.app.vault.getAbstractFileByPath(this.settings.documentSettings.template);
if (templateFile && templateFile instanceof TFile) {
try {
template = await this.app.vault.read(templateFile);
} catch (error) {
this.logger.error(`Error reading template file '${templateFile.path}'`, error);
}
}

const file = await this.app.vault.create(path.join(folder, `${newFileName}.md`), ``);
document.file = file;
const newFileName = API.documentModel.generateMetadataFilename(document);
const file = await this.app.vault.create(path.join(folder, `${newFileName}.md`), template);
document.file = file;
} else {
// Existing file, rename it properly
await API.documentModel.syncMetadataToFile(document.file);
}

return document;
}
Expand Down
8 changes: 5 additions & 3 deletions src/models/DocumentModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class DocumentModel extends FileModel<DocumentData> implements IPrjModel<
* Returns the linked file of the document
* @returns TFile of the linked file
*/
public getFile(): TFile | undefined {
public getLinkedFile(): TFile | undefined {
const fileLinkData = Helper.extractDataFromWikilink(this.data.file);
const file = this.fileCache.findFileByLinkText(fileLinkData.filename ?? "", this.file.path);
if (file instanceof TFile) {
Expand All @@ -158,10 +158,12 @@ export class DocumentModel extends FileModel<DocumentData> implements IPrjModel<
* @remarks - This function sets the `file` property of the document to the wikilink of the file.
* - If no file is provided, the function will return.
*/
public setLinkedFile(file: TFile | undefined, path?: string): void {
public setLinkedFile(file: TFile | undefined, path?: string): string | undefined {
if (!file || !(file instanceof TFile)) return;
const linktext = this.global.app.metadataCache.fileToLinktext(file, path ? path : this.file.path);
this.data.file = `[[${linktext}]]`;
const link = `[[${linktext}]]`;
this.data.file = link;
return link;
}

/**
Expand Down
19 changes: 9 additions & 10 deletions src/models/FileModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,7 @@ export class FileModel<T extends object> extends TransactionModel<T> {
constructor(file: TFile | undefined, ctor: new (data?: Partial<T>) => T, yamlKeyMap: YamlKeyMap | undefined) {
super(undefined);
if (file) {
super.setWriteChanges((update) => {
return this.setFrontmatter(update as Record<string, unknown>);
});
this._file = file;
this.file = file;
}
this.ctor = ctor;
this.initYamlKeyMap(yamlKeyMap);
Expand All @@ -95,10 +92,8 @@ export class FileModel<T extends object> extends TransactionModel<T> {
if (!frontmatter) {
this.logger.trace('Creating empty object');
const emptyObject = new this.ctor();
// Save the properties of the empty object to changes in transaction
for (const key in emptyObject) {
this.updateKeyValue(key, emptyObject[key]);
}
// Save the default values to the changes object in `TransactionModel`
this.changes = emptyObject;
this.dataProxy = this.createProxy(emptyObject) as T;
return this.dataProxy;
}
Expand All @@ -121,12 +116,16 @@ export class FileModel<T extends object> extends TransactionModel<T> {
/**
* Sets the data object.
* @param values The values to set.
* @remarks Overwrites only the given values.
* @remarks Overwrites only the given values:
* - If value is `undefined`, the value is not overwritten.
* - If value is `null`, the value is cleared.
*/
protected set _data(values: Partial<T>) {
const dataObject: T = new this.ctor(values);
for (const key in dataObject) {
this._data[key] = values[key];
if (values[key] !== undefined) {
this._data[key] = values[key];
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/models/StaticHelper/StaticDocumentModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class StaticDocumentModel {
const fileCache = Global.getInstance().fileCache.Cache;
const setOfPDFsWithMetadata = new Set(metadataCache
.filter(file => file.metadata?.frontmatter?.type === "Metadata" && file.metadata?.frontmatter?.file)
.map(file => new DocumentModel(file.file).getFile())
.map(file => new DocumentModel(file.file).getLinkedFile())
.filter(file => file !== undefined && file.extension === "pdf"));
const listOfPDFWithoutMetadata = fileCache.filter(file => file.extension === "pdf" && !setOfPDFsWithMetadata.has(file));
return listOfPDFWithoutMetadata;
Expand Down Expand Up @@ -144,7 +144,7 @@ export class StaticDocumentModel {
}

// PDF file
const pdfFile = document.getFile();
const pdfFile = document.getLinkedFile();
if (!pdfFile) return;

const documentDate = document.data.date ? new Date(document.data.date) : undefined;
Expand Down
60 changes: 32 additions & 28 deletions src/models/TransactionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,47 @@ export class TransactionModel<T> {
/**
* Creates a new instance of the TransactionModel class.
* @param writeChanges A function that writes the changes to the file.
* @remarks - If no `writeChanges` function is provided, a transaction is started immediately.
*/
constructor(writeChanges: ((update: T) => Promise<void>) | undefined) {
if (writeChanges) {
this.transactionActive = false;
this.writeChanges = writeChanges;
} else {
this.startTransaction();
}
this.transactionActive = true;
}


/**
* Sets the callback function for writing changes to the transaction model.
* @param writeChanges The callback function that takes an update of type T and returns a Promise that resolves when the changes are written.
* @remarks - If a transaction is active, the transaction is finished after the callback function is set and the `changes` property is not empty.
* - Else, if the transaction is active and the `changes` property is empty, the transaction is aborted.
*/
public setWriteChanges(writeChanges: (update: T) => Promise<void>) {
this.writeChanges = writeChanges;
if (this.isTransactionActive && !(Object.keys(this.changes).length === 0 && this.changes.constructor === Object)) {
this.callWriteChanges();
this.changes = {};
this.transactionActive = false;
this.finishTransaction();
} else if (this.isTransactionActive) {
this.abortTransaction();
}
this.transactionActive = false;
}

protected callWriteChanges(update: T = this.changes as T) {
/**
* Calls the `writeChanges` function if it is available.
* @param update The changes to write.
* @returns `true` if the `writeChanges` function is available, otherwise `false`.
* @remarks - If the `writeChanges` function is available, it will be called asynchronously (No waiting for the function to finish).
*/
protected callWriteChanges(update: T = this.changes as T): boolean {
if (this.writeChanges) {
this.writeChanges(update);
this.writeChanges(update)
.then(() => { this.logger.debug("Changes written to file"); })
.catch((error) => { this.logger.error("Failed to write changes to file:", error); })
return true;
} else {
this.logger.debug("No writeChanges function available");
return false;
}
}

Expand All @@ -69,26 +86,16 @@ export class TransactionModel<T> {
* Finishes a transaction
* @remarks - If no transaction is active, this method does nothing and logs a warning.
* - This method writes the changes to the file.
* @remarks - If the `writeChanges` method throws an error, the error is logged and the transaction is aborted.
* @remarks - If the `writeChanges` method is not available, this method does nothing.
*/
public async finishTransaction(): Promise<void> {
public finishTransaction(): void {
if (!this.isTransactionActive) {
this.logger.warn('No transaction active');
return;
} else if (!this.writeChanges) {
this.logger.info('No writeChanges function available');
return;
}
try {
this.callWriteChanges();
}
catch (error) {
this.logger.error("`writeChanges` failed with error:", error);
}
finally {
this.changes = {};
this.transactionActive = false;
}
const writeChanges = this.callWriteChanges();
this.changes = writeChanges ? {} : this.changes;
this.transactionActive = writeChanges ? false : this.transactionActive;
}

/**
Expand All @@ -100,9 +107,6 @@ export class TransactionModel<T> {
if (!this.isTransactionActive) {
this.logger.warn('No transaction active');
return;
} else if (!this.writeChanges) {
this.logger.info('No writeChanges function available');
return;
}
this.changes = {};
this.transactionActive = false;
Expand All @@ -127,8 +131,8 @@ export class TransactionModel<T> {
});

if (!this.isTransactionActive) {
this.callWriteChanges();
this.changes = {};
const writeChanges = this.callWriteChanges();
this.changes = writeChanges ? {} : this.changes;
}
}

Expand Down
3 changes: 2 additions & 1 deletion versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@
"0.0.29": "0.15.0",
"0.0.30": "0.15.0",
"0.0.31": "0.15.0",
"0.0.32": "0.15.0"
"0.0.32": "0.15.0",
"0.0.33": "0.15.0"
}

0 comments on commit 07cf1b2

Please sign in to comment.