feat: replace prettier with oxfmt

This commit is contained in:
Jacobtread 2026-03-26 16:05:06 +13:00
parent fbf3e46daa
commit a1fcc3ff21
67 changed files with 3915 additions and 5133 deletions

View file

@ -1,27 +1,32 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: 'Bug: ' title: "Bug: "
labels: bug labels: bug
assignees: jacobtread assignees: jacobtread
--- ---
**Describe the bug** **Describe the bug**
<!-- A clear and concise description of what the bug is. --> <!-- A clear and concise description of what the bug is. -->
**To Reproduce** **To Reproduce**
<!-- Steps to reproduce the behavior: --> <!-- Steps to reproduce the behavior: -->
1.
1.
2. 2.
3. 3.
4. 4.
**Expected behavior** **Expected behavior**
<!-- A clear and concise description of what you expected to happen. --> <!-- A clear and concise description of what you expected to happen. -->
**Screenshots** **Screenshots**
<!-- If applicable, add screenshots to help explain your problem. --> <!-- If applicable, add screenshots to help explain your problem. -->
**Additional context** **Additional context**
<!-- Add any other context about the problem here. --> <!-- Add any other context about the problem here. -->

View file

@ -1,20 +1,23 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: 'Feature:' title: "Feature:"
labels: enhancement labels: enhancement
assignees: jacobtread assignees: jacobtread
--- ---
## Is your feature request related to a problem? Please describe. ## Is your feature request related to a problem? Please describe.
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] --> <!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
## Describe the solution you'd like ## Describe the solution you'd like
<!-- A clear and concise description of what you want to happen. -->
<!-- A clear and concise description of what you want to happen. -->
## Describe alternatives you've considered ## Describe alternatives you've considered
<!-- A clear and concise description of any alternative solutions or features you've considered. --> <!-- A clear and concise description of any alternative solutions or features you've considered. -->
## Additional context ## Additional context
<!-- Add any other context or screenshots about the feature request here. --> <!-- Add any other context or screenshots about the feature request here. -->

View file

@ -1,10 +1,9 @@
--- ---
name: Task name: Task
about: Template for a task that needs to be done about: Template for a task that needs to be done
title: 'Task: ' title: "Task: "
labels: enhancement labels: enhancement
assignees: jacobtread assignees: jacobtread
--- ---
## Description ## Description

View file

@ -1,9 +1,11 @@
## Description ## Description
## Changes ## Changes
-
-
## Related Issues ## Related Issues
-
-
## Screenshots (If applicable) ## Screenshots (If applicable)

5
.gitignore vendored
View file

@ -1,5 +1,4 @@
# vscode
.vscode
# Intellij # Intellij
*.iml *.iml
@ -25,4 +24,4 @@ dist
coverage coverage
test-vault/.obsidian test-vault/.obsidian

9
.oxfmtrc.json Normal file
View file

@ -0,0 +1,9 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"tabWidth": 4,
"trailingComma": "es5",
"semi": true,
"singleQuote": false,
"bracketSameLine": true,
"ignorePatterns": ["dist/**", "*.min.js", "README.md", "types/**"]
}

View file

@ -1,8 +0,0 @@
/** @type {import("prettier").Config} */
module.exports = {
trailingComma: "es5",
tabWidth: 4,
semi: true,
singleQuote: false,
bracketSameLine: true,
};

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["oxc.oxc-vscode"]
}

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"oxc.fmt.configPath": ".oxfmtrc.json",
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "always"
}
}

10
.zed/settings.json Normal file
View file

@ -0,0 +1,10 @@
{
"prettier": {
"allowed": false
},
"formatter": {
"language_server": {
"name": "oxfmt"
}
}
}

View file

@ -14,23 +14,23 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behaviour that contributes to a positive environment for our Examples of behaviour that contributes to a positive environment for our
community include: community include:
- Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologising to those affected by our mistakes, - Accepting responsibility and apologising to those affected by our mistakes,
and learning from the experience and learning from the experience
- Focusing on what is best not just for us as individuals, but for the - Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behaviour include: Examples of unacceptable behaviour include:
- The use of sexualised language or imagery, and sexual attention or advances - The use of sexualised language or imagery, and sexual attention or advances
- Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Our Responsibilities ## Our Responsibilities

View file

@ -8,24 +8,24 @@ All types of contributions are encouraged and valued. See the [Table of Contents
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> >
> - Star the project > - Star the project
> - Tweet about it > - Tweet about it
> - Refer this project in your project's readme > - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues > - Mention the project at local meetups and tell your friends/colleagues
<!-- omit in toc --> <!-- omit in toc -->
## Table of Contents ## Table of Contents
- [Use of Generative AI](#use-of-generative-ai) - [Use of Generative AI](#use-of-generative-ai)
- [Code of Conduct](#code-of-conduct) - [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question) - [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute) - [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs) - [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements) - [Suggesting Enhancements](#suggesting-enhancements)
- [Styleguides](#styleguides) - [Styleguides](#styleguides)
- [Commit Messages](#commit-messages) - [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team) - [Join The Project Team](#join-the-project-team)
## Use of Generative AI ## Use of Generative AI
@ -48,9 +48,9 @@ Before you ask a question, it is best to search for existing [Issues](https://gi
If you then still feel the need to ask a question and need clarification, we recommend the following: If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/jacobtread/obsidian-timekeep/issues/new). - Open an [Issue](https://github.com/jacobtread/obsidian-timekeep/issues/new).
- Provide as much context as you can about what you're running into. - Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible. We will then take care of the issue as soon as possible.
@ -83,16 +83,16 @@ Depending on how large the project is, you may want to outsource the questioning
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version. - Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (If you are looking for support, you might want to check [this section](#i-have-a-question)). - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/jacobtread/obsidian-timekeep/issues?q=label%3Abug). - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/jacobtread/obsidian-timekeep/issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug: - Collect information about the bug:
- Stack trace (Traceback) - Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM) - OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output - Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions? - Can you reliably reproduce the issue? And can you also reproduce it with older versions?
<!-- omit in toc --> <!-- omit in toc -->
@ -104,16 +104,16 @@ A good bug report shouldn't leave others needing to chase you up for more inform
We use GitHub issues to track bugs and errors. If you run into an issue with the project: We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/jacobtread/obsidian-timekeep/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) - Open an [Issue](https://github.com/jacobtread/obsidian-timekeep/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior. - Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. - Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section. - Provide the information you collected in the previous section.
Once it's filed: Once it's filed:
- The project team will label the issue accordingly. - The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be implemented by someone. - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be implemented by someone.
<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. --> <!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
@ -125,9 +125,9 @@ This section guides you through submitting an enhancement suggestion for Obsidia
#### Before Submitting an Enhancement #### Before Submitting an Enhancement
- Make sure that you are using the latest version. - Make sure that you are using the latest version.
- Perform a [search](https://github.com/jacobtread/obsidian-timekeep/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. - Perform a [search](https://github.com/jacobtread/obsidian-timekeep/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
<!-- omit in toc --> <!-- omit in toc -->
@ -135,11 +135,11 @@ This section guides you through submitting an enhancement suggestion for Obsidia
Enhancement suggestions are tracked as [GitHub issues](https://github.com/jacobtread/obsidian-timekeep/issues). Enhancement suggestions are tracked as [GitHub issues](https://github.com/jacobtread/obsidian-timekeep/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion. - Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. - Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on Linux. <!-- this should only be included if the project has a GUI --> - You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most Timekeep users. You may also want to point out the other projects that solved it better and which could serve as inspiration. - **Explain why this enhancement would be useful** to most Timekeep users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. --> <!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->

View file

@ -1,14 +1,14 @@
{ {
"words": [ "words": [
"datetime", "datetime",
"Jacobtread", "Jacobtread",
"lucide", "lucide",
"Roboto", "Roboto",
"stylesheet", "stylesheet",
"timekeep", "timekeep",
"timesheet", "timesheet",
"unstarted", "unstarted",
"Timekeeps", "Timekeeps",
"pdfmake" "pdfmake"
] ]
} }

View file

@ -8,4 +8,4 @@
"authorUrl": "https://jacobtread.com", "authorUrl": "https://jacobtread.com",
"fundingUrl": "https://www.buymeacoffee.com/jacobtread", "fundingUrl": "https://www.buymeacoffee.com/jacobtread",
"isDesktopOnly": false "isDesktopOnly": false
} }

6051
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,37 @@
{ {
"name": "obsidian-timekeep", "name": "obsidian-timekeep",
"version": "1.16.0", "version": "1.16.0",
"description": "Obsidian plugin for time tracking", "description": "Obsidian plugin for time tracking",
"main": "main.js", "keywords": [],
"type": "module", "license": "MIT",
"scripts": { "author": "Jacobtread",
"dev": "node scripts/dev.js", "type": "module",
"build": "node scripts/build.js", "main": "main.js",
"version": "node scripts/version-bump.js && git add manifest.json versions.json", "scripts": {
"lint": "oxlint src", "dev": "node scripts/dev.js",
"lint:fix": "oxlint --fix src", "build": "node scripts/build.js",
"format": "prettier -w src", "version": "node scripts/version-bump.js && git add manifest.json versions.json",
"test": "vitest run --coverage" "lint": "oxlint",
}, "lint:fix": "oxlint --fix",
"keywords": [], "fmt": "oxfmt",
"author": "Jacobtread", "fmt:check": "oxfmt --check",
"license": "MIT", "test": "vitest run --coverage"
"devDependencies": { },
"@types/node": "^20.19.37", "dependencies": {
"@types/pdfmake": "^0.3.2", "moment": "^2.30.1",
"@vitest/coverage-v8": "^4.1.1", "pdfmake": "^0.3.7",
"eslint-config-prettier": "^10.1.8", "uuid": "^11.0.5",
"eslint-plugin-perfectionist": "^5.7.0", "zod": "^4.3.6"
"obsidian": "latest", },
"oxlint": "^1.57.0", "devDependencies": {
"prettier": "^3.8.1", "@types/node": "^20.19.37",
"typescript": "5.9.2", "@types/pdfmake": "^0.3.2",
"vite": "^8.0.2", "@vitest/coverage-v8": "^4.1.1",
"vitest": "^4.1.1" "obsidian": "latest",
}, "oxfmt": "^0.42.0",
"dependencies": { "oxlint": "^1.57.0",
"moment": "^2.30.1", "typescript": "5.9.2",
"pdfmake": "^0.3.7", "vite": "^8.0.2",
"uuid": "^11.0.5", "vitest": "^4.1.1"
"zod": "^4.3.6" }
}
} }

View file

@ -4,34 +4,34 @@ import { build } from "vite";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
async function buildPlugin() { async function buildPlugin() {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, "../"); const rootPath = path.resolve(__dirname, "../");
const manifestPath = path.join(rootPath, "manifest.json"); const manifestPath = path.join(rootPath, "manifest.json");
const outputPath = path.join(rootPath, "dist"); const outputPath = path.join(rootPath, "dist");
await ensureDir(outputPath); await ensureDir(outputPath);
await build({ await build({
configFile: path.resolve(rootPath, "vite.config.js"), configFile: path.resolve(rootPath, "vite.config.js"),
}); });
const destManifest = path.join(outputPath, "manifest.json"); const destManifest = path.join(outputPath, "manifest.json");
try { try {
await fs.copyFile(manifestPath, destManifest); await fs.copyFile(manifestPath, destManifest);
console.info(`Copied manifest.json`); console.info(`Copied manifest.json`);
} catch (e) { } catch (e) {
console.error(`Failed to copy manifest.json:`, e.message); console.error(`Failed to copy manifest.json:`, e.message);
} }
} }
async function ensureDir(dir) { async function ensureDir(dir) {
try { try {
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
} catch {} } catch {}
} }
buildPlugin().catch((err) => { buildPlugin().catch((err) => {
console.error(err); console.error(err);
process.exit(1); process.exit(1);
}); });

View file

@ -4,74 +4,66 @@ import { build } from "vite";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
async function dev() { async function dev() {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, "../"); const rootPath = path.resolve(__dirname, "../");
const manifestPath = path.join(rootPath, "manifest.json"); const manifestPath = path.join(rootPath, "manifest.json");
const outputPath = path.join(rootPath, "dist"); const outputPath = path.join(rootPath, "dist");
const manifestContents = await fs.readFile(manifestPath, "utf-8"); const manifestContents = await fs.readFile(manifestPath, "utf-8");
const manifest = JSON.parse(manifestContents); const manifest = JSON.parse(manifestContents);
const vaultPath = path.join(rootPath, "test-vault"); const vaultPath = path.join(rootPath, "test-vault");
const pluginPath = path.join( const pluginPath = path.join(vaultPath, ".obsidian", "plugins", manifest.id);
vaultPath,
".obsidian",
"plugins",
manifest.id
);
await ensureDir(pluginPath); await ensureDir(pluginPath);
await ensureDir(outputPath); await ensureDir(outputPath);
const inputFiles = [ const inputFiles = [
path.join(outputPath, "main.js"), path.join(outputPath, "main.js"),
path.join(outputPath, "styles.css"), path.join(outputPath, "styles.css"),
manifestPath, manifestPath,
]; ];
build({ build({
mode: "development", mode: "development",
configFile: path.resolve(rootPath, "vite.config.js"), configFile: path.resolve(rootPath, "vite.config.js"),
build: { build: {
watch: {}, watch: {},
}, },
plugins: [ plugins: [
{ {
name: "build-finish-copy", name: "build-finish-copy",
closeBundle() { closeBundle() {
console.info("Build finished copying files"); console.info("Build finished copying files");
copyFiles(); copyFiles();
}, },
}, },
], ],
}); });
const copyFiles = async () => { const copyFiles = async () => {
for (const filePath of inputFiles) { for (const filePath of inputFiles) {
const destPath = path.join(pluginPath, path.basename(filePath)); const destPath = path.join(pluginPath, path.basename(filePath));
try { try {
await fs.copyFile(filePath, destPath); await fs.copyFile(filePath, destPath);
console.info(`✓ Updated ${path.basename(filePath)}`); console.info(`✓ Updated ${path.basename(filePath)}`);
} catch (e) { } catch (e) {
console.error( console.error(`Failed to copy ${path.basename(filePath)}:`, e.message);
`Failed to copy ${path.basename(filePath)}:`, }
e.message }
); };
}
}
};
console.info("🚀 Dev server started. Watching for changes..."); console.info("🚀 Dev server started. Watching for changes...");
} }
async function ensureDir(dir) { async function ensureDir(dir) {
try { try {
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
} catch {} } catch {}
} }
dev().catch((err) => { dev().catch((err) => {
console.error(err); console.error(err);
process.exit(1); process.exit(1);
}); });

View file

@ -32,9 +32,7 @@ export class MockVault {
} }
getMarkdownFiles = vi.fn(() => { getMarkdownFiles = vi.fn(() => {
return Object.keys(this._files).map((path) => return Object.keys(this._files).map((path) => createTFile(this.asVault(), path));
createTFile(this.asVault(), path)
);
}); });
process = vi.fn(async (file: TFile, func: (data: string) => string) => { process = vi.fn(async (file: TFile, func: (data: string) => string) => {

View file

@ -8,10 +8,7 @@ import { expect, it, describe } from "vitest";
describe("stopAllTimekeeps", () => { describe("stopAllTimekeeps", () => {
it("should stop nothing when no markdown files", async () => { it("should stop nothing when no markdown files", async () => {
const vault = new MockVault(); const vault = new MockVault();
const amount = await stopAllTimekeeps( const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
vault.asVault(),
moment("2025-11-07T00:31:03.714Z")
);
expect(amount).toBe(0); expect(amount).toBe(0);
}); });
@ -23,10 +20,7 @@ describe("stopAllTimekeeps", () => {
vault.addFile("TEST_MARKDOWN_2.md", ""); vault.addFile("TEST_MARKDOWN_2.md", "");
vault.addFile("TEST_MARKDOWN_3.md", ""); vault.addFile("TEST_MARKDOWN_3.md", "");
const amount = await stopAllTimekeeps( const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
vault.asVault(),
moment("2025-11-07T00:31:03.714Z")
);
expect(amount).toBe(0); expect(amount).toBe(0);
}); });
@ -55,10 +49,7 @@ describe("stopAllTimekeeps", () => {
const file2 = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_3); const file2 = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_3);
const file3 = vault.addFile("TEST_MARKDOWN_3.md", TEST_MARKDOWN_3); const file3 = vault.addFile("TEST_MARKDOWN_3.md", TEST_MARKDOWN_3);
const amount = await stopAllTimekeeps( const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
vault.asVault(),
moment("2025-11-07T00:31:03.714Z")
);
const output1 = await vault.read(file1); const output1 = await vault.read(file1);
expect(output1).toEqual(TEST_MARKDOWN_1_STOPPED); expect(output1).toEqual(TEST_MARKDOWN_1_STOPPED);
@ -103,10 +94,7 @@ describe("stopAllTimekeeps", () => {
const file2 = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_2); const file2 = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_2);
const file3 = vault.addFile("TEST_MARKDOWN_3.md", TEST_MARKDOWN_3); const file3 = vault.addFile("TEST_MARKDOWN_3.md", TEST_MARKDOWN_3);
const amount = await stopAllTimekeeps( const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
vault.asVault(),
moment("2025-11-07T00:31:03.714Z")
);
const output1 = await vault.read(file1); const output1 = await vault.read(file1);
expect(output1).toEqual(TEST_MARKDOWN_1_STOPPED); expect(output1).toEqual(TEST_MARKDOWN_1_STOPPED);
@ -137,10 +125,7 @@ describe("stopAllTimekeeps", () => {
vault.read.mockReturnValueOnce(Promise.resolve("")); vault.read.mockReturnValueOnce(Promise.resolve(""));
await expect( await expect(
stopAllTimekeeps( stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"))
vault.asVault(),
moment("2025-11-07T00:31:03.714Z")
)
).rejects.toThrow(); ).rejects.toThrow();
}); });
}); });

View file

@ -14,8 +14,7 @@ export async function stopAllTimekeeps(vault: Vault, currentTime: Moment) {
const markdownFiles = vault.getMarkdownFiles(); const markdownFiles = vault.getMarkdownFiles();
const batchSize = 10; const batchSize = 10;
const processFile = (file: TFile) => const processFile = (file: TFile) => stopFileTimekeeps(vault, file, currentTime);
stopFileTimekeeps(vault, file, currentTime);
let totalStopped = 0; let totalStopped = 0;

View file

@ -26,10 +26,7 @@ describe("stopFileTimekeeps", () => {
"utf-8" "utf-8"
); );
const file1 = vault.addFile( const file1 = vault.addFile("TEST_MARKDOWN_1.md", TEST_MARKDOWN_1_STOPPED);
"TEST_MARKDOWN_1.md",
TEST_MARKDOWN_1_STOPPED
);
const amount = await stopFileTimekeeps( const amount = await stopFileTimekeeps(
vault.asVault(), vault.asVault(),
@ -81,11 +78,7 @@ describe("stopFileTimekeeps", () => {
); );
const file = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_2); const file = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_2);
await stopFileTimekeeps( await stopFileTimekeeps(vault.asVault(), file, moment("2025-11-07T00:31:03.714Z"));
vault.asVault(),
file,
moment("2025-11-07T00:31:03.714Z")
);
const output = await vault.read(file); const output = await vault.read(file);
expect(output).toEqual(TEST_MARKDOWN_2_STOPPED); expect(output).toEqual(TEST_MARKDOWN_2_STOPPED);
@ -108,11 +101,7 @@ describe("stopFileTimekeeps", () => {
vault.read.mockReturnValueOnce(Promise.resolve("")); vault.read.mockReturnValueOnce(Promise.resolve(""));
await expect( await expect(
stopFileTimekeeps( stopFileTimekeeps(vault.asVault(), file, moment("2025-11-07T00:31:03.714Z"))
vault.asVault(),
file,
moment("2025-11-07T00:31:03.714Z")
)
).rejects.toThrow(); ).rejects.toThrow();
}); });
}); });

View file

@ -2,10 +2,7 @@ import { Moment } from "moment";
import { TFile, Vault } from "obsidian"; import { TFile, Vault } from "obsidian";
import { Timekeep } from "@/timekeep/schema"; import { Timekeep } from "@/timekeep/schema";
import { getRunningEntry, stopRunningEntries } from "@/timekeep"; import { getRunningEntry, stopRunningEntries } from "@/timekeep";
import { import { replaceTimekeepCodeblock, extractTimekeepCodeblocksWithPosition } from "@/timekeep/parser";
replaceTimekeepCodeblock,
extractTimekeepCodeblocksWithPosition,
} from "@/timekeep/parser";
/** /**
* Stops all timekeeps in the provided file if there are any running. * Stops all timekeeps in the provided file if there are any running.
@ -18,27 +15,20 @@ import {
* @param currentTime The current time to use as the stopped time * @param currentTime The current time to use as the stopped time
* @returns The total number of stopped timekeeps * @returns The total number of stopped timekeeps
*/ */
export async function stopFileTimekeeps( export async function stopFileTimekeeps(vault: Vault, file: TFile, currentTime: Moment) {
vault: Vault,
file: TFile,
currentTime: Moment
) {
const content = await vault.cachedRead(file); const content = await vault.cachedRead(file);
const initialTimekeeps = extractTimekeepCodeblocksWithPosition(content); const initialTimekeeps = extractTimekeepCodeblocksWithPosition(content);
// Collect the indexes of running timekeeps // Collect the indexes of running timekeeps
const runningIndexes = initialTimekeeps.reduce<number[]>( const runningIndexes = initialTimekeeps.reduce<number[]>((runningIndexes, timekeep, index) => {
(runningIndexes, timekeep, index) => { const entry = getRunningEntry(timekeep.timekeep.entries);
const entry = getRunningEntry(timekeep.timekeep.entries);
if (entry !== null) { if (entry !== null) {
runningIndexes.push(index); runningIndexes.push(index);
} }
return runningIndexes; return runningIndexes;
}, }, []);
[]
);
// Nothing to process // Nothing to process
if (runningIndexes.length < 1) { if (runningIndexes.length < 1) {
@ -64,12 +54,7 @@ export async function stopFileTimekeeps(
entries: stopRunningEntries(timekeep.entries, currentTime), entries: stopRunningEntries(timekeep.entries, currentTime),
}; };
content = replaceTimekeepCodeblock( content = replaceTimekeepCodeblock(stoppedTimekeep, content, startLine, endLine);
stoppedTimekeep,
content,
startLine,
endLine
);
} }
return content; return content;

View file

@ -4,10 +4,10 @@ import type { Moment } from "moment";
import pdfMake from "pdfmake"; import pdfMake from "pdfmake";
import type { import type {
DynamicContent, DynamicContent,
TableCell, TableCell,
TDocumentDefinitions, TDocumentDefinitions,
TFontDictionary, TFontDictionary,
} from "pdfmake/interfaces"; } from "pdfmake/interfaces";
// Embedded fonts // Embedded fonts
@ -16,362 +16,346 @@ import RubikRegular from "@/fonts/Rubik-Regular.ttf";
import RubikBold from "@/fonts/Rubik-Bold.ttf"; import RubikBold from "@/fonts/Rubik-Bold.ttf";
import RobotoRegular from "@/fonts/Roboto-Regular.ttf"; import RobotoRegular from "@/fonts/Roboto-Regular.ttf";
import { getEntryDuration, getTotalDuration } from "@/timekeep"; import { getEntryDuration, getTotalDuration } from "@/timekeep";
import { import { formatDurationLong, formatDurationShort, formatPdfDate, formatPdfRowDate } from "@/utils";
formatDurationLong,
formatDurationShort,
formatPdfDate,
formatPdfRowDate,
} from "@/utils";
import { Readable } from "stream"; import { Readable } from "stream";
const fonts: TFontDictionary = { const fonts: TFontDictionary = {
Roboto: { Roboto: {
normal: "Roboto-Regular.ttf", normal: "Roboto-Regular.ttf",
bold: "Roboto-Bold.ttf", bold: "Roboto-Bold.ttf",
}, },
Rubik: { Rubik: {
normal: "Rubik-Regular.ttf", normal: "Rubik-Regular.ttf",
bold: "Rubik-Bold.ttf", bold: "Rubik-Bold.ttf",
}, },
}; };
function stripDataUrlPrefix(text: string): string { function stripDataUrlPrefix(text: string): string {
return text.substring("data:font/ttf;base64,".length); return text.substring("data:font/ttf;base64,".length);
} }
pdfMake.addVirtualFileSystem({ pdfMake.addVirtualFileSystem({
"Roboto-Regular.ttf": stripDataUrlPrefix(RobotoRegular), "Roboto-Regular.ttf": stripDataUrlPrefix(RobotoRegular),
"Roboto-Bold.ttf": stripDataUrlPrefix(RobotoBold), "Roboto-Bold.ttf": stripDataUrlPrefix(RobotoBold),
"Rubik-Regular.ttf": stripDataUrlPrefix(RubikRegular), "Rubik-Regular.ttf": stripDataUrlPrefix(RubikRegular),
"Rubik-Bold.ttf": stripDataUrlPrefix(RubikBold), "Rubik-Bold.ttf": stripDataUrlPrefix(RubikBold),
}); });
pdfMake.addFonts(fonts); pdfMake.addFonts(fonts);
export async function createPdfExport( export async function createPdfExport(
timekeep: Timekeep, timekeep: Timekeep,
settings: TimekeepSettings, settings: TimekeepSettings,
currentTime: Moment currentTime: Moment
): Promise<NodeJS.ReadableStream> { ): Promise<NodeJS.ReadableStream> {
const definition = createPdfDefinition(timekeep, settings, currentTime); const definition = createPdfDefinition(timekeep, settings, currentTime);
const pdf = pdfMake.createPdf(definition, {}); const pdf = pdfMake.createPdf(definition, {});
const stream = await pdf.getBuffer(); const stream = await pdf.getBuffer();
return Readable.from([stream]); return Readable.from([stream]);
} }
export function createPdfDefinition( export function createPdfDefinition(
timekeep: Timekeep, timekeep: Timekeep,
settings: TimekeepSettings, settings: TimekeepSettings,
currentTime: Moment currentTime: Moment
): TDocumentDefinitions { ): TDocumentDefinitions {
const duration = getTotalDuration(timekeep.entries, currentTime); const duration = getTotalDuration(timekeep.entries, currentTime);
const currentDate = formatPdfDate(currentTime, settings); const currentDate = formatPdfDate(currentTime, settings);
const totalDuration = formatDurationLong(duration); const totalDuration = formatDurationLong(duration);
const totalDurationShort = formatDurationShort(duration); const totalDurationShort = formatDurationShort(duration);
return { return {
content: [ content: [
createPdfHeader( createPdfHeader(settings.pdfTitle, currentDate, totalDuration, totalDurationShort),
settings.pdfTitle, createPdfTable(timekeep, totalDuration, settings, currentTime),
currentDate, ],
totalDuration, footer: createPdfFooter(settings),
totalDurationShort defaultStyle: {
), font: settings.pdfFontFamily,
createPdfTable(timekeep, totalDuration, settings, currentTime), },
], pageSize: "A4",
footer: createPdfFooter(settings), pageMargins: 15,
defaultStyle: { styles: {
font: settings.pdfFontFamily, heading: {
}, marginBottom: 5,
pageSize: "A4", columnGap: 10,
pageMargins: 15, },
styles: { title: {
heading: { bold: true,
marginBottom: 5, fontSize: 12,
columnGap: 10, marginBottom: 5,
}, },
title: { details: {
bold: true, marginBottom: 5,
fontSize: 12, },
marginBottom: 5, footNote: {
}, noWrap: true,
details: { fontSize: 6,
marginBottom: 5, bold: true,
}, },
footNote: { pageNumber: {
noWrap: true, noWrap: true,
fontSize: 6, fontSize: 6,
bold: true, },
}, footer: {
pageNumber: { marginLeft: 15,
noWrap: true, marginRight: 15,
fontSize: 6, },
}, tableCell: {
footer: { fontSize: 8,
marginLeft: 15, },
marginRight: 15, tableCellTime: {
}, alignment: "right",
tableCell: { },
fontSize: 8, tableCellHeader: {
}, bold: true,
tableCellTime: { },
alignment: "right", },
}, };
tableCellHeader: {
bold: true,
},
},
};
} }
function createPdfHeader( function createPdfHeader(
pdfTitle: string, pdfTitle: string,
currentDate: string, currentDate: string,
totalDuration: string, totalDuration: string,
totalDurationShort: string totalDurationShort: string
): pdfMake.Content { ): pdfMake.Content {
return [ return [
{ {
columns: [ columns: [
{ {
text: pdfTitle, text: pdfTitle,
alignment: "left", alignment: "left",
style: "title", style: "title",
width: "50%", width: "50%",
}, },
{ {
text: "Timesheet", text: "Timesheet",
alignment: "right", alignment: "right",
style: "title", style: "title",
width: "50%", width: "50%",
}, },
], ],
style: "heading", style: "heading",
}, },
{ {
stack: [ stack: [
createPdfHeaderDetailField("Date", currentDate), createPdfHeaderDetailField("Date", currentDate),
createPdfHeaderDetailField("Total Duration", totalDuration), createPdfHeaderDetailField("Total Duration", totalDuration),
createPdfHeaderDetailField( createPdfHeaderDetailField("Total Duration (hours)", totalDurationShort),
"Total Duration (hours)", ],
totalDurationShort
),
],
style: "details", style: "details",
}, },
]; ];
} }
function createPdfHeaderDetailField( function createPdfHeaderDetailField(title: string, value: string): pdfMake.Content {
title: string, return {
value: string text: [
): pdfMake.Content { { text: title + ": ", fontSize: 8, bold: true },
return { { text: value, fontSize: 8 },
text: [ ],
{ text: title + ": ", fontSize: 8, bold: true }, marginBottom: 8,
{ text: value, fontSize: 8 }, };
],
marginBottom: 8,
};
} }
function createPdfFooter(settings: TimekeepSettings): DynamicContent { function createPdfFooter(settings: TimekeepSettings): DynamicContent {
return function (currentPage: number, pageCount: number) { return function (currentPage: number, pageCount: number) {
return { return {
columns: [ columns: [
{ {
text: `${settings.pdfFootnote}`, text: `${settings.pdfFootnote}`,
style: "footNote", style: "footNote",
width: "50%", width: "50%",
alignment: "left", alignment: "left",
}, },
{ {
text: `${currentPage} of ${pageCount}`, text: `${currentPage} of ${pageCount}`,
style: "pageNumber", style: "pageNumber",
width: "50%", width: "50%",
alignment: "right", alignment: "right",
}, },
], ],
style: "footer", style: "footer",
}; };
}; };
} }
function createPdfTable( function createPdfTable(
timekeep: Timekeep, timekeep: Timekeep,
totalDuration: string, totalDuration: string,
settings: TimekeepSettings, settings: TimekeepSettings,
currentTime: Moment currentTime: Moment
): pdfMake.Content { ): pdfMake.Content {
const rows = createPdfTableRows(timekeep.entries, settings, currentTime); const rows = createPdfTableRows(timekeep.entries, settings, currentTime);
return { return {
table: { table: {
headerRows: 1, headerRows: 1,
widths: ["*", 70, 70, 50], widths: ["*", 70, 70, 50],
body: [ body: [
[ [
{ {
text: "Block Name", text: "Block Name",
style: ["tableCell", "tableCellHeader"], style: ["tableCell", "tableCellHeader"],
border: [true, false, false, true], border: [true, false, false, true],
}, },
{ {
text: "Start Time", text: "Start Time",
style: ["tableCell", "tableCellHeader"], style: ["tableCell", "tableCellHeader"],
alignment: "center", alignment: "center",
border: [false, false, false, true], border: [false, false, false, true],
}, },
{ {
text: "End Time", text: "End Time",
style: ["tableCell", "tableCellHeader"], style: ["tableCell", "tableCellHeader"],
alignment: "center", alignment: "center",
border: [false, false, false, true], border: [false, false, false, true],
}, },
{ {
text: "Duration", text: "Duration",
style: ["tableCell", "tableCellHeader"], style: ["tableCell", "tableCellHeader"],
alignment: "right", alignment: "right",
border: [false, false, true, true], border: [false, false, true, true],
}, },
], ],
...rows.map((row) => row.row), ...rows.map((row) => row.row),
[ [
{ text: "Total", style: "tableCell" }, { text: "Total", style: "tableCell" },
{ text: "", style: "tableCell" }, { text: "", style: "tableCell" },
{ text: "", style: "tableCell" }, { text: "", style: "tableCell" },
{ {
text: totalDuration, text: totalDuration,
style: ["tableCell", "tableCellFooter"], style: ["tableCell", "tableCellFooter"],
bold: true, bold: true,
alignment: "right", alignment: "right",
}, },
], ],
], ],
}, },
layout: { layout: {
fillColor: function (rowIndex, _node, _columnIndex) { fillColor: function (rowIndex, _node, _columnIndex) {
// Header & Footer styling // Header & Footer styling
if (rowIndex === 0 || rowIndex === rows.length + 1) { if (rowIndex === 0 || rowIndex === rows.length + 1) {
return "#ececec"; return "#ececec";
} }
// Group row styling // Group row styling
if (rowIndex > 0 && rowIndex - 1 < rows.length) { if (rowIndex > 0 && rowIndex - 1 < rows.length) {
if (rows[rowIndex - 1].group) { if (rows[rowIndex - 1].group) {
return "#f4f4f4"; return "#f4f4f4";
} }
} }
// Default styling // Default styling
return "#ffffff"; return "#ffffff";
}, },
vLineColor: function (_columnIndex, _node, _rowIndex) { vLineColor: function (_columnIndex, _node, _rowIndex) {
return "#ececec"; return "#ececec";
}, },
hLineColor: function (_rowIndex, _node, _columnIndex) { hLineColor: function (_rowIndex, _node, _columnIndex) {
return "#ececec"; return "#ececec";
}, },
paddingBottom: cellPadding, paddingBottom: cellPadding,
paddingLeft: cellPadding, paddingLeft: cellPadding,
paddingTop: cellPadding, paddingTop: cellPadding,
paddingRight: cellPadding, paddingRight: cellPadding,
}, },
}; };
} }
const cellPadding = () => 8; const cellPadding = () => 8;
type TableEntryRow = { type TableEntryRow = {
row: TableCell[]; row: TableCell[];
depth: number; depth: number;
group: boolean; group: boolean;
}; };
function createPdfTableRows( function createPdfTableRows(
entries: TimeEntry[], entries: TimeEntry[],
settings: TimekeepSettings, settings: TimekeepSettings,
currentTime: Moment currentTime: Moment
): TableEntryRow[] { ): TableEntryRow[] {
type StackEntry = { entry: TimeEntry; depth: number }; type StackEntry = { entry: TimeEntry; depth: number };
const rows: TableEntryRow[] = []; const rows: TableEntryRow[] = [];
const stack: StackEntry[] = entries.map((entry) => ({ entry, depth: 0 })); const stack: StackEntry[] = entries.map((entry) => ({ entry, depth: 0 }));
while (stack.length > 0) { while (stack.length > 0) {
const { entry, depth } = stack.pop()!; const { entry, depth } = stack.pop()!;
rows.push({ rows.push({
group: entry.subEntries !== null && entry.subEntries.length > 0, group: entry.subEntries !== null && entry.subEntries.length > 0,
row: createTableEntryCells(entry, depth, settings, currentTime), row: createTableEntryCells(entry, depth, settings, currentTime),
depth, depth,
}); });
if (entry.subEntries && entry.subEntries.length > 0) { if (entry.subEntries && entry.subEntries.length > 0) {
for (let i = entry.subEntries.length - 1; i >= 0; i--) { for (let i = entry.subEntries.length - 1; i >= 0; i--) {
stack.push({ stack.push({
entry: entry.subEntries[i], entry: entry.subEntries[i],
depth: depth + 1, depth: depth + 1,
}); });
} }
} }
} }
return rows; return rows;
} }
function createTableEntryCells( function createTableEntryCells(
entry: TimeEntry, entry: TimeEntry,
depth: number, depth: number,
settings: TimekeepSettings, settings: TimekeepSettings,
currentTime: Moment currentTime: Moment
): TableCell[] { ): TableCell[] {
const duration = getEntryDuration(entry, currentTime); const duration = getEntryDuration(entry, currentTime);
const durationFormatted = formatDurationLong(duration); const durationFormatted = formatDurationLong(duration);
let startTime: string; let startTime: string;
let endTime: string; let endTime: string;
if (entry.startTime !== null) { if (entry.startTime !== null) {
startTime = formatPdfRowDate(entry.startTime, settings); startTime = formatPdfRowDate(entry.startTime, settings);
endTime = formatPdfRowDate(entry.endTime ?? currentTime, settings); endTime = formatPdfRowDate(entry.endTime ?? currentTime, settings);
} else { } else {
startTime = ""; startTime = "";
endTime = ""; endTime = "";
} }
const marginLeft = depth * 5; const marginLeft = depth * 5;
if (depth > 0) { if (depth > 0) {
const _bg = "#999"; const _bg = "#999";
} }
return [ return [
{ {
text: entry.name, text: entry.name,
style: ["tableCell", "tableCellBlock"], style: ["tableCell", "tableCellBlock"],
marginLeft, marginLeft,
border: [true, false, false, true], border: [true, false, false, true],
}, },
{ {
text: startTime, text: startTime,
style: ["tableCell", "tableCellTime"], style: ["tableCell", "tableCellTime"],
alignment: "center", alignment: "center",
}, },
{ {
text: endTime, text: endTime,
style: ["tableCell", "tableCellTime"], style: ["tableCell", "tableCellTime"],
alignment: "center", alignment: "center",
}, },
{ {
text: durationFormatted, text: durationFormatted,
style: ["tableCell", "tableCellTime"], style: ["tableCell", "tableCellTime"],
alignment: "right", alignment: "right",
border: [false, false, true, true], border: [false, false, true, true],
}, },
]; ];
} }

View file

@ -63,25 +63,11 @@ export class TimesheetApp extends Component {
this.#wrapperEl = wrapperEl; this.#wrapperEl = wrapperEl;
const counters = new TimesheetCounters( const counters = new TimesheetCounters(wrapperEl, this.settings, this.timekeep);
wrapperEl,
this.settings,
this.timekeep
);
const start = new TimesheetStart( const start = new TimesheetStart(wrapperEl, this.app, this.timekeep, this.settings);
wrapperEl,
this.app,
this.timekeep,
this.settings
);
const table = new TimesheetTable( const table = new TimesheetTable(wrapperEl, this.app, this.timekeep, this.settings);
wrapperEl,
this.app,
this.timekeep,
this.settings
);
const exportActions = new TimesheetExportActions( const exportActions = new TimesheetExportActions(
wrapperEl, wrapperEl,

View file

@ -3,12 +3,7 @@ import type { Store } from "@/store";
import type { Timekeep } from "@/timekeep/schema"; import type { Timekeep } from "@/timekeep/schema";
import { Component } from "obsidian"; import { Component } from "obsidian";
import moment from "moment"; import moment from "moment";
import { import { getEntryDuration, getRunningEntry, getTotalDuration, isKeepRunning } from "@/timekeep";
getEntryDuration,
getRunningEntry,
getTotalDuration,
isKeepRunning,
} from "@/timekeep";
import { formatDuration } from "@/utils"; import { formatDuration } from "@/utils";
import { TimesheetTimer } from "./timesheetTimer"; import { TimesheetTimer } from "./timesheetTimer";
@ -96,10 +91,7 @@ export class TimesheetCounters extends Component {
// Only schedule further updates if we are running // Only schedule further updates if we are running
const timekeep = this.timekeep.getState(); const timekeep = this.timekeep.getState();
if (isKeepRunning(timekeep)) { if (isKeepRunning(timekeep)) {
const intervalID = window.setInterval( const intervalID = window.setInterval(this.updateTimers.bind(this), 1000);
this.updateTimers.bind(this),
1000
);
this.currentContentInterval = intervalID; this.currentContentInterval = intervalID;
this.registerInterval(intervalID); this.registerInterval(intervalID);
@ -116,9 +108,7 @@ export class TimesheetCounters extends Component {
const currentTime = moment(); const currentTime = moment();
const total = getTotalDuration(timekeep.entries, currentTime); const total = getTotalDuration(timekeep.entries, currentTime);
const runningEntry = getRunningEntry(timekeep.entries); const runningEntry = getRunningEntry(timekeep.entries);
const current = runningEntry const current = runningEntry ? getEntryDuration(runningEntry, currentTime) : 0;
? getEntryDuration(runningEntry, currentTime)
: 0;
this.currentTimer.setHidden(runningEntry === null); this.currentTimer.setHidden(runningEntry === null);
this.currentTimer.setValues( this.currentTimer.setValues(

View file

@ -66,29 +66,13 @@ export class TimesheetExportActions extends Component {
text: "Save PDF", text: "Save PDF",
}); });
this.registerDomEvent( this.registerDomEvent(copyMarkdownButton, "click", this.onCopyMarkdown.bind(this));
copyMarkdownButton,
"click",
this.onCopyMarkdown.bind(this)
);
this.registerDomEvent( this.registerDomEvent(copyCSVButton, "click", this.onCopyCSV.bind(this));
copyCSVButton,
"click",
this.onCopyCSV.bind(this)
);
this.registerDomEvent( this.registerDomEvent(copyJSONButton, "click", this.onCopyJSON.bind(this));
copyJSONButton,
"click",
this.onCopyJSON.bind(this)
);
this.registerDomEvent( this.registerDomEvent(savePdfButton, "click", this.onSavePDF.bind(this));
savePdfButton,
"click",
this.onSavePDF.bind(this)
);
// Disable and hide the save as PDF button on mobile // Disable and hide the save as PDF button on mobile
if (Platform.isMobileApp) { if (Platform.isMobileApp) {
@ -96,10 +80,8 @@ export class TimesheetExportActions extends Component {
savePdfButton.hidden = true; savePdfButton.hidden = true;
} }
const createCustomButtons = const createCustomButtons = this.createCustomOutputFormatButtons.bind(this);
this.createCustomOutputFormatButtons.bind(this); const unsubscribeCustomFormats = this.customOutputFormats.subscribe(createCustomButtons);
const unsubscribeCustomFormats =
this.customOutputFormats.subscribe(createCustomButtons);
this.register(unsubscribeCustomFormats); this.register(unsubscribeCustomFormats);
createCustomButtons(); createCustomButtons();
} }

View file

@ -98,9 +98,7 @@ export class TimesheetRowContainer extends Component {
* *
* @param content The new content to show * @param content The new content to show
*/ */
private swapContent( private swapContent(content: TimesheetRowContent | TimesheetRowContentEditing | undefined) {
content: TimesheetRowContent | TimesheetRowContentEditing | undefined
) {
if (this.#content) { if (this.#content) {
this.removeChild(this.#content); this.removeChild(this.#content);
} }

View file

@ -84,11 +84,7 @@ export class TimesheetRowContent extends Component {
title: entry.name, title: entry.name,
}); });
this.registerDomEvent( this.registerDomEvent(nameEl, "click", this.onToggleCollapsed.bind(this));
nameEl,
"click",
this.onToggleCollapsed.bind(this)
);
if (entry.subEntries !== null && entry.folder) { if (entry.subEntries !== null && entry.folder) {
createObsidianIcon(nameEl, "folder", "timekeep-folder-icon"); createObsidianIcon(nameEl, "folder", "timekeep-folder-icon");
@ -131,10 +127,7 @@ export class TimesheetRowContent extends Component {
cls: ["timekeep-col", "timekeep-col--duration"], cls: ["timekeep-col", "timekeep-col--duration"],
}); });
const duration = new TimesheetRowDurationComponent( const duration = new TimesheetRowDurationComponent(durationColEl, entry);
durationColEl,
entry
);
this.addChild(duration); this.addChild(duration);
@ -158,30 +151,18 @@ export class TimesheetRowContent extends Component {
createObsidianIcon(editButton, "edit", "button-icon"); createObsidianIcon(editButton, "edit", "button-icon");
this.registerDomEvent( this.registerDomEvent(startButton, "click", this.onClickStart.bind(this));
startButton,
"click",
this.onClickStart.bind(this)
);
this.registerDomEvent(editButton, "click", this.onBeginEditing); this.registerDomEvent(editButton, "click", this.onBeginEditing);
this.updateTimes(); this.updateTimes();
this.updateState(); this.updateState();
const unsubscribeSettings = this.settings.subscribe( const unsubscribeSettings = this.settings.subscribe(this.updateTimes.bind(this));
this.updateTimes.bind(this)
);
this.register(unsubscribeSettings); this.register(unsubscribeSettings);
this.#columns.push( this.#columns.push(nameColEl, startTimeColEl, endTimeColEl, durationColEl, actionsColEl);
nameColEl,
startTimeColEl,
endTimeColEl,
durationColEl,
actionsColEl
);
} }
onunload(): void { onunload(): void {
@ -203,9 +184,7 @@ export class TimesheetRowContent extends Component {
? formatTimestamp(entry.startTime, settings) ? formatTimestamp(entry.startTime, settings)
: ""; : "";
this.#endTimeEl.textContent = entry.endTime this.#endTimeEl.textContent = entry.endTime ? formatTimestamp(entry.endTime, settings) : "";
? formatTimestamp(entry.endTime, settings)
: "";
} }
updateState() { updateState() {
@ -213,12 +192,10 @@ export class TimesheetRowContent extends Component {
const entry = this.entry; const entry = this.entry;
const isSelfRunning = const isSelfRunning = entry.subEntries === null && isEntryRunning(entry);
entry.subEntries === null && isEntryRunning(entry);
const isRunningWithin = const isRunningWithin =
entry.subEntries !== null && entry.subEntries !== null && getRunningEntry(entry.subEntries) !== null;
getRunningEntry(entry.subEntries) !== null;
const isInvalidEntry = const isInvalidEntry =
entry.startTime !== null && entry.startTime !== null &&
@ -229,10 +206,7 @@ export class TimesheetRowContent extends Component {
rowEl.setAttribute("data-running", String(isSelfRunning)); rowEl.setAttribute("data-running", String(isSelfRunning));
rowEl.setAttribute("data-running-within", String(isRunningWithin)); rowEl.setAttribute("data-running-within", String(isRunningWithin));
rowEl.setAttribute( rowEl.setAttribute("data-sub-entires", String(this.entry.subEntries !== null));
"data-sub-entires",
String(this.entry.subEntries !== null)
);
rowEl.setAttribute("data-invalid", String(isInvalidEntry)); rowEl.setAttribute("data-invalid", String(isInvalidEntry));
} }
@ -252,11 +226,7 @@ export class TimesheetRowContent extends Component {
this.timekeep.setState((timekeep) => { this.timekeep.setState((timekeep) => {
const currentTime = moment(); const currentTime = moment();
const entries = startNewNestedEntry( const entries = startNewNestedEntry(currentTime, entry.id, timekeep.entries);
currentTime,
entry.id,
timekeep.entries
);
return { return {
...timekeep, ...timekeep,

View file

@ -129,11 +129,7 @@ export class TimesheetRowContentEditing extends Component {
createObsidianIcon(deleteButton, "trash", "text-button-icon"); createObsidianIcon(deleteButton, "trash", "text-button-icon");
deleteButton.appendText("Delete"); deleteButton.appendText("Delete");
this.registerDomEvent( this.registerDomEvent(deleteButton, "click", this.onConfirmDelete.bind(this));
deleteButton,
"click",
this.onConfirmDelete.bind(this)
);
const onUpdateState = this.onUpdateState.bind(this); const onUpdateState = this.onUpdateState.bind(this);
const unsubscribeSettings = this.settings.subscribe(onUpdateState); const unsubscribeSettings = this.settings.subscribe(onUpdateState);
@ -196,11 +192,7 @@ export class TimesheetRowContentEditing extends Component {
} }
onSubmit(event: Event) { onSubmit(event: Event) {
if ( if (!this.#nameInputEl || !this.#startTimeInputEl || !this.#endTimeInputEl) {
!this.#nameInputEl ||
!this.#startTimeInputEl ||
!this.#endTimeInputEl
) {
return; return;
} }
@ -219,10 +211,7 @@ export class TimesheetRowContentEditing extends Component {
// Update the start and end times for non groups // Update the start and end times for non groups
if (newEntry.subEntries === null) { if (newEntry.subEntries === null) {
if (entry.startTime !== null) { if (entry.startTime !== null) {
const startTimeValue = parseEditableTimestamp( const startTimeValue = parseEditableTimestamp(startTime, settings);
startTime,
settings
);
if (startTimeValue.isValid()) { if (startTimeValue.isValid()) {
newEntry.startTime = startTimeValue; newEntry.startTime = startTimeValue;
} }

View file

@ -46,10 +46,7 @@ export class TimesheetRowDurationComponent extends Component {
// Only schedule further updates if we are running // Only schedule further updates if we are running
if (isRunning) { if (isRunning) {
const intervalID = window.setInterval( const intervalID = window.setInterval(this.updateTime.bind(this), 1000);
this.updateTime.bind(this),
1000
);
this.currentContentInterval = intervalID; this.currentContentInterval = intervalID;
this.registerInterval(intervalID); this.registerInterval(intervalID);

View file

@ -57,11 +57,7 @@ export class TimesheetSaveError extends Component {
text: "Copy Timekeep", text: "Copy Timekeep",
}); });
this.registerDomEvent( this.registerDomEvent(retryButton, "click", this.onRetrySave.bind(this));
retryButton,
"click",
this.onRetrySave.bind(this)
);
this.registerDomEvent(copyButton, "click", this.onCopy.bind(this)); this.registerDomEvent(copyButton, "click", this.onCopy.bind(this));
} }

View file

@ -117,9 +117,7 @@ export class TimesheetStart extends Component {
* *
* @param content The new content to show * @param content The new content to show
*/ */
private swapContent( private swapContent(content: TimesheetStartRunning | TimekeepStartEditing | undefined) {
content: TimesheetStartRunning | TimekeepStartEditing | undefined
) {
if (this.#content) { if (this.#content) {
this.removeChild(this.#content); this.removeChild(this.#content);
} }
@ -149,12 +147,9 @@ export class TimesheetStart extends Component {
const currentEntry = getRunningEntry(timekeep.entries); const currentEntry = getRunningEntry(timekeep.entries);
const isTimekeepRunning = currentEntry !== null; const isTimekeepRunning = currentEntry !== null;
this.#blockPauseWarningEl.hidden = this.#blockPauseWarningEl.hidden = currentEntry === null || currentEntry.startTime === null;
currentEntry === null || currentEntry.startTime === null;
this.#startButtonEl.title = isTimekeepRunning this.#startButtonEl.title = isTimekeepRunning ? "Stop and start" : "Start";
? "Stop and start"
: "Start";
} }
setEmptyView() { setEmptyView() {

View file

@ -139,10 +139,7 @@ export class TimesheetStartRunning extends Component {
const timekeep = this.timekeep.getState(); const timekeep = this.timekeep.getState();
const settings = this.settings.getState(); const settings = this.settings.getState();
this.#timeValueEl.textContent = formatTimestamp( this.#timeValueEl.textContent = formatTimestamp(currentEntry.startTime, settings);
currentEntry.startTime,
settings
);
// Clear existing path // Clear existing path
this.#pathEl.empty(); this.#pathEl.empty();
@ -151,9 +148,7 @@ export class TimesheetStartRunning extends Component {
if (pathToEntry && pathToEntry.length > 0) { if (pathToEntry && pathToEntry.length > 0) {
for (let i = 0; i < pathToEntry.length; i++) { for (let i = 0; i < pathToEntry.length; i++) {
const path = pathToEntry[i]; const path = pathToEntry[i];
const text = `${path.name} ${ const text = `${path.name} ${i < pathToEntry.length - 1 ? " >" : ""}`;
i < pathToEntry.length - 1 ? " >" : ""
}`;
this.#pathEl.createSpan({ text }); this.#pathEl.createSpan({ text });
} }
} }

View file

@ -151,15 +151,8 @@ export class TimesheetTable extends Component {
this.addChild(row); this.addChild(row);
this.#rows.push(row); this.#rows.push(row);
if ( if (entry.subEntries && !entry.collapsed && entry.subEntries.length > 0) {
entry.subEntries && const sortedEntries = getEntriesSorted(entry.subEntries, settings);
!entry.collapsed &&
entry.subEntries.length > 0
) {
const sortedEntries = getEntriesSorted(
entry.subEntries,
settings
);
for (let i = sortedEntries.length - 1; i >= 0; i--) { for (let i = sortedEntries.length - 1; i >= 0; i--) {
stack.push({ stack.push({

View file

@ -25,9 +25,7 @@ export function createRawTable(
settings: TimekeepSettings, settings: TimekeepSettings,
currentTime: Moment currentTime: Moment
): RawTableRow[] { ): RawTableRow[] {
return entries.flatMap((entry) => return entries.flatMap((entry) => createRawTableEntries(entry, settings, currentTime));
createRawTableEntries(entry, settings, currentTime)
);
} }
/** /**
@ -51,8 +49,7 @@ export function createRawTableEntries(
entry.startTime ? formatTimestamp(entry.startTime, settings) : "", entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
entry.endTime ? formatTimestamp(entry.endTime, settings) : "", entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
// Include duration for entries that are finished // Include duration for entries that are finished
(entry.startTime !== null && entry.endTime !== null) || (entry.startTime !== null && entry.endTime !== null) || entry.subEntries !== null
entry.subEntries !== null
? formatDuration( ? formatDuration(
settings.exportDurationFormat, settings.exportDurationFormat,
getEntryDuration(entry, currentTime) getEntryDuration(entry, currentTime)

View file

@ -27,10 +27,7 @@ function createFooter(
currentTime: Moment, currentTime: Moment,
durationFormat: DurationFormat durationFormat: DurationFormat
): RawTableRow { ): RawTableRow {
const total: string = formatDuration( const total: string = formatDuration(durationFormat, getTotalDuration(entries, currentTime));
durationFormat,
getTotalDuration(entries, currentTime)
);
return ["**Total**", "", "", `**${total}**`]; return ["**Total**", "", "", `**${total}**`];
} }
@ -73,11 +70,7 @@ export function createMarkdownTable(
// Markdown raw table contents // Markdown raw table contents
...createRawTable(timekeep.entries, settings, currentTime), ...createRawTable(timekeep.entries, settings, currentTime),
// Markdown footer row // Markdown footer row
createFooter( createFooter(timekeep.entries, currentTime, settings.exportDurationFormat),
timekeep.entries,
currentTime,
settings.exportDurationFormat
),
]; ];
// Array of indexes for all the columns (0 - TOTAL_COLUMNS) // Array of indexes for all the columns (0 - TOTAL_COLUMNS)

View file

@ -6,10 +6,7 @@ import { Timekeep } from "@/timekeep/schema";
import { mkdir, writeFile } from "fs/promises"; import { mkdir, writeFile } from "fs/promises";
import { TimekeepSettings, PdfExportBehavior } from "@/settings"; import { TimekeepSettings, PdfExportBehavior } from "@/settings";
export async function exportPdf( export async function exportPdf(timekeep: Timekeep, settings: TimekeepSettings) {
timekeep: Timekeep,
settings: TimekeepSettings
) {
// Pdf exports don't work in mobile mode // Pdf exports don't work in mobile mode
if (Platform.isMobileApp) return; if (Platform.isMobileApp) return;

View file

@ -1,16 +1,8 @@
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
import { Store, createStore } from "@/store"; import { Store, createStore } from "@/store";
import { TimekeepSettingsTab } from "@/settings-tab"; import { TimekeepSettingsTab } from "@/settings-tab";
import { import { defaultSettings, TimekeepSettings, legacySettingsCompatibility } from "@/settings";
defaultSettings, import { load, replaceTimekeepCodeblock, extractTimekeepCodeblocks } from "@/timekeep/parser";
TimekeepSettings,
legacySettingsCompatibility,
} from "@/settings";
import {
load,
replaceTimekeepCodeblock,
extractTimekeepCodeblocks,
} from "@/timekeep/parser";
import { import {
isKeepRunning, isKeepRunning,
isEntryRunning, isEntryRunning,
@ -53,11 +45,7 @@ export default class TimekeepPlugin extends Plugin {
getEntryDuration: (entry: TimeEntry, currentTime: Moment) => number; getEntryDuration: (entry: TimeEntry, currentTime: Moment) => number;
getTotalDuration: (entries: TimeEntry[], currentTime: Moment) => number; getTotalDuration: (entries: TimeEntry[], currentTime: Moment) => number;
stopAllTimekeeps: (vault: Vault, currentTime: Moment) => Promise<number>; stopAllTimekeeps: (vault: Vault, currentTime: Moment) => Promise<number>;
stopFileTimekeeps: ( stopFileTimekeeps: (vault: Vault, file: TFile, currentTime: Moment) => Promise<number>;
vault: Vault,
file: TFile,
currentTime: Moment
) => Promise<number>;
constructor(app: ObsidianApp, manifest: PluginManifest) { constructor(app: ObsidianApp, manifest: PluginManifest) {
super(app, manifest); super(app, manifest);
@ -103,11 +91,7 @@ export default class TimekeepPlugin extends Plugin {
this.registerMarkdownCodeBlockProcessor( this.registerMarkdownCodeBlockProcessor(
"timekeep", "timekeep",
( (source: string, el: HTMLElement, context: MarkdownPostProcessorContext) => {
source: string,
el: HTMLElement,
context: MarkdownPostProcessorContext
) => {
const loadResult = load(source); const loadResult = load(source);
context.addChild( context.addChild(
@ -140,23 +124,13 @@ export default class TimekeepPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: `create-merged`, id: `create-merged`,
name: `Create Merged Tracker`, name: `Create Merged Tracker`,
callback: () => callback: () => new TimekeepMergerModal(this.app, this.settingsStore, false).open(),
new TimekeepMergerModal(
this.app,
this.settingsStore,
false
).open(),
}); });
this.addCommand({ this.addCommand({
id: `export-merged-pdf`, id: `export-merged-pdf`,
name: `Export Merged Tracker PDF`, name: `Export Merged Tracker PDF`,
callback: () => callback: () => new TimekeepMergerModal(this.app, this.settingsStore, true).open(),
new TimekeepMergerModal(
this.app,
this.settingsStore,
true
).open(),
}); });
this.addCommand({ this.addCommand({
@ -186,10 +160,7 @@ export default class TimekeepPlugin extends Plugin {
error = "Unknown error occurred"; error = "Unknown error occurred";
} }
new Notice( new Notice("Failed to stop timekeeps: " + errorMessage, 1500);
"Failed to stop timekeeps: " + errorMessage,
1500
);
}); });
}, },
}); });
@ -199,8 +170,7 @@ export default class TimekeepPlugin extends Plugin {
name: `Stop All Running Trackers (Current File Only)`, name: `Stop All Running Trackers (Current File Only)`,
callback: () => { callback: () => {
const currentTime = moment(); const currentTime = moment();
const currentFile = const currentFile = this.app.workspace.activeEditor?.file ?? null;
this.app.workspace.activeEditor?.file ?? null;
if (currentFile === null) { if (currentFile === null) {
new Notice("No active file detected", 1500); new Notice("No active file detected", 1500);
@ -229,10 +199,7 @@ export default class TimekeepPlugin extends Plugin {
error = "Unknown error occurred"; error = "Unknown error occurred";
} }
new Notice( new Notice("Failed to stop timekeeps: " + errorMessage, 1500);
"Failed to stop timekeeps: " + errorMessage,
1500
);
}); });
}, },
}); });

View file

@ -17,9 +17,5 @@ export interface CustomOutputFormat {
* @param settings The timekeep settings * @param settings The timekeep settings
* @param currentTime The current time to use for unfinished entries * @param currentTime The current time to use for unfinished entries
*/ */
onExport( onExport(timekeep: Timekeep, settings: TimekeepSettings, currentTime: Moment): void;
timekeep: Timekeep,
settings: TimekeepSettings,
currentTime: Moment
): void;
} }

View file

@ -43,9 +43,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
t.setValue(String(settings.timestampFormat)); t.setValue(String(settings.timestampFormat));
t.onChange((v) => { t.onChange((v) => {
// Only use a custom format if the value is not blank // Only use a custom format if the value is not blank
const newFormat = v.length const newFormat = v.length ? v : defaultSettings.timestampFormat;
? v
: defaultSettings.timestampFormat;
this.settingsStore.setState((currentValue) => ({ this.settingsStore.setState((currentValue) => ({
...currentValue, ...currentValue,
@ -56,18 +54,13 @@ export class TimekeepSettingsTab extends PluginSettingTab {
new Setting(this.containerEl) new Setting(this.containerEl)
.setName("Primary duration format") .setName("Primary duration format")
.setDesc( .setDesc("Format to show durations for the current and total timers")
"Format to show durations for the current and total timers"
)
.addDropdown((t) => { .addDropdown((t) => {
t.addOptions({ t.addOptions({
[DurationFormat.LONG]: [DurationFormat.LONG]: "Long - Format including all units (1h 30m 25s)",
"Long - Format including all units (1h 30m 25s)", [DurationFormat.SHORT]: "Short - Format just including hours (1.5h)",
[DurationFormat.SHORT]: [DurationFormat.DECIMAL]: "Decimal - Short format without units (1.5)",
"Short - Format just including hours (1.5h)",
[DurationFormat.DECIMAL]:
"Decimal - Short format without units (1.5)",
}); });
t.setValue(String(settings.primaryDurationFormat)); t.setValue(String(settings.primaryDurationFormat));
t.onChange((v) => { t.onChange((v) => {
@ -80,18 +73,13 @@ export class TimekeepSettingsTab extends PluginSettingTab {
new Setting(this.containerEl) new Setting(this.containerEl)
.setName("Secondary duration format") .setName("Secondary duration format")
.setDesc( .setDesc("Format to show a second durations under the current and total timers")
"Format to show a second durations under the current and total timers"
)
.addDropdown((t) => { .addDropdown((t) => {
t.addOptions({ t.addOptions({
[DurationFormat.LONG]: [DurationFormat.LONG]: "Long - Format including all units (1h 30m 25s)",
"Long - Format including all units (1h 30m 25s)", [DurationFormat.SHORT]: "Short - Format just including hours (1.5h)",
[DurationFormat.SHORT]: [DurationFormat.DECIMAL]: "Decimal - Short format without units (1.5)",
"Short - Format just including hours (1.5h)",
[DurationFormat.DECIMAL]:
"Decimal - Short format without units (1.5)",
[DurationFormat.NONE]: "None - No time is displayed", [DurationFormat.NONE]: "None - No time is displayed",
}); });
t.setValue(String(settings.secondaryDurationFormat)); t.setValue(String(settings.secondaryDurationFormat));
@ -105,9 +93,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
new Setting(this.containerEl) new Setting(this.containerEl)
.setName("Sort order") .setName("Sort order")
.setDesc( .setDesc("How entries should be sorted both when viewing and exporting")
"How entries should be sorted both when viewing and exporting"
)
.addDropdown((t) => { .addDropdown((t) => {
t.addOptions({ t.addOptions({
@ -175,12 +161,9 @@ export class TimekeepSettingsTab extends PluginSettingTab {
.addDropdown((t) => { .addDropdown((t) => {
t.addOptions({ t.addOptions({
[DurationFormat.LONG]: [DurationFormat.LONG]: "Long - Format including all units (1h 30m 25s)",
"Long - Format including all units (1h 30m 25s)", [DurationFormat.SHORT]: "Short - Format just including hours (1.5h)",
[DurationFormat.SHORT]: [DurationFormat.DECIMAL]: "Decimal - Short format without units (1.5)",
"Short - Format just including hours (1.5h)",
[DurationFormat.DECIMAL]:
"Decimal - Short format without units (1.5)",
}); });
t.setValue(String(settings.exportDurationFormat)); t.setValue(String(settings.exportDurationFormat));
t.onChange((v) => { t.onChange((v) => {
@ -219,9 +202,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
t.setValue(String(settings.pdfFootnote)); t.setValue(String(settings.pdfFootnote));
t.onChange((v) => { t.onChange((v) => {
// Only use a custom format if the value is not blank // Only use a custom format if the value is not blank
const newPdfFootnote = v.length const newPdfFootnote = v.length ? v : defaultSettings.pdfFootnote;
? v
: defaultSettings.pdfFootnote;
this.settingsStore.setState((currentValue) => ({ this.settingsStore.setState((currentValue) => ({
...currentValue, ...currentValue,
@ -237,10 +218,8 @@ export class TimekeepSettingsTab extends PluginSettingTab {
.addDropdown((t) => { .addDropdown((t) => {
t.addOptions({ t.addOptions({
[PdfExportBehavior.NONE]: "Do nothing", [PdfExportBehavior.NONE]: "Do nothing",
[PdfExportBehavior.OPEN_FILE]: [PdfExportBehavior.OPEN_FILE]: "Open exported file with default app",
"Open exported file with default app", [PdfExportBehavior.OPEN_PATH]: "Open directory containing the exported file",
[PdfExportBehavior.OPEN_PATH]:
"Open directory containing the exported file",
}); });
t.setValue(String(settings.pdfExportBehavior)); t.setValue(String(settings.pdfExportBehavior));
t.onChange((v) => { t.onChange((v) => {
@ -269,9 +248,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
t.setValue(String(settings.pdfDateFormat)); t.setValue(String(settings.pdfDateFormat));
t.onChange((v) => { t.onChange((v) => {
// Only use a custom format if the value is not blank // Only use a custom format if the value is not blank
const newPdfDateFormat = v.length const newPdfDateFormat = v.length ? v : defaultSettings.pdfDateFormat;
? v
: defaultSettings.pdfDateFormat;
this.settingsStore.setState((currentValue) => ({ this.settingsStore.setState((currentValue) => ({
...currentValue, ...currentValue,
@ -298,9 +275,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
t.setValue(String(settings.pdfRowDateFormat)); t.setValue(String(settings.pdfRowDateFormat));
t.onChange((v) => { t.onChange((v) => {
// Only use a custom format if the value is not blank // Only use a custom format if the value is not blank
const newPdfRowDateFormat = v.length const newPdfRowDateFormat = v.length ? v : defaultSettings.pdfRowDateFormat;
? v
: defaultSettings.pdfRowDateFormat;
this.settingsStore.setState((currentValue) => ({ this.settingsStore.setState((currentValue) => ({
...currentValue, ...currentValue,
@ -334,9 +309,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
new Setting(this.containerEl) new Setting(this.containerEl)
.setName("CSV heading row") .setName("CSV heading row")
.setDesc( .setDesc("Whether to use the first row of generated CSV as a title row")
"Whether to use the first row of generated CSV as a title row"
)
.addToggle((t) => { .addToggle((t) => {
t.setValue(settings.csvTitle); t.setValue(settings.csvTitle);
t.onChange((v) => { t.onChange((v) => {
@ -355,9 +328,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
.addText((t) => { .addText((t) => {
t.setValue(String(settings.csvDelimiter)); t.setValue(String(settings.csvDelimiter));
t.onChange((v) => { t.onChange((v) => {
const newCsvDelimiter = v.length const newCsvDelimiter = v.length ? v : defaultSettings.csvDelimiter;
? v
: defaultSettings.csvDelimiter;
this.settingsStore.setState((currentValue) => ({ this.settingsStore.setState((currentValue) => ({
...currentValue, ...currentValue,
@ -371,9 +342,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
new Setting(this.containerEl) new Setting(this.containerEl)
.setName("Format copied JSON") .setName("Format copied JSON")
.setDesc( .setDesc("Whether to format the JSON contents before copying them to clipboard.")
"Whether to format the JSON contents before copying them to clipboard."
)
.addToggle((t) => { .addToggle((t) => {
t.setValue(settings.formatCopiedJSON); t.setValue(settings.formatCopiedJSON);
t.onChange((v) => { t.onChange((v) => {

View file

@ -24,32 +24,18 @@ describe("legacy settings compatibility conversion", () => {
}); });
test.each([ test.each([
[ [{ reverseSegmentOrder: true }, { sortOrder: SortOrder.REVERSE_INSERTION }],
{ reverseSegmentOrder: true },
{ sortOrder: SortOrder.REVERSE_INSERTION },
],
[{ reverseSegmentOrder: false }, { sortOrder: SortOrder.INSERTION }], [{ reverseSegmentOrder: false }, { sortOrder: SortOrder.INSERTION }],
[ [{ showDecimalHours: true }, { secondaryDurationFormat: DurationFormat.SHORT }],
{ showDecimalHours: true }, [{ showDecimalHours: false }, { secondaryDurationFormat: DurationFormat.NONE }],
{ secondaryDurationFormat: DurationFormat.SHORT },
],
[
{ showDecimalHours: false },
{ secondaryDurationFormat: DurationFormat.NONE },
],
])('for "%s" should expected "%s"', (legacySetting, expected) => { ])('for "%s" should expected "%s"', (legacySetting, expected) => {
// Check the legacy setting gets replaced with the new setting // Check the legacy setting gets replaced with the new setting
const partialSetting = Object.assign( const partialSetting = Object.assign({}, legacySetting) as TimekeepSettings;
{},
legacySetting
) as TimekeepSettings;
legacySettingsCompatibility(partialSetting); legacySettingsCompatibility(partialSetting);
expect(partialSetting).toStrictEqual(expected); expect(partialSetting).toStrictEqual(expected);
const setting = Object.assign({}, defaultSettings, legacySetting); const setting = Object.assign({}, defaultSettings, legacySetting);
legacySettingsCompatibility(setting); legacySettingsCompatibility(setting);
expect(setting).toStrictEqual( expect(setting).toStrictEqual(Object.assign({}, defaultSettings, expected));
Object.assign({}, defaultSettings, expected)
);
}); });
}); });

View file

@ -7,11 +7,7 @@
* @param linesAfter Number of lines after the codeblock * @param linesAfter Number of lines after the codeblock
* @returns The generated codeblock * @returns The generated codeblock
*/ */
export function createCodeBlock( export function createCodeBlock(json: string, linesBefore: number, linesAfter: number) {
json: string,
linesBefore: number,
linesAfter: number
) {
let output = ""; let output = "";
for (let i = 0; i < linesBefore; i++) { for (let i = 0; i < linesBefore; i++) {
output += "\n"; output += "\n";

View file

@ -5,119 +5,99 @@ import { stripEntryRuntimeData, stripEntriesRuntimeData } from "./schema";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
describe("createEntry", () => { describe("createEntry", () => {
it("creating entry should use current time", () => { it("creating entry should use current time", () => {
const currentTime = moment(); const currentTime = moment();
const entry = createEntry("Block 1", currentTime); const entry = createEntry("Block 1", currentTime);
expect(stripEntryRuntimeData(entry)).toStrictEqual({ expect(stripEntryRuntimeData(entry)).toStrictEqual({
name: "Block 1", name: "Block 1",
startTime: currentTime, startTime: currentTime,
endTime: null, endTime: null,
subEntries: null, subEntries: null,
}); });
}); });
it("each created entry should have a unique id", () => { it("each created entry should have a unique id", () => {
const currentTime = moment(); const currentTime = moment();
const entry1 = createEntry("Block 1", currentTime); const entry1 = createEntry("Block 1", currentTime);
const entry2 = createEntry("Block 2", currentTime); const entry2 = createEntry("Block 2", currentTime);
expect(entry1.id).not.toBe(entry2.id); expect(entry1.id).not.toBe(entry2.id);
}); });
}); });
describe("withEntry", () => { describe("withEntry", () => {
it("should add new entry to entries", async () => { it("should add new entry to entries", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_entry/addNewEntry"); await import("./__fixtures__/manipulating/adding_entry/addNewEntry");
const output = withEntry(input, "New Entry", currentTime); const output = withEntry(input, "New Entry", currentTime);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected) });
);
});
it("should generate block name when empty", async () => { it("should generate block name when empty", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_entry/addEmptyBlockName"); await import("./__fixtures__/manipulating/adding_entry/addEmptyBlockName");
const output1 = withEntry(input, "", currentTime); const output1 = withEntry(input, "", currentTime);
expect(stripEntriesRuntimeData(output1)).toEqual( expect(stripEntriesRuntimeData(output1)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
// Empty whitespace string should also count as an empty name // Empty whitespace string should also count as an empty name
const output2 = withEntry(input, " ".repeat(5), currentTime); const output2 = withEntry(input, " ".repeat(5), currentTime);
expect(stripEntriesRuntimeData(output2)).toEqual( expect(stripEntriesRuntimeData(output2)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected) });
);
});
it("should maintain existing entries when adding to a list", async () => { it("should maintain existing entries when adding to a list", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_entry/addEntryToList"); await import("./__fixtures__/manipulating/adding_entry/addEntryToList");
const output = withEntry(input, "New Test Entry", currentTime); const output = withEntry(input, "New Test Entry", currentTime);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected) });
);
});
}); });
describe("withSubEntry", () => { describe("withSubEntry", () => {
it("adding first entry should convert to group", async () => { it("adding first entry should convert to group", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_sub_entry/addConvertToGroup"); await import("./__fixtures__/manipulating/adding_sub_entry/addConvertToGroup");
const output = withSubEntry(input, "New Entry", currentTime); const output = withSubEntry(input, "New Entry", currentTime);
expect(stripEntryRuntimeData(output)).toEqual( expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
stripEntryRuntimeData(expected) });
);
});
it("adding first entry for folder should populate subentries", async () => { it("adding first entry for folder should populate subentries", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_sub_entry/addFolderPopulateSubEntries"); await import("./__fixtures__/manipulating/adding_sub_entry/addFolderPopulateSubEntries");
const output = withSubEntry(input, "New Entry", currentTime); const output = withSubEntry(input, "New Entry", currentTime);
expect(stripEntryRuntimeData(output)).toEqual( expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
stripEntryRuntimeData(expected) });
);
});
it("adding first entry for folder should extend subentries", async () => { it("adding first entry for folder should extend subentries", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_sub_entry/addFolderExtendSubEntries"); await import("./__fixtures__/manipulating/adding_sub_entry/addFolderExtendSubEntries");
const output = withSubEntry(input, "New Entry 2", currentTime); const output = withSubEntry(input, "New Entry 2", currentTime);
expect(stripEntryRuntimeData(output)).toEqual( expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
stripEntryRuntimeData(expected) });
);
});
it("adding to group should extend sub entries", async () => { it("adding to group should extend sub entries", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_sub_entry/addToGroupExtendSubEntries"); await import("./__fixtures__/manipulating/adding_sub_entry/addToGroupExtendSubEntries");
const output = withSubEntry(input, "New Entry", currentTime); const output = withSubEntry(input, "New Entry", currentTime);
expect(stripEntryRuntimeData(output)).toEqual( expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
stripEntryRuntimeData(expected) });
);
});
it("empty name should generate a part name (single)", async () => { it("empty name should generate a part name (single)", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameSingle"); await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameSingle");
const output = withSubEntry(input, "", currentTime); const output = withSubEntry(input, "", currentTime);
expect(stripEntryRuntimeData(output)).toEqual( expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
stripEntryRuntimeData(expected) });
);
});
it("empty name should generate a part name (group)", async () => { it("empty name should generate a part name (group)", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameGroup"); await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameGroup");
const output = withSubEntry(input, "", currentTime); const output = withSubEntry(input, "", currentTime);
expect(stripEntryRuntimeData(output)).toEqual( expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
stripEntryRuntimeData(expected) });
);
});
}); });

View file

@ -29,11 +29,7 @@ export function createEntry(name: string, startTime: Moment): TimeEntry {
* @param startTime The start time of the new entry * @param startTime The start time of the new entry
* @returns The new collection of entries * @returns The new collection of entries
*/ */
export function withEntry( export function withEntry(entries: TimeEntry[], name: string, startTime: Moment): TimeEntry[] {
entries: TimeEntry[],
name: string,
startTime: Moment
): TimeEntry[] {
const entryName = getEntryName(name, entries); const entryName = getEntryName(name, entries);
return [...entries, createEntry(entryName, startTime)]; return [...entries, createEntry(entryName, startTime)];
} }
@ -66,11 +62,7 @@ function getEntryName(name: string, entries: TimeEntry[]) {
* @param startTime The start time for the new entry * @param startTime The start time for the new entry
* @returns The updated/created entry * @returns The updated/created entry
*/ */
export function withSubEntry( export function withSubEntry(parent: TimeEntry, name: string, startTime: Moment): TimeEntry {
parent: TimeEntry,
name: string,
startTime: Moment
): TimeEntry {
const groupEntry = makeGroupEntry(parent); const groupEntry = makeGroupEntry(parent);
const entryName = getSubEntryName(name, groupEntry); const entryName = getSubEntryName(name, groupEntry);
const newEntry = createEntry(entryName, startTime); const newEntry = createEntry(entryName, startTime);

View file

@ -2,11 +2,11 @@ import moment from "moment";
import { Timekeep, stripTimekeepRuntimeData } from "./schema"; import { Timekeep, stripTimekeepRuntimeData } from "./schema";
import { import {
load, load,
LoadError, LoadError,
LoadSuccess, LoadSuccess,
replaceTimekeepCodeblock, replaceTimekeepCodeblock,
extractTimekeepCodeblocks, extractTimekeepCodeblocks,
} from "./parser"; } from "./parser";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
@ -19,183 +19,168 @@ import { expect, it, describe } from "vitest";
* @param linesAfter Number of lines after the codeblock * @param linesAfter Number of lines after the codeblock
* @returns The generated codeblock * @returns The generated codeblock
*/ */
const createCodeBlock = ( const createCodeBlock = (json: string, linesBefore: number, linesAfter: number) => {
json: string, let output = "";
linesBefore: number, for (let i = 0; i < linesBefore; i++) {
linesAfter: number output += "\n";
) => { }
let output = ""; output += "```timekeep\n";
for (let i = 0; i < linesBefore; i++) { output += json;
output += "\n"; output += "\n```";
} for (let i = 0; i < linesAfter; i++) {
output += "```timekeep\n"; output += "\n";
output += json; }
output += "\n```"; return output;
for (let i = 0; i < linesAfter; i++) {
output += "\n";
}
return output;
}; };
describe("extracting code blocks", () => { describe("extracting code blocks", () => {
it("should extract codeblock contents", async () => { it("should extract codeblock contents", async () => {
const { text, inputTimekeep1, inputTimekeep2 } = const { text, inputTimekeep1, inputTimekeep2 } =
await import("./__fixtures__/extracting/codeblockContents"); await import("./__fixtures__/extracting/codeblockContents");
const output = extractTimekeepCodeblocks(text); const output = extractTimekeepCodeblocks(text);
expect(stripTimekeepRuntimeData(output[0])).toStrictEqual( expect(stripTimekeepRuntimeData(output[0])).toStrictEqual(
stripTimekeepRuntimeData(inputTimekeep1) stripTimekeepRuntimeData(inputTimekeep1)
); );
expect(stripTimekeepRuntimeData(output[1])).toStrictEqual( expect(stripTimekeepRuntimeData(output[1])).toStrictEqual(
stripTimekeepRuntimeData(inputTimekeep2) stripTimekeepRuntimeData(inputTimekeep2)
); );
expect(output.length).toBe(2); expect(output.length).toBe(2);
}); });
it("should ignore codeblocks that are not closed", async () => { it("should ignore codeblocks that are not closed", async () => {
const { text, inputTimekeep1 } = const { text, inputTimekeep1 } =
await import("./__fixtures__/extracting/unclosedCodeBlock"); await import("./__fixtures__/extracting/unclosedCodeBlock");
const output = extractTimekeepCodeblocks(text); const output = extractTimekeepCodeblocks(text);
expect(stripTimekeepRuntimeData(output[0])).toStrictEqual( expect(stripTimekeepRuntimeData(output[0])).toStrictEqual(
stripTimekeepRuntimeData(inputTimekeep1) stripTimekeepRuntimeData(inputTimekeep1)
); );
expect(output.length).toBe(1); expect(output.length).toBe(1);
}); });
}); });
describe("replacing content", () => { describe("replacing content", () => {
it("should replace codeblock contents", () => { it("should replace codeblock contents", () => {
const lineStart = 4; // Line the codeblock should start on const lineStart = 4; // Line the codeblock should start on
const lineEnd = lineStart + 2; // Line the codeblock should end on const lineEnd = lineStart + 2; // Line the codeblock should end on
// Input data to replace // Input data to replace
const input = createCodeBlock( const input = createCodeBlock(
`{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`, `{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
4, 4,
4 4
); );
// Timekeep with a renamed block // Timekeep with a renamed block
const inputTimekeep: Timekeep = { const inputTimekeep: Timekeep = {
entries: [ entries: [
{ {
id: "49b99108-b1ad-4355-baa9-89c49c342be2", id: "49b99108-b1ad-4355-baa9-89c49c342be2",
name: "Block 2", name: "Block 2",
startTime: moment("2024-03-17T01:33:51.630Z"), startTime: moment("2024-03-17T01:33:51.630Z"),
endTime: moment("2024-03-17T01:33:55.151Z"), endTime: moment("2024-03-17T01:33:55.151Z"),
subEntries: null, subEntries: null,
}, },
], ],
}; };
// Value with the renamed block // Value with the renamed block
const expected = createCodeBlock( const expected = createCodeBlock(
`{"entries":[{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`, `{"entries":[{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
4, 4,
4 4
); );
const output = replaceTimekeepCodeblock( const output = replaceTimekeepCodeblock(inputTimekeep, input, lineStart, lineEnd);
inputTimekeep,
input,
lineStart,
lineEnd
);
expect(output).toBe(expected); expect(output).toBe(expected);
}); });
it("should fail if codeblock is missing", () => { it("should fail if codeblock is missing", () => {
const input = createCodeBlock("", 4, 4); const input = createCodeBlock("", 4, 4);
// Start not code fences // Start not code fences
expect(() => expect(() => replaceTimekeepCodeblock({ entries: [] }, input, 2, 4)).toThrow();
replaceTimekeepCodeblock({ entries: [] }, input, 2, 4)
).toThrow();
// End not code fences // End not code fences
expect(() => expect(() => replaceTimekeepCodeblock({ entries: [] }, input, 4, 8)).toThrow();
replaceTimekeepCodeblock({ entries: [] }, input, 4, 8) });
).toThrow();
});
}); });
describe("loading timekeep", () => { describe("loading timekeep", () => {
it("should give empty timekeep when given empty string", () => { it("should give empty timekeep when given empty string", () => {
const result = load(""); const result = load("");
expect(result.success).toBe(true); expect(result.success).toBe(true);
const successResult = result as LoadSuccess; const successResult = result as LoadSuccess;
// Ensure the contents match // Ensure the contents match
expect(successResult.timekeep).toEqual({ entries: [] }); expect(successResult.timekeep).toEqual({ entries: [] });
}); });
it("should load valid timekeep successfully", () => { it("should load valid timekeep successfully", () => {
const data = `{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null},{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":null,"subEntries":null},{"name":"Non Started Block","startTime":null,"endTime":null,"subEntries":null}]}`; const data = `{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null},{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":null,"subEntries":null},{"name":"Non Started Block","startTime":null,"endTime":null,"subEntries":null}]}`;
const expected = { const expected = {
entries: [ entries: [
{ {
name: "Block 1", name: "Block 1",
startTime: moment("2024-03-17T01:33:51.630Z"), startTime: moment("2024-03-17T01:33:51.630Z"),
endTime: moment("2024-03-17T01:33:55.151Z"), endTime: moment("2024-03-17T01:33:55.151Z"),
subEntries: null, subEntries: null,
}, },
{ {
name: "Block 2", name: "Block 2",
startTime: moment("2024-03-17T01:33:51.630Z"), startTime: moment("2024-03-17T01:33:51.630Z"),
endTime: null, endTime: null,
subEntries: null, subEntries: null,
}, },
{ {
name: "Non Started Block", name: "Non Started Block",
startTime: null, startTime: null,
endTime: null, endTime: null,
subEntries: null, subEntries: null,
}, },
], ],
}; };
const result = load(data); const result = load(data);
expect(result.success).toBe(true); expect(result.success).toBe(true);
const successResult = result as LoadSuccess; const successResult = result as LoadSuccess;
// Ensure the contents match // Ensure the contents match
expect(stripTimekeepRuntimeData(successResult.timekeep)).toEqual( expect(stripTimekeepRuntimeData(successResult.timekeep)).toEqual(expected);
expected });
);
});
it("should give error on invalid timekeep (JSON)", () => { it("should give error on invalid timekeep (JSON)", () => {
const data = "{"; const data = "{";
const result = load(data); const result = load(data);
expect(result.success).toBe(false); expect(result.success).toBe(false);
const errorResult = result as LoadError; const errorResult = result as LoadError;
expect(errorResult.error).toBe("Failed to parse timekeep JSON"); expect(errorResult.error).toBe("Failed to parse timekeep JSON");
}); });
it("should tolerate a timekeep with leading or trailing whitespaces", () => { it("should tolerate a timekeep with leading or trailing whitespaces", () => {
const input = ` const input = `
\`\`\`timekeep \`\`\`timekeep
\`\`\` \`\`\`
`; `;
// Start not code fences // Start not code fences
replaceTimekeepCodeblock({ entries: [] }, input, 1, 2); replaceTimekeepCodeblock({ entries: [] }, input, 1, 2);
}); });
it("should give error on invalid timekeep (validation)", () => { it("should give error on invalid timekeep (validation)", () => {
const data = `{"entries":[{"startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`; const data = `{"entries":[{"startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`;
const result = load(data); const result = load(data);
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
}); });

View file

@ -1,9 +1,5 @@
import { isEmptyString } from "@/utils"; import { isEmptyString } from "@/utils";
import { import { TIMEKEEP, Timekeep, stripTimekeepRuntimeData } from "@/timekeep/schema";
TIMEKEEP,
Timekeep,
stripTimekeepRuntimeData,
} from "@/timekeep/schema";
export type LoadResult = LoadSuccess | LoadError; export type LoadResult = LoadSuccess | LoadError;
@ -18,40 +14,40 @@ export type LoadError = { success: false; error: string };
* @return The load result * @return The load result
*/ */
export function load(value: string): LoadResult { export function load(value: string): LoadResult {
// Empty string should create an empty timekeep // Empty string should create an empty timekeep
if (isEmptyString(value)) { if (isEmptyString(value)) {
return { success: true, timekeep: { entries: [] } }; return { success: true, timekeep: { entries: [] } };
} }
// Load the JSON value // Load the JSON value
let parsedValue: unknown; let parsedValue: unknown;
try { try {
parsedValue = JSON.parse(value); parsedValue = JSON.parse(value);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return { return {
success: false, success: false,
error: "Failed to parse timekeep JSON", error: "Failed to parse timekeep JSON",
}; };
} }
// Parse the data against the schema // Parse the data against the schema
const timekeepResult = TIMEKEEP.safeParse(parsedValue); const timekeepResult = TIMEKEEP.safeParse(parsedValue);
if (!timekeepResult.success) { if (!timekeepResult.success) {
return { return {
success: false, success: false,
error: timekeepResult.error.toString(), error: timekeepResult.error.toString(),
}; };
} }
const timekeep = timekeepResult.data; const timekeep = timekeepResult.data;
return { success: true, timekeep }; return { success: true, timekeep };
} }
export interface TimekeepWithPosition { export interface TimekeepWithPosition {
timekeep: Timekeep; timekeep: Timekeep;
startLine: number; startLine: number;
endLine: number; endLine: number;
} }
/** /**
@ -63,45 +59,41 @@ export interface TimekeepWithPosition {
* @param value The file text contents * @param value The file text contents
* @returns The extracted timekeep blocks * @returns The extracted timekeep blocks
*/ */
export function extractTimekeepCodeblocksWithPosition( export function extractTimekeepCodeblocksWithPosition(value: string): TimekeepWithPosition[] {
value: string const out: TimekeepWithPosition[] = [];
): TimekeepWithPosition[] { const lines = value.replace("\n\r", "\n").split("\n");
const out: TimekeepWithPosition[] = [];
const lines = value.replace("\n\r", "\n").split("\n");
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const startLine = lines[i]; const startLine = lines[i];
// Skip lines till a timekeep block is found // Skip lines till a timekeep block is found
if (!startLine.trim().startsWith("```timekeep")) { if (!startLine.trim().startsWith("```timekeep")) {
continue; continue;
} }
// Find end of codeblock // Find end of codeblock
const endLineIndex = lines.findIndex( const endLineIndex = lines.findIndex((line, index) => index > i && line.trim() === "```");
(line, index) => index > i && line.trim() === "```"
);
if (endLineIndex === -1) { if (endLineIndex === -1) {
continue; continue;
} }
let content = ""; let content = "";
for (let lineIndex = i + 1; lineIndex < endLineIndex; lineIndex++) { for (let lineIndex = i + 1; lineIndex < endLineIndex; lineIndex++) {
content += lines[lineIndex] + "\n"; content += lines[lineIndex] + "\n";
} }
const result = load(content); const result = load(content);
if (result.success) { if (result.success) {
out.push({ out.push({
timekeep: result.timekeep, timekeep: result.timekeep,
startLine: i, startLine: i,
endLine: endLineIndex, endLine: endLineIndex,
}); });
} }
} }
return out; return out;
} }
/** /**
@ -112,11 +104,11 @@ export function extractTimekeepCodeblocksWithPosition(
* @returns The extracted timekeep blocks * @returns The extracted timekeep blocks
*/ */
export function extractTimekeepCodeblocks(value: string): Timekeep[] { export function extractTimekeepCodeblocks(value: string): Timekeep[] {
return ( return (
extractTimekeepCodeblocksWithPosition(value) extractTimekeepCodeblocksWithPosition(value)
// //
.map((value) => value.timekeep) .map((value) => value.timekeep)
); );
} }
/** /**
@ -124,37 +116,37 @@ export function extractTimekeepCodeblocks(value: string): Timekeep[] {
* a file returning the modified contents to be saved * a file returning the modified contents to be saved
*/ */
export function replaceTimekeepCodeblock( export function replaceTimekeepCodeblock(
timekeep: Timekeep, timekeep: Timekeep,
content: string, content: string,
lineStart: number, lineStart: number,
lineEnd: number lineEnd: number
): string { ): string {
const timekeepJSON = JSON.stringify(stripTimekeepRuntimeData(timekeep)); const timekeepJSON = JSON.stringify(stripTimekeepRuntimeData(timekeep));
// The actual JSON is the line after the code block start // The actual JSON is the line after the code block start
const contentStart = lineStart + 1; const contentStart = lineStart + 1;
const contentLength = lineEnd - contentStart; const contentLength = lineEnd - contentStart;
// Split the content into lines // Split the content into lines
const lines = content.split("\n"); const lines = content.split("\n");
// Sanity checks to prevent overriding content // Sanity checks to prevent overriding content
if (!lines[lineStart].trim().startsWith("```")) { if (!lines[lineStart].trim().startsWith("```")) {
throw new Error( throw new Error(
"Content timekeep out of sync, line number for codeblock start doesn't match: " + "Content timekeep out of sync, line number for codeblock start doesn't match: " +
content[lineStart] content[lineStart]
); );
} }
if (!lines[lineEnd].trim().startsWith("```")) { if (!lines[lineEnd].trim().startsWith("```")) {
throw new Error( throw new Error(
"Content timekeep out of sync, line number for codeblock end doesn't match" + "Content timekeep out of sync, line number for codeblock end doesn't match" +
content[lineEnd] content[lineEnd]
); );
} }
// Splice the new JSON content in between the codeblock, removing the old codeblock lines // Splice the new JSON content in between the codeblock, removing the old codeblock lines
lines.splice(contentStart, contentLength, timekeepJSON); lines.splice(contentStart, contentLength, timekeepJSON);
return lines.join("\n"); return lines.join("\n");
} }

View file

@ -1,169 +1,164 @@
import { import {
getEntryById, getEntryById,
isKeepRunning, isKeepRunning,
getPathToEntry, getPathToEntry,
isEntryRunning, isEntryRunning,
getRunningEntry, getRunningEntry,
getEntryDuration, getEntryDuration,
getTotalDuration, getTotalDuration,
} from "./queries"; } from "./queries";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
describe("getEntryById", () => { describe("getEntryById", () => {
it("find top level entry", async () => { it("find top level entry", async () => {
const { input, targetEntry, targetEntryId } = const { input, targetEntry, targetEntryId } =
await import("./__fixtures__/checking/findEntryById"); await import("./__fixtures__/checking/findEntryById");
const output = getEntryById(targetEntryId, input); const output = getEntryById(targetEntryId, input);
expect(output).toEqual(targetEntry); expect(output).toEqual(targetEntry);
}); });
it("find nested entry", async () => { it("find nested entry", async () => {
const { input, targetEntry, targetEntryId } = const { input, targetEntry, targetEntryId } =
await import("./__fixtures__/checking/findEntryByIdNested"); await import("./__fixtures__/checking/findEntryByIdNested");
const output = getEntryById(targetEntryId, input); const output = getEntryById(targetEntryId, input);
expect(output).toEqual(targetEntry); expect(output).toEqual(targetEntry);
}); });
it("find entry non existent", async () => { it("find entry non existent", async () => {
const { input, targetEntryId } = const { input, targetEntryId } =
await import("./__fixtures__/checking/findEntryByIdMissing"); await import("./__fixtures__/checking/findEntryByIdMissing");
const output = getEntryById(targetEntryId, input); const output = getEntryById(targetEntryId, input);
expect(output).toBeUndefined(); expect(output).toBeUndefined();
}); });
}); });
describe("getPathToEntry", () => { describe("getPathToEntry", () => {
it("path not found", async () => { it("path not found", async () => {
const { targetEntry, entries, expected } = const { targetEntry, entries, expected } = await import("./__fixtures__/path/pathNotFound");
await import("./__fixtures__/path/pathNotFound"); const output = getPathToEntry(entries, targetEntry);
const output = getPathToEntry(entries, targetEntry); expect(output).toEqual(expected);
expect(output).toEqual(expected); });
});
it("top level path found", () => {}); it("top level path found", () => {});
it("deep path found", () => {}); it("deep path found", () => {});
it("should find running entry path", async () => { it("should find running entry path", async () => {
const { input, runningEntry, path } = const { input, runningEntry, path } =
await import("./__fixtures__/checking/findRunningEntryPath"); await import("./__fixtures__/checking/findRunningEntryPath");
const output = getPathToEntry(input, runningEntry); const output = getPathToEntry(input, runningEntry);
expect(output).toEqual(path); expect(output).toEqual(path);
}); });
}); });
describe("isEntryRunning", () => { describe("isEntryRunning", () => {
it("should determine entry running state", async () => { it("should determine entry running state", async () => {
const { running, notRunning } = const { running, notRunning } = await import("./__fixtures__/checking/runningState");
await import("./__fixtures__/checking/runningState");
expect(isEntryRunning(running)).toBe(true); expect(isEntryRunning(running)).toBe(true);
expect(isEntryRunning(notRunning)).toBe(false); expect(isEntryRunning(notRunning)).toBe(false);
}); });
it("should determine entry running state (nested)", async () => { it("should determine entry running state (nested)", async () => {
const { runningNested, stoppedNested } = const { runningNested, stoppedNested } =
await import("./__fixtures__/checking/runningState"); await import("./__fixtures__/checking/runningState");
expect(isEntryRunning(runningNested)).toBe(true); expect(isEntryRunning(runningNested)).toBe(true);
expect(isEntryRunning(stoppedNested)).toBe(false); expect(isEntryRunning(stoppedNested)).toBe(false);
}); });
}); });
describe("getRunningEntry", () => { describe("getRunningEntry", () => {
it("should find running entry", async () => { it("should find running entry", async () => {
const { input, runningEntry } = const { input, runningEntry } =
await import("./__fixtures__/checking/shouldFindRunningEntry"); await import("./__fixtures__/checking/shouldFindRunningEntry");
const output = getRunningEntry(input); const output = getRunningEntry(input);
expect(output).toBe(runningEntry); expect(output).toBe(runningEntry);
}); });
it("should find nested running entry", async () => { it("should find nested running entry", async () => {
const { input, runningEntry } = const { input, runningEntry } =
await import("./__fixtures__/checking/shouldFindRunningEntryNested"); await import("./__fixtures__/checking/shouldFindRunningEntryNested");
const output = getRunningEntry(input); const output = getRunningEntry(input);
expect(output).toBe(runningEntry); expect(output).toBe(runningEntry);
}); });
it("should not find running entry", async () => { it("should not find running entry", async () => {
const { input } = const { input } = await import("./__fixtures__/checking/shouldNotFindRunningEntry");
await import("./__fixtures__/checking/shouldNotFindRunningEntry");
const output = getRunningEntry(input); const output = getRunningEntry(input);
expect(output).toBe(null); expect(output).toBe(null);
}); });
}); });
describe("isKeepRunning", () => { describe("isKeepRunning", () => {
it("should show keep running", async () => { it("should show keep running", async () => {
const { input } = const { input } = await import("./__fixtures__/checking/shouldBeRunning");
await import("./__fixtures__/checking/shouldBeRunning");
expect(isKeepRunning(input)).toBe(true); expect(isKeepRunning(input)).toBe(true);
}); });
it("should show keep not running", async () => { it("should show keep not running", async () => {
const { input } = const { input } = await import("./__fixtures__/checking/shouldNotBeRunning");
await import("./__fixtures__/checking/shouldNotBeRunning");
expect(isKeepRunning(input)).toBe(false); expect(isKeepRunning(input)).toBe(false);
}); });
}); });
describe("getEntryDuration", () => { describe("getEntryDuration", () => {
it("should get entry duration", async () => { it("should get entry duration", async () => {
const { input, currentTime, durationMs } = const { input, currentTime, durationMs } =
await import("./__fixtures__/duration/shouldGetEntryDuration"); await import("./__fixtures__/duration/shouldGetEntryDuration");
const output = getEntryDuration(input, currentTime); const output = getEntryDuration(input, currentTime);
expect(output).toBe(durationMs); expect(output).toBe(durationMs);
}); });
it("duration of non started entry should be zero", async () => { it("duration of non started entry should be zero", async () => {
const { input, currentTime, durationMs } = const { input, currentTime, durationMs } =
await import("./__fixtures__/duration/nonStartedZeroDuration"); await import("./__fixtures__/duration/nonStartedZeroDuration");
const output = getEntryDuration(input, currentTime); const output = getEntryDuration(input, currentTime);
expect(output).toBe(durationMs); expect(output).toBe(durationMs);
}); });
it("duration should include children", async () => { it("duration should include children", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/duration/durationIncludeChildren"); await import("./__fixtures__/duration/durationIncludeChildren");
const output = getEntryDuration(input, currentTime); const output = getEntryDuration(input, currentTime);
expect(output).toBe(expected); expect(output).toBe(expected);
}); });
it("duration should use current as end for unfinished entries", async () => { it("duration should use current as end for unfinished entries", async () => {
const { input, endTime, durationMs } = const { input, endTime, durationMs } =
await import("./__fixtures__/duration/currentEndUnfinished"); await import("./__fixtures__/duration/currentEndUnfinished");
const output = getEntryDuration(input, endTime); const output = getEntryDuration(input, endTime);
expect(output).toBe(durationMs); expect(output).toBe(durationMs);
}); });
}); });
describe("getTotalDuration", () => { describe("getTotalDuration", () => {
it("should get total duration", async () => { it("should get total duration", async () => {
const { input, currentTime, expected } = const { input, currentTime, expected } =
await import("./__fixtures__/duration/totalDuration"); await import("./__fixtures__/duration/totalDuration");
const output = getTotalDuration(input, currentTime); const output = getTotalDuration(input, currentTime);
expect(output).toBe(expected); expect(output).toBe(expected);
}); });
}); });

View file

@ -8,10 +8,7 @@ import { Timekeep, TimeEntry } from "@/timekeep/schema";
* @param entries List of entries to search * @param entries List of entries to search
* @returns The found entry or undefined * @returns The found entry or undefined
*/ */
export function getEntryById( export function getEntryById(entryId: string, entries: TimeEntry[]): TimeEntry | undefined {
entryId: string,
entries: TimeEntry[]
): TimeEntry | undefined {
for (const entry of entries) { for (const entry of entries) {
if (entry.id === entryId) { if (entry.id === entryId) {
return entry; return entry;
@ -127,10 +124,7 @@ export function isKeepRunning(timekeep: Timekeep): boolean {
* @param currentTime The current time to use for unfinished entries * @param currentTime The current time to use for unfinished entries
* @returns The duration in milliseconds * @returns The duration in milliseconds
*/ */
export function getEntryDuration( export function getEntryDuration(entry: TimeEntry, currentTime: Moment): number {
entry: TimeEntry,
currentTime: Moment
): number {
if (entry.subEntries !== null) { if (entry.subEntries !== null) {
return getTotalDuration(entry.subEntries, currentTime); return getTotalDuration(entry.subEntries, currentTime);
} }
@ -153,13 +147,9 @@ export function getEntryDuration(
* @param currentTime The current time to use for unfinished entries * @param currentTime The current time to use for unfinished entries
* @returns The total duration in milliseconds * @returns The total duration in milliseconds
*/ */
export function getTotalDuration( export function getTotalDuration(entries: TimeEntry[], currentTime: Moment): number {
entries: TimeEntry[],
currentTime: Moment
): number {
return entries.reduce( return entries.reduce(
(totalDuration, entry) => (totalDuration, entry) => totalDuration + getEntryDuration(entry, currentTime),
totalDuration + getEntryDuration(entry, currentTime),
0 0
); );
} }

View file

@ -55,18 +55,15 @@ const TIME_ENTRY_GROUP_BASE = z.object({
}); });
// Schema for a time entry group // Schema for a time entry group
const TIME_ENTRY_GROUP: z.ZodType<TimeEntryGroup> = const TIME_ENTRY_GROUP: z.ZodType<TimeEntryGroup> = TIME_ENTRY_GROUP_BASE.extend({
TIME_ENTRY_GROUP_BASE.extend({ subEntries: z.lazy(() => z.array(z.union([TIME_ENTRY_SINGLE, TIME_ENTRY_GROUP]))),
subEntries: z.lazy(() => })
z.array(z.union([TIME_ENTRY_SINGLE, TIME_ENTRY_GROUP])) // At runtime a unique ID is inserted
), /* istanbul ignore next */
}) .transform((entry) => ({
// At runtime a unique ID is inserted ...entry,
/* istanbul ignore next */ id: uuid(),
.transform((entry) => ({ }));
...entry,
id: uuid(),
}));
// Schema for time entries // Schema for time entries
const TIME_ENTRY = z.union([TIME_ENTRY_SINGLE, TIME_ENTRY_GROUP]); const TIME_ENTRY = z.union([TIME_ENTRY_SINGLE, TIME_ENTRY_GROUP]);

View file

@ -1,16 +1,10 @@
import { import { SortOrder, UnstartedOrder, defaultSettings, TimekeepSettings } from "@/settings";
SortOrder,
UnstartedOrder,
defaultSettings,
TimekeepSettings,
} from "@/settings";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
import { getEntriesSorted } from "./sort"; import { getEntriesSorted } from "./sort";
describe("getEntriesSorted", () => { describe("getEntriesSorted", () => {
it("should be in reverse order", async () => { it("should be in reverse order", async () => {
const { input, expected } = const { input, expected } = await import("./__fixtures__/ordering/reverseOrder");
await import("./__fixtures__/ordering/reverseOrder");
const settings: TimekeepSettings = defaultSettings; const settings: TimekeepSettings = defaultSettings;
settings.sortOrder = SortOrder.REVERSE_INSERTION; settings.sortOrder = SortOrder.REVERSE_INSERTION;
@ -20,8 +14,7 @@ describe("getEntriesSorted", () => {
}); });
it("should be in newest first order", async () => { it("should be in newest first order", async () => {
const { input, expected } = const { input, expected } = await import("./__fixtures__/ordering/newestFirstOrder");
await import("./__fixtures__/ordering/newestFirstOrder");
const settings: TimekeepSettings = defaultSettings; const settings: TimekeepSettings = defaultSettings;
settings.sortOrder = SortOrder.NEWEST_START; settings.sortOrder = SortOrder.NEWEST_START;
@ -31,8 +24,7 @@ describe("getEntriesSorted", () => {
}); });
it("should be in newest last order", async () => { it("should be in newest last order", async () => {
const { input, expected } = const { input, expected } = await import("./__fixtures__/ordering/newestLastOrder");
await import("./__fixtures__/ordering/newestLastOrder");
const settings: TimekeepSettings = defaultSettings; const settings: TimekeepSettings = defaultSettings;
settings.sortOrder = SortOrder.OLDEST_START; settings.sortOrder = SortOrder.OLDEST_START;
@ -42,8 +34,7 @@ describe("getEntriesSorted", () => {
}); });
it("should be in newest last order with nulls first", async () => { it("should be in newest last order with nulls first", async () => {
const { input, expected } = const { input, expected } = await import("./__fixtures__/ordering/newestLastNullsFirst");
await import("./__fixtures__/ordering/newestLastNullsFirst");
const settings: TimekeepSettings = defaultSettings; const settings: TimekeepSettings = defaultSettings;
settings.sortOrder = SortOrder.OLDEST_START; settings.sortOrder = SortOrder.OLDEST_START;
@ -54,8 +45,7 @@ describe("getEntriesSorted", () => {
}); });
it("order should not change", async () => { it("order should not change", async () => {
const { input, expected } = const { input, expected } = await import("./__fixtures__/ordering/orderShouldNotChange");
await import("./__fixtures__/ordering/orderShouldNotChange");
const settings: TimekeepSettings = defaultSettings; const settings: TimekeepSettings = defaultSettings;
settings.sortOrder = SortOrder.INSERTION; settings.sortOrder = SortOrder.INSERTION;

View file

@ -5,10 +5,7 @@ import { getStartTime } from "./queries";
type TimeEntryWithIndex = TimeEntry & { index: number }; type TimeEntryWithIndex = TimeEntry & { index: number };
type EntriesComparator = ( type EntriesComparator = (a: TimeEntryWithIndex, b: TimeEntryWithIndex) => number;
a: TimeEntryWithIndex,
b: TimeEntryWithIndex
) => number;
/** /**
* Provides a sorted copy of the provided entries list. * Provides a sorted copy of the provided entries list.
@ -20,10 +17,7 @@ type EntriesComparator = (
* @param settings The timekeep settings for which order to use * @param settings The timekeep settings for which order to use
* @returns The sorted entries list * @returns The sorted entries list
*/ */
export function getEntriesSorted( export function getEntriesSorted(entries: TimeEntry[], settings: TimekeepSettings): TimeEntry[] {
entries: TimeEntry[],
settings: TimekeepSettings
): TimeEntry[] {
// List order should be unchanged // List order should be unchanged
if (settings.sortOrder === SortOrder.INSERTION) { if (settings.sortOrder === SortOrder.INSERTION) {
return entries; return entries;
@ -56,9 +50,7 @@ function getEntriesSortedReverseInsertion(entries: TimeEntry[]): TimeEntry[] {
if (entry.subEntries !== null) { if (entry.subEntries !== null) {
return { return {
...entry, ...entry,
subEntries: getEntriesSortedReverseInsertion( subEntries: getEntriesSortedReverseInsertion(entry.subEntries),
entry.subEntries
),
}; };
} }
@ -86,10 +78,7 @@ function getEntriesSortedComparator(
if (entry.subEntries !== null) { if (entry.subEntries !== null) {
return { return {
...entry, ...entry,
subEntries: getEntriesSortedComparator( subEntries: getEntriesSortedComparator(entry.subEntries, comparator),
entry.subEntries,
comparator
),
index, index,
}; };
} }
@ -125,9 +114,7 @@ function createEntriesComparator(
// Sort newest when both have a start time // Sort newest when both have a start time
if (aStartTime && bStartTime) { if (aStartTime && bStartTime) {
return newest return newest ? bStartTime.diff(aStartTime) : aStartTime.diff(bStartTime);
? bStartTime.diff(aStartTime)
: aStartTime.diff(bStartTime);
} }
if (aStartTime) { if (aStartTime) {

View file

@ -13,9 +13,7 @@ describe("startNewEntry", () => {
expect(stoppedEntry).toBeDefined(); expect(stoppedEntry).toBeDefined();
expect(stoppedEntry!.endTime).not.toBeNull(); expect(stoppedEntry!.endTime).not.toBeNull();
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
it("starting a new entry should add a new entry", async () => { it("starting a new entry should add a new entry", async () => {
@ -23,9 +21,7 @@ describe("startNewEntry", () => {
await import("./__fixtures__/manipulating/start_entry/startShouldStopRunning"); await import("./__fixtures__/manipulating/start_entry/startShouldStopRunning");
const output = startNewEntry(name, currentTime, input); const output = startNewEntry(name, currentTime, input);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
}); });
@ -35,9 +31,7 @@ describe("startNewNestedEntry", () => {
await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry"); await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry");
const output = startNewNestedEntry(currentTime, targetEntry.id, input); const output = startNewNestedEntry(currentTime, targetEntry.id, input);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
it("starting a new entry within a folder should create a subentry", async () => { it("starting a new entry within a folder should create a subentry", async () => {
@ -45,9 +39,7 @@ describe("startNewNestedEntry", () => {
await import("./__fixtures__/manipulating/start_entry/startNestedFolderEntry"); await import("./__fixtures__/manipulating/start_entry/startNestedFolderEntry");
const output = startNewNestedEntry(currentTime, targetEntry.id, input); const output = startNewNestedEntry(currentTime, targetEntry.id, input);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
it("starting a new entry should stop any running entries", async () => { it("starting a new entry should stop any running entries", async () => {
@ -55,9 +47,7 @@ describe("startNewNestedEntry", () => {
await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry"); await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry");
const output = startNewNestedEntry(currentTime, targetEntry.id, input); const output = startNewNestedEntry(currentTime, targetEntry.id, input);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
it("starting a new entry should stop any running entries", async () => { it("starting a new entry should stop any running entries", async () => {
@ -65,9 +55,7 @@ describe("startNewNestedEntry", () => {
await import("./__fixtures__/manipulating/start_entry/startNestedNonExistent"); await import("./__fixtures__/manipulating/start_entry/startNestedNonExistent");
const output = startNewNestedEntry(currentTime, targetEntryId, input); const output = startNewNestedEntry(currentTime, targetEntryId, input);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
it("starting a new entry should stop any running entries", async () => { it("starting a new entry should stop any running entries", async () => {
@ -77,16 +65,12 @@ describe("startNewNestedEntry", () => {
const output = startNewNestedEntry(currentTime, targetEntry.id, input); const output = startNewNestedEntry(currentTime, targetEntry.id, input);
const outerEntry = output[0]; const outerEntry = output[0];
const stoppedEntry = outerEntry.subEntries?.find( const stoppedEntry = outerEntry.subEntries?.find((entry) => entry.id === stopped);
(entry) => entry.id === stopped
);
expect(stoppedEntry).toBeDefined(); expect(stoppedEntry).toBeDefined();
expect(stoppedEntry!.endTime).not.toBeNull(); expect(stoppedEntry!.endTime).not.toBeNull();
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
it("starting a new entry should add a new entry", async () => { it("starting a new entry should add a new entry", async () => {
@ -94,8 +78,6 @@ describe("startNewNestedEntry", () => {
await import("./__fixtures__/manipulating/start_entry/startNestedShouldStopRunning"); await import("./__fixtures__/manipulating/start_entry/startNestedShouldStopRunning");
const output = startNewNestedEntry(currentTime, targetEntry.id, input); const output = startNewNestedEntry(currentTime, targetEntry.id, input);
expect(stripEntriesRuntimeData(output)).toEqual( expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
stripEntriesRuntimeData(expected)
);
}); });
}); });

View file

@ -60,9 +60,5 @@ export function startNewNestedEntry(
} }
// If the entry hasn't been started then start it // If the entry hasn't been started then start it
return updateEntry( return updateEntry(entries, currentEntry.id, createEntry(currentEntry.name, currentTime));
entries,
currentEntry.id,
createEntry(currentEntry.name, currentTime)
);
} }

View file

@ -23,11 +23,7 @@ export function updateEntry(
if (entry.subEntries !== null) { if (entry.subEntries !== null) {
return { return {
...entry, ...entry,
subEntries: updateEntry( subEntries: updateEntry(entry.subEntries, previousEntryId, newEntry),
entry.subEntries,
previousEntryId,
newEntry
),
}; };
} }
@ -43,10 +39,7 @@ export function updateEntry(
* @param collapsed The collapse state * @param collapsed The collapse state
* @returns The new updated entry * @returns The new updated entry
*/ */
export function setEntryCollapsed( export function setEntryCollapsed(entry: TimeEntry, collapsed: boolean): TimeEntry {
entry: TimeEntry,
collapsed: boolean
): TimeEntry {
// Entry cannot be collapsed // Entry cannot be collapsed
if (entry.subEntries === null) { if (entry.subEntries === null) {
return entry; return entry;
@ -69,10 +62,7 @@ export function setEntryCollapsed(
* @param entries The entries to stop * @param entries The entries to stop
* @returns The new list of stopped entries * @returns The new list of stopped entries
*/ */
export function stopRunningEntries( export function stopRunningEntries(entries: TimeEntry[], endTime: Moment): TimeEntry[] {
entries: TimeEntry[],
endTime: Moment
): TimeEntry[] {
return entries.map((entry) => { return entries.map((entry) => {
// Stop the sub entries // Stop the sub entries
if (entry.subEntries) { if (entry.subEntries) {
@ -105,10 +95,7 @@ export function stopRunningEntries(
* @param target The target entry to remove * @param target The target entry to remove
* @returns The new list with the entry removed * @returns The new list with the entry removed
*/ */
export function removeEntry( export function removeEntry(entries: TimeEntry[], target: TimeEntry): TimeEntry[] {
entries: TimeEntry[],
target: TimeEntry
): TimeEntry[] {
return entries.reduce((acc: TimeEntry[], entry: TimeEntry) => { return entries.reduce((acc: TimeEntry[], entry: TimeEntry) => {
if (entry.id !== target.id) { if (entry.id !== target.id) {
// Filter sub entries for matching entries // Filter sub entries for matching entries

View file

@ -10,11 +10,7 @@ export class ConfirmModal extends Modal {
// Whether an option was picked // Whether an option was picked
picked: boolean; picked: boolean;
constructor( constructor(app: App, message: string, callback: (choice: boolean) => void) {
app: App,
message: string,
callback: (choice: boolean) => void
) {
super(app); super(app);
this.message = message; this.message = message;
this.callback = callback; this.callback = callback;

View file

@ -2,132 +2,128 @@ import { NameSegmentType, parseNameSegments } from "./name";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
describe("parseNameSegments", () => { describe("parseNameSegments", () => {
it("should parse plain text without modification", () => { it("should parse plain text without modification", () => {
const input = "Hello World"; const input = "Hello World";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([{ type: NameSegmentType.Text, text: "Hello World" }]);
{ type: NameSegmentType.Text, text: "Hello World" }, });
]);
});
it("should parse a Wikilink with no other content", () => { it("should parse a Wikilink with no other content", () => {
const input = "[[Wikilink]]"; const input = "[[Wikilink]]";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "Wikilink", url: "Wikilink",
text: "Wikilink", text: "Wikilink",
}, },
]); ]);
}); });
it("should parse text with a Wikilink", () => { it("should parse text with a Wikilink", () => {
const input = "Hello [[Wikilink]]"; const input = "Hello [[Wikilink]]";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ type: NameSegmentType.Text, text: "Hello " }, { type: NameSegmentType.Text, text: "Hello " },
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "Wikilink", url: "Wikilink",
text: "Wikilink", text: "Wikilink",
}, },
]); ]);
}); });
it("should parse a Markdown link with no other content", () => { it("should parse a Markdown link with no other content", () => {
const input = "[Text](https://example.com)"; const input = "[Text](https://example.com)";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "https://example.com", url: "https://example.com",
text: "Text", text: "Text",
}, },
]); ]);
}); });
it("should parse text with a Markdown link", () => { it("should parse text with a Markdown link", () => {
const input = "Hello [Text](https://example.com)"; const input = "Hello [Text](https://example.com)";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ type: NameSegmentType.Text, text: "Hello " }, { type: NameSegmentType.Text, text: "Hello " },
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "https://example.com", url: "https://example.com",
text: "Text", text: "Text",
}, },
]); ]);
}); });
it("should handle malformed Wikilinks gracefully", () => { it("should handle malformed Wikilinks gracefully", () => {
const input = "[[Wikilink"; const input = "[[Wikilink";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([{ type: NameSegmentType.Text, text: "[[Wikilink" }]);
{ type: NameSegmentType.Text, text: "[[Wikilink" }, });
]);
});
it("should handle malformed Markdown links gracefully", () => { it("should handle malformed Markdown links gracefully", () => {
const input = "[Text](https://example.com"; const input = "[Text](https://example.com";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ type: NameSegmentType.Text, text: "[Text](https://example.com" }, { type: NameSegmentType.Text, text: "[Text](https://example.com" },
]); ]);
}); });
it("should parse a combination of Wikilinks and Markdown links", () => { it("should parse a combination of Wikilinks and Markdown links", () => {
const input = "Hello [[Wikilink]] and [Text](https://example.com)"; const input = "Hello [[Wikilink]] and [Text](https://example.com)";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ type: NameSegmentType.Text, text: "Hello " }, { type: NameSegmentType.Text, text: "Hello " },
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "Wikilink", url: "Wikilink",
text: "Wikilink", text: "Wikilink",
}, },
{ type: NameSegmentType.Text, text: " and " }, { type: NameSegmentType.Text, text: " and " },
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "https://example.com", url: "https://example.com",
text: "Text", text: "Text",
}, },
]); ]);
}); });
it("should parse a combination of Wikilinks and Markdown links with trailing text", () => { it("should parse a combination of Wikilinks and Markdown links with trailing text", () => {
const input = "Hello [[Wikilink]] and [Text](https://example.com)."; const input = "Hello [[Wikilink]] and [Text](https://example.com).";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ type: NameSegmentType.Text, text: "Hello " }, { type: NameSegmentType.Text, text: "Hello " },
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "Wikilink", url: "Wikilink",
text: "Wikilink", text: "Wikilink",
}, },
{ type: NameSegmentType.Text, text: " and " }, { type: NameSegmentType.Text, text: " and " },
{ {
type: NameSegmentType.Link, type: NameSegmentType.Link,
url: "https://example.com", url: "https://example.com",
text: "Text", text: "Text",
}, },
{ type: NameSegmentType.Text, text: "." }, { type: NameSegmentType.Text, text: "." },
]); ]);
}); });
it("should handle multiple Wikilinks correctly", () => { it("should handle multiple Wikilinks correctly", () => {
const input = "[[Link1]] and [[Link2]]"; const input = "[[Link1]] and [[Link2]]";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([ expect(result).toEqual([
{ type: NameSegmentType.Link, url: "Link1", text: "Link1" }, { type: NameSegmentType.Link, url: "Link1", text: "Link1" },
{ type: NameSegmentType.Text, text: " and " }, { type: NameSegmentType.Text, text: " and " },
{ type: NameSegmentType.Link, url: "Link2", text: "Link2" }, { type: NameSegmentType.Link, url: "Link2", text: "Link2" },
]); ]);
}); });
it("should handle empty input gracefully", () => { it("should handle empty input gracefully", () => {
const input = ""; const input = "";
const result = parseNameSegments(input); const result = parseNameSegments(input);
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
}); });

View file

@ -29,8 +29,7 @@ export function parseNameSegments(input: string): NameSegment[] {
// Handle the text before the current link // Handle the text before the current link
const beforeLink = input.slice(lastIndex, index); const beforeLink = input.slice(lastIndex, index);
if (beforeLink) if (beforeLink) segments.push({ type: NameSegmentType.Text, text: beforeLink });
segments.push({ type: NameSegmentType.Text, text: beforeLink });
// Handle the matched link // Handle the matched link
if (match[1]) { if (match[1]) {

View file

@ -60,14 +60,11 @@ describe("format duration", () => {
[1000 * 60 * 60 * 2, "2h 0s"], [1000 * 60 * 60 * 2, "2h 0s"],
[1000 * 60 * 60 * 2.5, "2h 30m 0s"], [1000 * 60 * 60 * 2.5, "2h 30m 0s"],
[1000 * 60 * 60 * 2.505, "2h 30m 18s"], [1000 * 60 * 60 * 2.505, "2h 30m 18s"],
])( ])('for duration "%s" should expected formatted "%s"', (input, expected) => {
'for duration "%s" should expected formatted "%s"', const output = formatDurationLong(input);
(input, expected) => {
const output = formatDurationLong(input);
expect(output).toBe(expected); expect(output).toBe(expected);
} });
);
}); });
describe("format duration short", () => { describe("format duration short", () => {
@ -76,14 +73,11 @@ describe("format duration short", () => {
[1000 * 60 * 60 * 25.25, "25.25h"], [1000 * 60 * 60 * 25.25, "25.25h"],
[1000 * 60 * 60 * 25.255, "25.25h"], [1000 * 60 * 60 * 25.255, "25.25h"],
[1000 * 60 * 60 * 50.5, "50.50h"], [1000 * 60 * 60 * 50.5, "50.50h"],
])( ])('for duration "%s" should expected formatted "%s"', (input, expected) => {
'for duration "%s" should expected formatted "%s"', const output = formatDurationShort(input);
(input, expected) => {
const output = formatDurationShort(input);
expect(output).toBe(expected); expect(output).toBe(expected);
} });
);
}); });
describe("format duration decimal", () => { describe("format duration decimal", () => {
@ -92,14 +86,11 @@ describe("format duration decimal", () => {
[1000 * 60 * 60 * 25.25, "25.25"], [1000 * 60 * 60 * 25.25, "25.25"],
[1000 * 60 * 60 * 25.255, "25.25"], [1000 * 60 * 60 * 25.255, "25.25"],
[1000 * 60 * 60 * 50.5, "50.50"], [1000 * 60 * 60 * 50.5, "50.50"],
])( ])('for duration "%s" should expected formatted "%s"', (input, expected) => {
'for duration "%s" should expected formatted "%s"', const output = formatDurationDecimal(input);
(input, expected) => {
const output = formatDurationDecimal(input);
expect(output).toBe(expected); expect(output).toBe(expected);
} });
);
}); });
describe("format duration with format", () => { describe("format duration with format", () => {
@ -134,14 +125,11 @@ describe("format duration with format", () => {
test.each([ test.each([
["ANY" as DurationFormat, 1000, "unknown duration format"], ["ANY" as DurationFormat, 1000, "unknown duration format"],
["" as DurationFormat, 1000, "unknown duration format"], ["" as DurationFormat, 1000, "unknown duration format"],
])( ])('for format "%s" with duration "%s" should thrown Error "%s"', (format, input, error) => {
'for format "%s" with duration "%s" should thrown Error "%s"', expect(() => {
(format, input, error) => { formatDuration(format, input);
expect(() => { }).toThrow(error);
formatDuration(format, input); });
}).toThrow(error);
}
);
}); });
describe("format pdf date", () => { describe("format pdf date", () => {
@ -169,14 +157,11 @@ describe("format pdf row date", () => {
["2024-01-12 14:34:45", "12/01/2024 14:34"], ["2024-01-12 14:34:45", "12/01/2024 14:34"],
["2024-09-24 18:34:45", "24/09/2024 18:34"], ["2024-09-24 18:34:45", "24/09/2024 18:34"],
["2023-09-24 20:34:45", "24/09/2023 20:34"], ["2023-09-24 20:34:45", "24/09/2023 20:34"],
])( ])('for date time "%s" should expected formatted "%s"', (input, expected) => {
'for date time "%s" should expected formatted "%s"', const settings: TimekeepSettings = defaultSettings;
(input, expected) => { settings.pdfRowDateFormat = "DD/MM/YYYY HH:mm";
const settings: TimekeepSettings = defaultSettings; const output = formatPdfRowDate(moment(input), settings);
settings.pdfRowDateFormat = "DD/MM/YYYY HH:mm";
const output = formatPdfRowDate(moment(input), settings);
expect(output).toBe(expected); expect(output).toBe(expected);
} });
);
}); });

View file

@ -8,10 +8,7 @@ import { DurationFormat, TimekeepSettings } from "@/settings";
* @param settings The timekeep settings * @param settings The timekeep settings
* @returns The formatted timestamp * @returns The formatted timestamp
*/ */
export function formatTimestamp( export function formatTimestamp(timestamp: Moment, settings: TimekeepSettings): string {
timestamp: Moment,
settings: TimekeepSettings
): string {
return timestamp.format(settings.timestampFormat); return timestamp.format(settings.timestampFormat);
} }
@ -23,10 +20,7 @@ export function formatTimestamp(
* @param settings The timekeep settings * @param settings The timekeep settings
* @returns The formatted timestamp * @returns The formatted timestamp
*/ */
export function formatEditableTimestamp( export function formatEditableTimestamp(timestamp: Moment, settings: TimekeepSettings): string {
timestamp: Moment,
settings: TimekeepSettings
): string {
return timestamp.format(settings.editableTimestampFormat); return timestamp.format(settings.editableTimestampFormat);
} }
@ -37,10 +31,7 @@ export function formatEditableTimestamp(
* @param settings The timekeep settings * @param settings The timekeep settings
* @returns The timestamp * @returns The timestamp
*/ */
export function parseEditableTimestamp( export function parseEditableTimestamp(formatted: string, settings: TimekeepSettings): Moment {
formatted: string,
settings: TimekeepSettings
): Moment {
return moment(formatted, settings.editableTimestampFormat, true); return moment(formatted, settings.editableTimestampFormat, true);
} }
@ -52,10 +43,7 @@ export function parseEditableTimestamp(
* @throws unknown duration format when format is not supported * @throws unknown duration format when format is not supported
* @returns The formatted duration * @returns The formatted duration
*/ */
export function formatDuration( export function formatDuration(format: DurationFormat, durationMS: number): string {
format: DurationFormat,
durationMS: number
): string {
switch (format) { switch (format) {
case DurationFormat.LONG: case DurationFormat.LONG:
return formatDurationLong(durationMS); return formatDurationLong(durationMS);
@ -124,10 +112,7 @@ export function formatDurationDecimal(durationMS: number): string {
* @param settings The settings * @param settings The settings
* @returns The formatted date * @returns The formatted date
*/ */
export function formatPdfDate( export function formatPdfDate(value: Moment, settings: TimekeepSettings): string {
value: Moment,
settings: TimekeepSettings
): string {
return value.format(settings.pdfDateFormat); return value.format(settings.pdfDateFormat);
} }
@ -140,9 +125,6 @@ export function formatPdfDate(
* @param settings The settings * @param settings The settings
* @returns The formatted timestamp * @returns The formatted timestamp
*/ */
export function formatPdfRowDate( export function formatPdfRowDate(value: Moment, settings: TimekeepSettings): string {
value: Moment,
settings: TimekeepSettings
): string {
return value.format(settings.pdfRowDateFormat); return value.format(settings.pdfRowDateFormat);
} }

View file

@ -5,12 +5,7 @@ import { TimekeepSettings } from "@/settings";
import { CustomOutputFormat } from "@/output"; import { CustomOutputFormat } from "@/output";
import { Timekeep, stripTimekeepRuntimeData } from "@/timekeep/schema"; import { Timekeep, stripTimekeepRuntimeData } from "@/timekeep/schema";
import { LoadResult, replaceTimekeepCodeblock } from "@/timekeep/parser"; import { LoadResult, replaceTimekeepCodeblock } from "@/timekeep/parser";
import { import { TFile, TAbstractFile, MarkdownRenderChild, MarkdownPostProcessorContext } from "obsidian";
TFile,
TAbstractFile,
MarkdownRenderChild,
MarkdownPostProcessorContext,
} from "obsidian";
import { Timesheet } from "@/components/timesheet"; import { Timesheet } from "@/components/timesheet";
import { TimesheetLoadError } from "@/components/timesheetLoadError"; import { TimesheetLoadError } from "@/components/timesheetLoadError";
@ -53,17 +48,11 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
onload(): void { onload(): void {
// Hook file renaming to update the file we are saving to if its renamed // Hook file renaming to update the file we are saving to if its renamed
this.registerEvent( this.registerEvent(
this.app.vault.on( this.app.vault.on("rename", (file: TAbstractFile, oldName: string) => {
"rename", if (file instanceof TFile && oldName == this.fileSourcePath) {
(file: TAbstractFile, oldName: string) => { this.fileSourcePath = file.path;
if (
file instanceof TFile &&
oldName == this.fileSourcePath
) {
this.fileSourcePath = file.path;
}
} }
) })
); );
// Render the content // Render the content
@ -106,10 +95,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
this.addChild(timesheet); this.addChild(timesheet);
this.timesheet = timesheet; this.timesheet = timesheet;
} else { } else {
const timesheet = new TimesheetLoadError( const timesheet = new TimesheetLoadError(this.containerEl, this.loadResult.error);
this.containerEl,
this.loadResult.error
);
this.addChild(timesheet); this.addChild(timesheet);
this.timesheet = timesheet; this.timesheet = timesheet;
} }
@ -134,10 +120,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
const editor = activeEditor.editor; const editor = activeEditor.editor;
if (!editor) return; if (!editor) return;
editor.scrollTo( editor.scrollTo(this.restoreScrollInfo.left, this.restoreScrollInfo.top);
this.restoreScrollInfo.left,
this.restoreScrollInfo.top
);
} }
/** /**
@ -166,10 +149,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
return false; return false;
} finally { } finally {
// Queue scroll restoration after 50ms (Should be enough for the codeblock to re-render) // Queue scroll restoration after 50ms (Should be enough for the codeblock to re-render)
this.restoreScrollTimeout = setTimeout( this.restoreScrollTimeout = setTimeout(this.restoreEditorScroll.bind(this), 50);
this.restoreEditorScroll.bind(this),
50
);
} }
} }
@ -182,8 +162,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
const sectionInfo = this.context.getSectionInfo(this.containerEl); const sectionInfo = this.context.getSectionInfo(this.containerEl);
// Ensure we actually have a section to write to // Ensure we actually have a section to write to
if (sectionInfo === null) if (sectionInfo === null) throw new Error("Section to write did not exist");
throw new Error("Section to write did not exist");
const file = this.app.vault.getFileByPath(this.fileSourcePath); const file = this.app.vault.getFileByPath(this.fileSourcePath);
@ -213,9 +192,6 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
const backupFileName = `timekeep-write-backup-${moment().format("YYYY-MM-DD HH-mm-ss")}.json`; const backupFileName = `timekeep-write-backup-${moment().format("YYYY-MM-DD HH-mm-ss")}.json`;
// Write to the backup file // Write to the backup file
this.app.vault.create( this.app.vault.create(backupFileName, JSON.stringify(stripTimekeepRuntimeData(timekeep)));
backupFileName,
JSON.stringify(stripTimekeepRuntimeData(timekeep))
);
} }
} }

View file

@ -26,11 +26,7 @@ export class TimekeepMergerModal extends Modal {
private exportPdf: boolean; private exportPdf: boolean;
private settings: Store<TimekeepSettings>; private settings: Store<TimekeepSettings>;
constructor( constructor(app: App, settings: Store<TimekeepSettings>, exportPdf = false) {
app: App,
settings: Store<TimekeepSettings>,
exportPdf = false
) {
super(app); super(app);
this.exportPdf = exportPdf; this.exportPdf = exportPdf;
@ -102,10 +98,7 @@ export class TimekeepMergerModal extends Modal {
}; };
for (const result of this.selectedResults) { for (const result of this.selectedResults) {
timekeep.entries = [ timekeep.entries = [...timekeep.entries, ...result.timekeep.entries];
...timekeep.entries,
...result.timekeep.entries,
];
} }
// Close after taking the results as closing resets the list // Close after taking the results as closing resets the list
@ -148,8 +141,7 @@ export class TimekeepMergerModal extends Modal {
updateSelectAll() { updateSelectAll() {
if (this.selectContainer) { if (this.selectContainer) {
this.selectContainer.style.display = this.selectContainer.style.display = this.filteredResults.length > 0 ? "flex" : "none";
this.filteredResults.length > 0 ? "flex" : "none";
} }
const isAllSelected = this.isAllSelected(); const isAllSelected = this.isAllSelected();
@ -168,9 +160,7 @@ export class TimekeepMergerModal extends Modal {
} }
for (const result of this.filteredResults) { for (const result of this.filteredResults) {
const selected = this.selectedResults.find( const selected = this.selectedResults.find((selected) => selected.id === result.id);
(selected) => selected.id === result.id
);
if (selected === undefined) { if (selected === undefined) {
return false; return false;
@ -192,9 +182,7 @@ export class TimekeepMergerModal extends Modal {
if (checked) { if (checked) {
// Find all results that aren't already selected // Find all results that aren't already selected
const toBeSelected = this.filteredResults.filter((result) => { const toBeSelected = this.filteredResults.filter((result) => {
const selected = this.selectedResults.find( const selected = this.selectedResults.find((selected) => selected.id === result.id);
(selected) => selected.id === result.id
);
return selected === undefined; return selected === undefined;
}); });
@ -279,8 +267,7 @@ export class TimekeepMergerModal extends Modal {
type: "checkbox", type: "checkbox",
}); });
checkbox.checked = checkbox.checked =
this.selectedResults.find((other) => other.id === result.id) !== this.selectedResults.find((other) => other.id === result.id) !== undefined;
undefined;
checkbox.id = `timekeep-${result.id}`; checkbox.id = `timekeep-${result.id}`;
checkbox.onchange = () => { checkbox.onchange = () => {

View file

@ -1,4 +1,3 @@
```dataviewjs ```dataviewjs
// Get the currently open file // Get the currently open file
const targetFile = this.app.vault.getFileByPath("Obsidian Timekeep.md"); const targetFile = this.app.vault.getFileByPath("Obsidian Timekeep.md");
@ -32,7 +31,7 @@ function formatDuration(totalTime) {
for (const timekeep of timekeeps) { for (const timekeep of timekeeps) {
// Create the entries // Create the entries
const data = timekeep.entries const data = timekeep.entries
.map((entry) => { .map((entry) => {
let duration = timekeepPlugin.getEntryDuration(entry, currentTime); let duration = timekeepPlugin.getEntryDuration(entry, currentTime);
return [entry.name, duration]; return [entry.name, duration];
}) })

File diff suppressed because one or more lines are too long

View file

@ -1,21 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"inlineSourceMap": true, "inlineSourceMap": true,
"inlineSources": true, "inlineSources": true,
"module": "ESNext", "module": "ESNext",
"target": "ESNext", "target": "ESNext",
"allowJs": true, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"isolatedModules": true, "isolatedModules": true,
"strictNullChecks": true, "strictNullChecks": true,
"lib": ["DOM", "ES5", "ES6"], "lib": ["DOM", "ES5", "ES6"],
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"esModuleInterop": true "esModuleInterop": true
}, },
"include": ["src/**/*", "types/**/*"] "include": ["src/**/*", "types/**/*"]
} }

View file

@ -38,4 +38,4 @@
"1.14.1": "1.5.12", "1.14.1": "1.5.12",
"1.15.0": "1.5.12", "1.15.0": "1.5.12",
"1.16.0": "1.5.12" "1.16.0": "1.5.12"
} }

View file

@ -7,52 +7,52 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
export default defineConfig((env) => ({ export default defineConfig((env) => ({
build: { build: {
outDir: "dist", outDir: "dist",
sourcemap: env.mode === "production" ? false : "inline", sourcemap: env.mode === "production" ? false : "inline",
target: "es2018", target: "es2018",
minify: true, minify: true,
lib: { lib: {
entry: "src/main.ts", entry: "src/main.ts",
formats: ["cjs"], formats: ["cjs"],
fileName: () => "main.js", fileName: () => "main.js",
}, },
rolldownOptions: { rolldownOptions: {
input: { input: {
main: path.resolve(__dirname, "src/main.ts"), main: path.resolve(__dirname, "src/main.ts"),
styles: path.resolve(__dirname, "src/styles.css"), styles: path.resolve(__dirname, "src/styles.css"),
}, },
external: [ external: [
"obsidian", "obsidian",
"electron", "electron",
"@codemirror/autocomplete", "@codemirror/autocomplete",
"@codemirror/collab", "@codemirror/collab",
"@codemirror/commands", "@codemirror/commands",
"@codemirror/language", "@codemirror/language",
"@codemirror/lint", "@codemirror/lint",
"@codemirror/search", "@codemirror/search",
"@codemirror/state", "@codemirror/state",
"@codemirror/view", "@codemirror/view",
"@lezer/common", "@lezer/common",
"@lezer/highlight", "@lezer/highlight",
"@lezer/lr", "@lezer/lr",
...builtinModules, ...builtinModules,
], ],
output: { output: {
exports: "auto", exports: "auto",
}, },
}, },
// //
cssCodeSplit: true, cssCodeSplit: true,
// Inline all assets (.ttf ...etc) // Inline all assets (.ttf ...etc)
assetsInlineLimit: Infinity, assetsInlineLimit: Infinity,
}, },
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "src"), "@": path.resolve(__dirname, "src"),
}, },
}, },
test: {}, test: {},
})); }));