mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 05:12:14 +00:00
feat: replace prettier with oxfmt
This commit is contained in:
parent
fbf3e46daa
commit
a1fcc3ff21
67 changed files with 3915 additions and 5133 deletions
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,27 +1,32 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: 'Bug: '
|
||||
title: "Bug: "
|
||||
labels: bug
|
||||
assignees: jacobtread
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1.
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
|
|
|||
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,20 +1,23 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: 'Feature:'
|
||||
title: "Feature:"
|
||||
labels: enhancement
|
||||
assignees: jacobtread
|
||||
|
||||
---
|
||||
|
||||
## 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 [...] -->
|
||||
|
||||
## 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
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
## Additional context
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
|
|
|||
3
.github/ISSUE_TEMPLATE/task.md
vendored
3
.github/ISSUE_TEMPLATE/task.md
vendored
|
|
@ -1,10 +1,9 @@
|
|||
---
|
||||
name: Task
|
||||
about: Template for a task that needs to be done
|
||||
title: 'Task: '
|
||||
title: "Task: "
|
||||
labels: enhancement
|
||||
assignees: jacobtread
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
## Description
|
||||
|
||||
## Changes
|
||||
-
|
||||
|
||||
-
|
||||
|
||||
## Related Issues
|
||||
-
|
||||
|
||||
-
|
||||
|
||||
## Screenshots (If applicable)
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
|
|
@ -25,4 +24,4 @@ dist
|
|||
|
||||
coverage
|
||||
|
||||
test-vault/.obsidian
|
||||
test-vault/.obsidian
|
||||
|
|
|
|||
9
.oxfmtrc.json
Normal file
9
.oxfmtrc.json
Normal 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/**"]
|
||||
}
|
||||
|
|
@ -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
3
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["oxc.oxc-vscode"]
|
||||
}
|
||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal 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
10
.zed/settings.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"prettier": {
|
||||
"allowed": false
|
||||
},
|
||||
"formatter": {
|
||||
"language_server": {
|
||||
"name": "oxfmt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,23 +14,23 @@ appearance, race, religion, or sexual identity and orientation.
|
|||
Examples of behaviour that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologising to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologising to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behaviour include:
|
||||
|
||||
- The use of sexualised language or imagery, and sexual attention or advances
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
- The use of sexualised language or imagery, and sexual attention or advances
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
>
|
||||
> - Star the project
|
||||
> - Tweet about it
|
||||
> - Refer this project in your project's readme
|
||||
> - Mention the project at local meetups and tell your friends/colleagues
|
||||
> - Star the project
|
||||
> - Tweet about it
|
||||
> - Refer this project in your project's readme
|
||||
> - Mention the project at local meetups and tell your friends/colleagues
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Use of Generative AI](#use-of-generative-ai)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [I Have a Question](#i-have-a-question)
|
||||
- [I Want To Contribute](#i-want-to-contribute)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Suggesting Enhancements](#suggesting-enhancements)
|
||||
- [Styleguides](#styleguides)
|
||||
- [Commit Messages](#commit-messages)
|
||||
- [Join The Project Team](#join-the-project-team)
|
||||
- [Use of Generative AI](#use-of-generative-ai)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [I Have a Question](#i-have-a-question)
|
||||
- [I Want To Contribute](#i-want-to-contribute)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Suggesting Enhancements](#suggesting-enhancements)
|
||||
- [Styleguides](#styleguides)
|
||||
- [Commit Messages](#commit-messages)
|
||||
- [Join The Project Team](#join-the-project-team)
|
||||
|
||||
## 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:
|
||||
|
||||
- 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 project and platform versions (nodejs, npm, etc), depending on what seems relevant.
|
||||
- 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 project and platform versions (nodejs, npm, etc), depending on what seems relevant.
|
||||
|
||||
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.
|
||||
|
||||
- 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)).
|
||||
- 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.
|
||||
- Collect information about the bug:
|
||||
- Stack trace (Traceback)
|
||||
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
|
||||
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
|
||||
- Possibly your input and the output
|
||||
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
|
||||
- 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)).
|
||||
- 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.
|
||||
- Collect information about the bug:
|
||||
- Stack trace (Traceback)
|
||||
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
|
||||
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
|
||||
- Possibly your input and the output
|
||||
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
|
||||
|
||||
<!-- 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:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
Once it's filed:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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. -->
|
||||
|
||||
|
|
@ -125,9 +125,9 @@ This section guides you through submitting an enhancement suggestion for Obsidia
|
|||
|
||||
#### Before Submitting an Enhancement
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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 -->
|
||||
|
||||
|
|
@ -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).
|
||||
|
||||
- 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.
|
||||
- **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 -->
|
||||
- **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.
|
||||
- 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.
|
||||
- **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 -->
|
||||
- **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. -->
|
||||
|
||||
|
|
|
|||
26
cspell.json
26
cspell.json
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"words": [
|
||||
"datetime",
|
||||
"Jacobtread",
|
||||
"lucide",
|
||||
"Roboto",
|
||||
"stylesheet",
|
||||
"timekeep",
|
||||
"timesheet",
|
||||
"unstarted",
|
||||
"Timekeeps",
|
||||
"pdfmake"
|
||||
]
|
||||
}
|
||||
"words": [
|
||||
"datetime",
|
||||
"Jacobtread",
|
||||
"lucide",
|
||||
"Roboto",
|
||||
"stylesheet",
|
||||
"timekeep",
|
||||
"timesheet",
|
||||
"unstarted",
|
||||
"Timekeeps",
|
||||
"pdfmake"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@
|
|||
"authorUrl": "https://jacobtread.com",
|
||||
"fundingUrl": "https://www.buymeacoffee.com/jacobtread",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6051
package-lock.json
generated
6051
package-lock.json
generated
File diff suppressed because it is too large
Load diff
71
package.json
71
package.json
|
|
@ -1,38 +1,37 @@
|
|||
{
|
||||
"name": "obsidian-timekeep",
|
||||
"version": "1.16.0",
|
||||
"description": "Obsidian plugin for time tracking",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"build": "node scripts/build.js",
|
||||
"version": "node scripts/version-bump.js && git add manifest.json versions.json",
|
||||
"lint": "oxlint src",
|
||||
"lint:fix": "oxlint --fix src",
|
||||
"format": "prettier -w src",
|
||||
"test": "vitest run --coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Jacobtread",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.37",
|
||||
"@types/pdfmake": "^0.3.2",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-perfectionist": "^5.7.0",
|
||||
"obsidian": "latest",
|
||||
"oxlint": "^1.57.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "5.9.2",
|
||||
"vite": "^8.0.2",
|
||||
"vitest": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1",
|
||||
"pdfmake": "^0.3.7",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
"name": "obsidian-timekeep",
|
||||
"version": "1.16.0",
|
||||
"description": "Obsidian plugin for time tracking",
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"author": "Jacobtread",
|
||||
"type": "module",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"build": "node scripts/build.js",
|
||||
"version": "node scripts/version-bump.js && git add manifest.json versions.json",
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"fmt": "oxfmt",
|
||||
"fmt:check": "oxfmt --check",
|
||||
"test": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1",
|
||||
"pdfmake": "^0.3.7",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.37",
|
||||
"@types/pdfmake": "^0.3.2",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"obsidian": "latest",
|
||||
"oxfmt": "^0.42.0",
|
||||
"oxlint": "^1.57.0",
|
||||
"typescript": "5.9.2",
|
||||
"vite": "^8.0.2",
|
||||
"vitest": "^4.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,34 +4,34 @@ import { build } from "vite";
|
|||
import { fileURLToPath } from "url";
|
||||
|
||||
async function buildPlugin() {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootPath = path.resolve(__dirname, "../");
|
||||
const manifestPath = path.join(rootPath, "manifest.json");
|
||||
const outputPath = path.join(rootPath, "dist");
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootPath = path.resolve(__dirname, "../");
|
||||
const manifestPath = path.join(rootPath, "manifest.json");
|
||||
const outputPath = path.join(rootPath, "dist");
|
||||
|
||||
await ensureDir(outputPath);
|
||||
await ensureDir(outputPath);
|
||||
|
||||
await build({
|
||||
configFile: path.resolve(rootPath, "vite.config.js"),
|
||||
});
|
||||
await build({
|
||||
configFile: path.resolve(rootPath, "vite.config.js"),
|
||||
});
|
||||
|
||||
const destManifest = path.join(outputPath, "manifest.json");
|
||||
try {
|
||||
await fs.copyFile(manifestPath, destManifest);
|
||||
console.info(`Copied manifest.json`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to copy manifest.json:`, e.message);
|
||||
}
|
||||
const destManifest = path.join(outputPath, "manifest.json");
|
||||
try {
|
||||
await fs.copyFile(manifestPath, destManifest);
|
||||
console.info(`Copied manifest.json`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to copy manifest.json:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDir(dir) {
|
||||
try {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
} catch {}
|
||||
try {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
buildPlugin().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
106
scripts/dev.js
106
scripts/dev.js
|
|
@ -4,74 +4,66 @@ import { build } from "vite";
|
|||
import { fileURLToPath } from "url";
|
||||
|
||||
async function dev() {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootPath = path.resolve(__dirname, "../");
|
||||
const manifestPath = path.join(rootPath, "manifest.json");
|
||||
const outputPath = path.join(rootPath, "dist");
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootPath = path.resolve(__dirname, "../");
|
||||
const manifestPath = path.join(rootPath, "manifest.json");
|
||||
const outputPath = path.join(rootPath, "dist");
|
||||
|
||||
const manifestContents = await fs.readFile(manifestPath, "utf-8");
|
||||
const manifest = JSON.parse(manifestContents);
|
||||
const manifestContents = await fs.readFile(manifestPath, "utf-8");
|
||||
const manifest = JSON.parse(manifestContents);
|
||||
|
||||
const vaultPath = path.join(rootPath, "test-vault");
|
||||
const pluginPath = path.join(
|
||||
vaultPath,
|
||||
".obsidian",
|
||||
"plugins",
|
||||
manifest.id
|
||||
);
|
||||
const vaultPath = path.join(rootPath, "test-vault");
|
||||
const pluginPath = path.join(vaultPath, ".obsidian", "plugins", manifest.id);
|
||||
|
||||
await ensureDir(pluginPath);
|
||||
await ensureDir(outputPath);
|
||||
await ensureDir(pluginPath);
|
||||
await ensureDir(outputPath);
|
||||
|
||||
const inputFiles = [
|
||||
path.join(outputPath, "main.js"),
|
||||
path.join(outputPath, "styles.css"),
|
||||
manifestPath,
|
||||
];
|
||||
const inputFiles = [
|
||||
path.join(outputPath, "main.js"),
|
||||
path.join(outputPath, "styles.css"),
|
||||
manifestPath,
|
||||
];
|
||||
|
||||
build({
|
||||
mode: "development",
|
||||
configFile: path.resolve(rootPath, "vite.config.js"),
|
||||
build: {
|
||||
watch: {},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "build-finish-copy",
|
||||
closeBundle() {
|
||||
console.info("Build finished copying files");
|
||||
copyFiles();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
build({
|
||||
mode: "development",
|
||||
configFile: path.resolve(rootPath, "vite.config.js"),
|
||||
build: {
|
||||
watch: {},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "build-finish-copy",
|
||||
closeBundle() {
|
||||
console.info("Build finished copying files");
|
||||
copyFiles();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const copyFiles = async () => {
|
||||
for (const filePath of inputFiles) {
|
||||
const destPath = path.join(pluginPath, path.basename(filePath));
|
||||
try {
|
||||
await fs.copyFile(filePath, destPath);
|
||||
console.info(`✓ Updated ${path.basename(filePath)}`);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to copy ${path.basename(filePath)}:`,
|
||||
e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
const copyFiles = async () => {
|
||||
for (const filePath of inputFiles) {
|
||||
const destPath = path.join(pluginPath, path.basename(filePath));
|
||||
try {
|
||||
await fs.copyFile(filePath, destPath);
|
||||
console.info(`✓ Updated ${path.basename(filePath)}`);
|
||||
} catch (e) {
|
||||
console.error(`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) {
|
||||
try {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
} catch {}
|
||||
try {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
dev().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,9 +32,7 @@ export class MockVault {
|
|||
}
|
||||
|
||||
getMarkdownFiles = vi.fn(() => {
|
||||
return Object.keys(this._files).map((path) =>
|
||||
createTFile(this.asVault(), path)
|
||||
);
|
||||
return Object.keys(this._files).map((path) => createTFile(this.asVault(), path));
|
||||
});
|
||||
|
||||
process = vi.fn(async (file: TFile, func: (data: string) => string) => {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ import { expect, it, describe } from "vitest";
|
|||
describe("stopAllTimekeeps", () => {
|
||||
it("should stop nothing when no markdown files", async () => {
|
||||
const vault = new MockVault();
|
||||
const amount = await stopAllTimekeeps(
|
||||
vault.asVault(),
|
||||
moment("2025-11-07T00:31:03.714Z")
|
||||
);
|
||||
const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
|
||||
|
||||
expect(amount).toBe(0);
|
||||
});
|
||||
|
|
@ -23,10 +20,7 @@ describe("stopAllTimekeeps", () => {
|
|||
vault.addFile("TEST_MARKDOWN_2.md", "");
|
||||
vault.addFile("TEST_MARKDOWN_3.md", "");
|
||||
|
||||
const amount = await stopAllTimekeeps(
|
||||
vault.asVault(),
|
||||
moment("2025-11-07T00:31:03.714Z")
|
||||
);
|
||||
const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
|
||||
|
||||
expect(amount).toBe(0);
|
||||
});
|
||||
|
|
@ -55,10 +49,7 @@ describe("stopAllTimekeeps", () => {
|
|||
const file2 = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_3);
|
||||
const file3 = vault.addFile("TEST_MARKDOWN_3.md", TEST_MARKDOWN_3);
|
||||
|
||||
const amount = await stopAllTimekeeps(
|
||||
vault.asVault(),
|
||||
moment("2025-11-07T00:31:03.714Z")
|
||||
);
|
||||
const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
|
||||
|
||||
const output1 = await vault.read(file1);
|
||||
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 file3 = vault.addFile("TEST_MARKDOWN_3.md", TEST_MARKDOWN_3);
|
||||
|
||||
const amount = await stopAllTimekeeps(
|
||||
vault.asVault(),
|
||||
moment("2025-11-07T00:31:03.714Z")
|
||||
);
|
||||
const amount = await stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"));
|
||||
|
||||
const output1 = await vault.read(file1);
|
||||
expect(output1).toEqual(TEST_MARKDOWN_1_STOPPED);
|
||||
|
|
@ -137,10 +125,7 @@ describe("stopAllTimekeeps", () => {
|
|||
vault.read.mockReturnValueOnce(Promise.resolve(""));
|
||||
|
||||
await expect(
|
||||
stopAllTimekeeps(
|
||||
vault.asVault(),
|
||||
moment("2025-11-07T00:31:03.714Z")
|
||||
)
|
||||
stopAllTimekeeps(vault.asVault(), moment("2025-11-07T00:31:03.714Z"))
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ export async function stopAllTimekeeps(vault: Vault, currentTime: Moment) {
|
|||
const markdownFiles = vault.getMarkdownFiles();
|
||||
const batchSize = 10;
|
||||
|
||||
const processFile = (file: TFile) =>
|
||||
stopFileTimekeeps(vault, file, currentTime);
|
||||
const processFile = (file: TFile) => stopFileTimekeeps(vault, file, currentTime);
|
||||
|
||||
let totalStopped = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ describe("stopFileTimekeeps", () => {
|
|||
"utf-8"
|
||||
);
|
||||
|
||||
const file1 = vault.addFile(
|
||||
"TEST_MARKDOWN_1.md",
|
||||
TEST_MARKDOWN_1_STOPPED
|
||||
);
|
||||
const file1 = vault.addFile("TEST_MARKDOWN_1.md", TEST_MARKDOWN_1_STOPPED);
|
||||
|
||||
const amount = await stopFileTimekeeps(
|
||||
vault.asVault(),
|
||||
|
|
@ -81,11 +78,7 @@ describe("stopFileTimekeeps", () => {
|
|||
);
|
||||
|
||||
const file = vault.addFile("TEST_MARKDOWN_2.md", TEST_MARKDOWN_2);
|
||||
await stopFileTimekeeps(
|
||||
vault.asVault(),
|
||||
file,
|
||||
moment("2025-11-07T00:31:03.714Z")
|
||||
);
|
||||
await stopFileTimekeeps(vault.asVault(), file, moment("2025-11-07T00:31:03.714Z"));
|
||||
|
||||
const output = await vault.read(file);
|
||||
expect(output).toEqual(TEST_MARKDOWN_2_STOPPED);
|
||||
|
|
@ -108,11 +101,7 @@ describe("stopFileTimekeeps", () => {
|
|||
vault.read.mockReturnValueOnce(Promise.resolve(""));
|
||||
|
||||
await expect(
|
||||
stopFileTimekeeps(
|
||||
vault.asVault(),
|
||||
file,
|
||||
moment("2025-11-07T00:31:03.714Z")
|
||||
)
|
||||
stopFileTimekeeps(vault.asVault(), file, moment("2025-11-07T00:31:03.714Z"))
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ import { Moment } from "moment";
|
|||
import { TFile, Vault } from "obsidian";
|
||||
import { Timekeep } from "@/timekeep/schema";
|
||||
import { getRunningEntry, stopRunningEntries } from "@/timekeep";
|
||||
import {
|
||||
replaceTimekeepCodeblock,
|
||||
extractTimekeepCodeblocksWithPosition,
|
||||
} from "@/timekeep/parser";
|
||||
import { replaceTimekeepCodeblock, extractTimekeepCodeblocksWithPosition } from "@/timekeep/parser";
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns The total number of stopped timekeeps
|
||||
*/
|
||||
export async function stopFileTimekeeps(
|
||||
vault: Vault,
|
||||
file: TFile,
|
||||
currentTime: Moment
|
||||
) {
|
||||
export async function stopFileTimekeeps(vault: Vault, file: TFile, currentTime: Moment) {
|
||||
const content = await vault.cachedRead(file);
|
||||
const initialTimekeeps = extractTimekeepCodeblocksWithPosition(content);
|
||||
|
||||
// Collect the indexes of running timekeeps
|
||||
const runningIndexes = initialTimekeeps.reduce<number[]>(
|
||||
(runningIndexes, timekeep, index) => {
|
||||
const entry = getRunningEntry(timekeep.timekeep.entries);
|
||||
const runningIndexes = initialTimekeeps.reduce<number[]>((runningIndexes, timekeep, index) => {
|
||||
const entry = getRunningEntry(timekeep.timekeep.entries);
|
||||
|
||||
if (entry !== null) {
|
||||
runningIndexes.push(index);
|
||||
}
|
||||
if (entry !== null) {
|
||||
runningIndexes.push(index);
|
||||
}
|
||||
|
||||
return runningIndexes;
|
||||
},
|
||||
[]
|
||||
);
|
||||
return runningIndexes;
|
||||
}, []);
|
||||
|
||||
// Nothing to process
|
||||
if (runningIndexes.length < 1) {
|
||||
|
|
@ -64,12 +54,7 @@ export async function stopFileTimekeeps(
|
|||
entries: stopRunningEntries(timekeep.entries, currentTime),
|
||||
};
|
||||
|
||||
content = replaceTimekeepCodeblock(
|
||||
stoppedTimekeep,
|
||||
content,
|
||||
startLine,
|
||||
endLine
|
||||
);
|
||||
content = replaceTimekeepCodeblock(stoppedTimekeep, content, startLine, endLine);
|
||||
}
|
||||
|
||||
return content;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import type { Moment } from "moment";
|
|||
|
||||
import pdfMake from "pdfmake";
|
||||
import type {
|
||||
DynamicContent,
|
||||
TableCell,
|
||||
TDocumentDefinitions,
|
||||
TFontDictionary,
|
||||
DynamicContent,
|
||||
TableCell,
|
||||
TDocumentDefinitions,
|
||||
TFontDictionary,
|
||||
} from "pdfmake/interfaces";
|
||||
|
||||
// Embedded fonts
|
||||
|
|
@ -16,362 +16,346 @@ import RubikRegular from "@/fonts/Rubik-Regular.ttf";
|
|||
import RubikBold from "@/fonts/Rubik-Bold.ttf";
|
||||
import RobotoRegular from "@/fonts/Roboto-Regular.ttf";
|
||||
import { getEntryDuration, getTotalDuration } from "@/timekeep";
|
||||
import {
|
||||
formatDurationLong,
|
||||
formatDurationShort,
|
||||
formatPdfDate,
|
||||
formatPdfRowDate,
|
||||
} from "@/utils";
|
||||
import { formatDurationLong, formatDurationShort, formatPdfDate, formatPdfRowDate } from "@/utils";
|
||||
import { Readable } from "stream";
|
||||
|
||||
const fonts: TFontDictionary = {
|
||||
Roboto: {
|
||||
normal: "Roboto-Regular.ttf",
|
||||
bold: "Roboto-Bold.ttf",
|
||||
},
|
||||
Rubik: {
|
||||
normal: "Rubik-Regular.ttf",
|
||||
bold: "Rubik-Bold.ttf",
|
||||
},
|
||||
Roboto: {
|
||||
normal: "Roboto-Regular.ttf",
|
||||
bold: "Roboto-Bold.ttf",
|
||||
},
|
||||
Rubik: {
|
||||
normal: "Rubik-Regular.ttf",
|
||||
bold: "Rubik-Bold.ttf",
|
||||
},
|
||||
};
|
||||
|
||||
function stripDataUrlPrefix(text: string): string {
|
||||
return text.substring("data:font/ttf;base64,".length);
|
||||
return text.substring("data:font/ttf;base64,".length);
|
||||
}
|
||||
|
||||
pdfMake.addVirtualFileSystem({
|
||||
"Roboto-Regular.ttf": stripDataUrlPrefix(RobotoRegular),
|
||||
"Roboto-Bold.ttf": stripDataUrlPrefix(RobotoBold),
|
||||
"Rubik-Regular.ttf": stripDataUrlPrefix(RubikRegular),
|
||||
"Rubik-Bold.ttf": stripDataUrlPrefix(RubikBold),
|
||||
"Roboto-Regular.ttf": stripDataUrlPrefix(RobotoRegular),
|
||||
"Roboto-Bold.ttf": stripDataUrlPrefix(RobotoBold),
|
||||
"Rubik-Regular.ttf": stripDataUrlPrefix(RubikRegular),
|
||||
"Rubik-Bold.ttf": stripDataUrlPrefix(RubikBold),
|
||||
});
|
||||
|
||||
pdfMake.addFonts(fonts);
|
||||
|
||||
export async function createPdfExport(
|
||||
timekeep: Timekeep,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
timekeep: Timekeep,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
): Promise<NodeJS.ReadableStream> {
|
||||
const definition = createPdfDefinition(timekeep, settings, currentTime);
|
||||
const pdf = pdfMake.createPdf(definition, {});
|
||||
const stream = await pdf.getBuffer();
|
||||
return Readable.from([stream]);
|
||||
const definition = createPdfDefinition(timekeep, settings, currentTime);
|
||||
const pdf = pdfMake.createPdf(definition, {});
|
||||
const stream = await pdf.getBuffer();
|
||||
return Readable.from([stream]);
|
||||
}
|
||||
|
||||
export function createPdfDefinition(
|
||||
timekeep: Timekeep,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
timekeep: Timekeep,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
): TDocumentDefinitions {
|
||||
const duration = getTotalDuration(timekeep.entries, currentTime);
|
||||
const currentDate = formatPdfDate(currentTime, settings);
|
||||
const totalDuration = formatDurationLong(duration);
|
||||
const totalDurationShort = formatDurationShort(duration);
|
||||
const duration = getTotalDuration(timekeep.entries, currentTime);
|
||||
const currentDate = formatPdfDate(currentTime, settings);
|
||||
const totalDuration = formatDurationLong(duration);
|
||||
const totalDurationShort = formatDurationShort(duration);
|
||||
|
||||
return {
|
||||
content: [
|
||||
createPdfHeader(
|
||||
settings.pdfTitle,
|
||||
currentDate,
|
||||
totalDuration,
|
||||
totalDurationShort
|
||||
),
|
||||
createPdfTable(timekeep, totalDuration, settings, currentTime),
|
||||
],
|
||||
footer: createPdfFooter(settings),
|
||||
defaultStyle: {
|
||||
font: settings.pdfFontFamily,
|
||||
},
|
||||
pageSize: "A4",
|
||||
pageMargins: 15,
|
||||
styles: {
|
||||
heading: {
|
||||
marginBottom: 5,
|
||||
columnGap: 10,
|
||||
},
|
||||
title: {
|
||||
bold: true,
|
||||
fontSize: 12,
|
||||
marginBottom: 5,
|
||||
},
|
||||
details: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
footNote: {
|
||||
noWrap: true,
|
||||
fontSize: 6,
|
||||
bold: true,
|
||||
},
|
||||
pageNumber: {
|
||||
noWrap: true,
|
||||
fontSize: 6,
|
||||
},
|
||||
footer: {
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
tableCell: {
|
||||
fontSize: 8,
|
||||
},
|
||||
tableCellTime: {
|
||||
alignment: "right",
|
||||
},
|
||||
tableCellHeader: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
content: [
|
||||
createPdfHeader(settings.pdfTitle, currentDate, totalDuration, totalDurationShort),
|
||||
createPdfTable(timekeep, totalDuration, settings, currentTime),
|
||||
],
|
||||
footer: createPdfFooter(settings),
|
||||
defaultStyle: {
|
||||
font: settings.pdfFontFamily,
|
||||
},
|
||||
pageSize: "A4",
|
||||
pageMargins: 15,
|
||||
styles: {
|
||||
heading: {
|
||||
marginBottom: 5,
|
||||
columnGap: 10,
|
||||
},
|
||||
title: {
|
||||
bold: true,
|
||||
fontSize: 12,
|
||||
marginBottom: 5,
|
||||
},
|
||||
details: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
footNote: {
|
||||
noWrap: true,
|
||||
fontSize: 6,
|
||||
bold: true,
|
||||
},
|
||||
pageNumber: {
|
||||
noWrap: true,
|
||||
fontSize: 6,
|
||||
},
|
||||
footer: {
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
tableCell: {
|
||||
fontSize: 8,
|
||||
},
|
||||
tableCellTime: {
|
||||
alignment: "right",
|
||||
},
|
||||
tableCellHeader: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPdfHeader(
|
||||
pdfTitle: string,
|
||||
currentDate: string,
|
||||
totalDuration: string,
|
||||
totalDurationShort: string
|
||||
pdfTitle: string,
|
||||
currentDate: string,
|
||||
totalDuration: string,
|
||||
totalDurationShort: string
|
||||
): pdfMake.Content {
|
||||
return [
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
text: pdfTitle,
|
||||
alignment: "left",
|
||||
style: "title",
|
||||
width: "50%",
|
||||
},
|
||||
{
|
||||
text: "Timesheet",
|
||||
alignment: "right",
|
||||
style: "title",
|
||||
width: "50%",
|
||||
},
|
||||
],
|
||||
style: "heading",
|
||||
},
|
||||
{
|
||||
stack: [
|
||||
createPdfHeaderDetailField("Date", currentDate),
|
||||
createPdfHeaderDetailField("Total Duration", totalDuration),
|
||||
createPdfHeaderDetailField(
|
||||
"Total Duration (hours)",
|
||||
totalDurationShort
|
||||
),
|
||||
],
|
||||
return [
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
text: pdfTitle,
|
||||
alignment: "left",
|
||||
style: "title",
|
||||
width: "50%",
|
||||
},
|
||||
{
|
||||
text: "Timesheet",
|
||||
alignment: "right",
|
||||
style: "title",
|
||||
width: "50%",
|
||||
},
|
||||
],
|
||||
style: "heading",
|
||||
},
|
||||
{
|
||||
stack: [
|
||||
createPdfHeaderDetailField("Date", currentDate),
|
||||
createPdfHeaderDetailField("Total Duration", totalDuration),
|
||||
createPdfHeaderDetailField("Total Duration (hours)", totalDurationShort),
|
||||
],
|
||||
|
||||
style: "details",
|
||||
},
|
||||
];
|
||||
style: "details",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function createPdfHeaderDetailField(
|
||||
title: string,
|
||||
value: string
|
||||
): pdfMake.Content {
|
||||
return {
|
||||
text: [
|
||||
{ text: title + ": ", fontSize: 8, bold: true },
|
||||
{ text: value, fontSize: 8 },
|
||||
],
|
||||
marginBottom: 8,
|
||||
};
|
||||
function createPdfHeaderDetailField(title: string, value: string): pdfMake.Content {
|
||||
return {
|
||||
text: [
|
||||
{ text: title + ": ", fontSize: 8, bold: true },
|
||||
{ text: value, fontSize: 8 },
|
||||
],
|
||||
marginBottom: 8,
|
||||
};
|
||||
}
|
||||
|
||||
function createPdfFooter(settings: TimekeepSettings): DynamicContent {
|
||||
return function (currentPage: number, pageCount: number) {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
text: `${settings.pdfFootnote}`,
|
||||
style: "footNote",
|
||||
width: "50%",
|
||||
alignment: "left",
|
||||
},
|
||||
{
|
||||
text: `${currentPage} of ${pageCount}`,
|
||||
style: "pageNumber",
|
||||
width: "50%",
|
||||
alignment: "right",
|
||||
},
|
||||
],
|
||||
style: "footer",
|
||||
};
|
||||
};
|
||||
return function (currentPage: number, pageCount: number) {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
text: `${settings.pdfFootnote}`,
|
||||
style: "footNote",
|
||||
width: "50%",
|
||||
alignment: "left",
|
||||
},
|
||||
{
|
||||
text: `${currentPage} of ${pageCount}`,
|
||||
style: "pageNumber",
|
||||
width: "50%",
|
||||
alignment: "right",
|
||||
},
|
||||
],
|
||||
style: "footer",
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function createPdfTable(
|
||||
timekeep: Timekeep,
|
||||
totalDuration: string,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
timekeep: Timekeep,
|
||||
totalDuration: string,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
): pdfMake.Content {
|
||||
const rows = createPdfTableRows(timekeep.entries, settings, currentTime);
|
||||
const rows = createPdfTableRows(timekeep.entries, settings, currentTime);
|
||||
|
||||
return {
|
||||
table: {
|
||||
headerRows: 1,
|
||||
widths: ["*", 70, 70, 50],
|
||||
body: [
|
||||
[
|
||||
{
|
||||
text: "Block Name",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
border: [true, false, false, true],
|
||||
},
|
||||
{
|
||||
text: "Start Time",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
alignment: "center",
|
||||
border: [false, false, false, true],
|
||||
},
|
||||
{
|
||||
text: "End Time",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
alignment: "center",
|
||||
border: [false, false, false, true],
|
||||
},
|
||||
{
|
||||
text: "Duration",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
alignment: "right",
|
||||
border: [false, false, true, true],
|
||||
},
|
||||
],
|
||||
...rows.map((row) => row.row),
|
||||
[
|
||||
{ text: "Total", style: "tableCell" },
|
||||
{ text: "", style: "tableCell" },
|
||||
{ text: "", style: "tableCell" },
|
||||
{
|
||||
text: totalDuration,
|
||||
style: ["tableCell", "tableCellFooter"],
|
||||
bold: true,
|
||||
alignment: "right",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
return {
|
||||
table: {
|
||||
headerRows: 1,
|
||||
widths: ["*", 70, 70, 50],
|
||||
body: [
|
||||
[
|
||||
{
|
||||
text: "Block Name",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
border: [true, false, false, true],
|
||||
},
|
||||
{
|
||||
text: "Start Time",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
alignment: "center",
|
||||
border: [false, false, false, true],
|
||||
},
|
||||
{
|
||||
text: "End Time",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
alignment: "center",
|
||||
border: [false, false, false, true],
|
||||
},
|
||||
{
|
||||
text: "Duration",
|
||||
style: ["tableCell", "tableCellHeader"],
|
||||
alignment: "right",
|
||||
border: [false, false, true, true],
|
||||
},
|
||||
],
|
||||
...rows.map((row) => row.row),
|
||||
[
|
||||
{ text: "Total", style: "tableCell" },
|
||||
{ text: "", style: "tableCell" },
|
||||
{ text: "", style: "tableCell" },
|
||||
{
|
||||
text: totalDuration,
|
||||
style: ["tableCell", "tableCellFooter"],
|
||||
bold: true,
|
||||
alignment: "right",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
layout: {
|
||||
fillColor: function (rowIndex, _node, _columnIndex) {
|
||||
// Header & Footer styling
|
||||
if (rowIndex === 0 || rowIndex === rows.length + 1) {
|
||||
return "#ececec";
|
||||
}
|
||||
layout: {
|
||||
fillColor: function (rowIndex, _node, _columnIndex) {
|
||||
// Header & Footer styling
|
||||
if (rowIndex === 0 || rowIndex === rows.length + 1) {
|
||||
return "#ececec";
|
||||
}
|
||||
|
||||
// Group row styling
|
||||
if (rowIndex > 0 && rowIndex - 1 < rows.length) {
|
||||
if (rows[rowIndex - 1].group) {
|
||||
return "#f4f4f4";
|
||||
}
|
||||
}
|
||||
// Group row styling
|
||||
if (rowIndex > 0 && rowIndex - 1 < rows.length) {
|
||||
if (rows[rowIndex - 1].group) {
|
||||
return "#f4f4f4";
|
||||
}
|
||||
}
|
||||
|
||||
// Default styling
|
||||
return "#ffffff";
|
||||
},
|
||||
// Default styling
|
||||
return "#ffffff";
|
||||
},
|
||||
|
||||
vLineColor: function (_columnIndex, _node, _rowIndex) {
|
||||
return "#ececec";
|
||||
},
|
||||
vLineColor: function (_columnIndex, _node, _rowIndex) {
|
||||
return "#ececec";
|
||||
},
|
||||
|
||||
hLineColor: function (_rowIndex, _node, _columnIndex) {
|
||||
return "#ececec";
|
||||
},
|
||||
paddingBottom: cellPadding,
|
||||
paddingLeft: cellPadding,
|
||||
paddingTop: cellPadding,
|
||||
paddingRight: cellPadding,
|
||||
},
|
||||
};
|
||||
hLineColor: function (_rowIndex, _node, _columnIndex) {
|
||||
return "#ececec";
|
||||
},
|
||||
paddingBottom: cellPadding,
|
||||
paddingLeft: cellPadding,
|
||||
paddingTop: cellPadding,
|
||||
paddingRight: cellPadding,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const cellPadding = () => 8;
|
||||
|
||||
type TableEntryRow = {
|
||||
row: TableCell[];
|
||||
depth: number;
|
||||
group: boolean;
|
||||
row: TableCell[];
|
||||
depth: number;
|
||||
group: boolean;
|
||||
};
|
||||
|
||||
function createPdfTableRows(
|
||||
entries: TimeEntry[],
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
entries: TimeEntry[],
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
): TableEntryRow[] {
|
||||
type StackEntry = { entry: TimeEntry; depth: number };
|
||||
type StackEntry = { entry: TimeEntry; depth: number };
|
||||
|
||||
const rows: TableEntryRow[] = [];
|
||||
const stack: StackEntry[] = entries.map((entry) => ({ entry, depth: 0 }));
|
||||
const rows: TableEntryRow[] = [];
|
||||
const stack: StackEntry[] = entries.map((entry) => ({ entry, depth: 0 }));
|
||||
|
||||
while (stack.length > 0) {
|
||||
const { entry, depth } = stack.pop()!;
|
||||
while (stack.length > 0) {
|
||||
const { entry, depth } = stack.pop()!;
|
||||
|
||||
rows.push({
|
||||
group: entry.subEntries !== null && entry.subEntries.length > 0,
|
||||
row: createTableEntryCells(entry, depth, settings, currentTime),
|
||||
depth,
|
||||
});
|
||||
rows.push({
|
||||
group: entry.subEntries !== null && entry.subEntries.length > 0,
|
||||
row: createTableEntryCells(entry, depth, settings, currentTime),
|
||||
depth,
|
||||
});
|
||||
|
||||
if (entry.subEntries && entry.subEntries.length > 0) {
|
||||
for (let i = entry.subEntries.length - 1; i >= 0; i--) {
|
||||
stack.push({
|
||||
entry: entry.subEntries[i],
|
||||
depth: depth + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (entry.subEntries && entry.subEntries.length > 0) {
|
||||
for (let i = entry.subEntries.length - 1; i >= 0; i--) {
|
||||
stack.push({
|
||||
entry: entry.subEntries[i],
|
||||
depth: depth + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
return rows;
|
||||
}
|
||||
|
||||
function createTableEntryCells(
|
||||
entry: TimeEntry,
|
||||
depth: number,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
entry: TimeEntry,
|
||||
depth: number,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
): TableCell[] {
|
||||
const duration = getEntryDuration(entry, currentTime);
|
||||
const durationFormatted = formatDurationLong(duration);
|
||||
const duration = getEntryDuration(entry, currentTime);
|
||||
const durationFormatted = formatDurationLong(duration);
|
||||
|
||||
let startTime: string;
|
||||
let endTime: string;
|
||||
let startTime: string;
|
||||
let endTime: string;
|
||||
|
||||
if (entry.startTime !== null) {
|
||||
startTime = formatPdfRowDate(entry.startTime, settings);
|
||||
endTime = formatPdfRowDate(entry.endTime ?? currentTime, settings);
|
||||
} else {
|
||||
startTime = "";
|
||||
endTime = "";
|
||||
}
|
||||
if (entry.startTime !== null) {
|
||||
startTime = formatPdfRowDate(entry.startTime, settings);
|
||||
endTime = formatPdfRowDate(entry.endTime ?? currentTime, settings);
|
||||
} else {
|
||||
startTime = "";
|
||||
endTime = "";
|
||||
}
|
||||
|
||||
const marginLeft = depth * 5;
|
||||
const marginLeft = depth * 5;
|
||||
|
||||
if (depth > 0) {
|
||||
const _bg = "#999";
|
||||
}
|
||||
if (depth > 0) {
|
||||
const _bg = "#999";
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
text: entry.name,
|
||||
style: ["tableCell", "tableCellBlock"],
|
||||
marginLeft,
|
||||
border: [true, false, false, true],
|
||||
},
|
||||
{
|
||||
text: startTime,
|
||||
style: ["tableCell", "tableCellTime"],
|
||||
alignment: "center",
|
||||
},
|
||||
{
|
||||
text: endTime,
|
||||
style: ["tableCell", "tableCellTime"],
|
||||
alignment: "center",
|
||||
},
|
||||
{
|
||||
text: durationFormatted,
|
||||
style: ["tableCell", "tableCellTime"],
|
||||
alignment: "right",
|
||||
border: [false, false, true, true],
|
||||
},
|
||||
];
|
||||
return [
|
||||
{
|
||||
text: entry.name,
|
||||
style: ["tableCell", "tableCellBlock"],
|
||||
marginLeft,
|
||||
border: [true, false, false, true],
|
||||
},
|
||||
{
|
||||
text: startTime,
|
||||
style: ["tableCell", "tableCellTime"],
|
||||
alignment: "center",
|
||||
},
|
||||
{
|
||||
text: endTime,
|
||||
style: ["tableCell", "tableCellTime"],
|
||||
alignment: "center",
|
||||
},
|
||||
{
|
||||
text: durationFormatted,
|
||||
style: ["tableCell", "tableCellTime"],
|
||||
alignment: "right",
|
||||
border: [false, false, true, true],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,25 +63,11 @@ export class TimesheetApp extends Component {
|
|||
|
||||
this.#wrapperEl = wrapperEl;
|
||||
|
||||
const counters = new TimesheetCounters(
|
||||
wrapperEl,
|
||||
this.settings,
|
||||
this.timekeep
|
||||
);
|
||||
const counters = new TimesheetCounters(wrapperEl, this.settings, this.timekeep);
|
||||
|
||||
const start = new TimesheetStart(
|
||||
wrapperEl,
|
||||
this.app,
|
||||
this.timekeep,
|
||||
this.settings
|
||||
);
|
||||
const start = new TimesheetStart(wrapperEl, this.app, this.timekeep, this.settings);
|
||||
|
||||
const table = new TimesheetTable(
|
||||
wrapperEl,
|
||||
this.app,
|
||||
this.timekeep,
|
||||
this.settings
|
||||
);
|
||||
const table = new TimesheetTable(wrapperEl, this.app, this.timekeep, this.settings);
|
||||
|
||||
const exportActions = new TimesheetExportActions(
|
||||
wrapperEl,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ import type { Store } from "@/store";
|
|||
import type { Timekeep } from "@/timekeep/schema";
|
||||
import { Component } from "obsidian";
|
||||
import moment from "moment";
|
||||
import {
|
||||
getEntryDuration,
|
||||
getRunningEntry,
|
||||
getTotalDuration,
|
||||
isKeepRunning,
|
||||
} from "@/timekeep";
|
||||
import { getEntryDuration, getRunningEntry, getTotalDuration, isKeepRunning } from "@/timekeep";
|
||||
import { formatDuration } from "@/utils";
|
||||
import { TimesheetTimer } from "./timesheetTimer";
|
||||
|
||||
|
|
@ -96,10 +91,7 @@ export class TimesheetCounters extends Component {
|
|||
// Only schedule further updates if we are running
|
||||
const timekeep = this.timekeep.getState();
|
||||
if (isKeepRunning(timekeep)) {
|
||||
const intervalID = window.setInterval(
|
||||
this.updateTimers.bind(this),
|
||||
1000
|
||||
);
|
||||
const intervalID = window.setInterval(this.updateTimers.bind(this), 1000);
|
||||
|
||||
this.currentContentInterval = intervalID;
|
||||
this.registerInterval(intervalID);
|
||||
|
|
@ -116,9 +108,7 @@ export class TimesheetCounters extends Component {
|
|||
const currentTime = moment();
|
||||
const total = getTotalDuration(timekeep.entries, currentTime);
|
||||
const runningEntry = getRunningEntry(timekeep.entries);
|
||||
const current = runningEntry
|
||||
? getEntryDuration(runningEntry, currentTime)
|
||||
: 0;
|
||||
const current = runningEntry ? getEntryDuration(runningEntry, currentTime) : 0;
|
||||
|
||||
this.currentTimer.setHidden(runningEntry === null);
|
||||
this.currentTimer.setValues(
|
||||
|
|
|
|||
|
|
@ -66,29 +66,13 @@ export class TimesheetExportActions extends Component {
|
|||
text: "Save PDF",
|
||||
});
|
||||
|
||||
this.registerDomEvent(
|
||||
copyMarkdownButton,
|
||||
"click",
|
||||
this.onCopyMarkdown.bind(this)
|
||||
);
|
||||
this.registerDomEvent(copyMarkdownButton, "click", this.onCopyMarkdown.bind(this));
|
||||
|
||||
this.registerDomEvent(
|
||||
copyCSVButton,
|
||||
"click",
|
||||
this.onCopyCSV.bind(this)
|
||||
);
|
||||
this.registerDomEvent(copyCSVButton, "click", this.onCopyCSV.bind(this));
|
||||
|
||||
this.registerDomEvent(
|
||||
copyJSONButton,
|
||||
"click",
|
||||
this.onCopyJSON.bind(this)
|
||||
);
|
||||
this.registerDomEvent(copyJSONButton, "click", this.onCopyJSON.bind(this));
|
||||
|
||||
this.registerDomEvent(
|
||||
savePdfButton,
|
||||
"click",
|
||||
this.onSavePDF.bind(this)
|
||||
);
|
||||
this.registerDomEvent(savePdfButton, "click", this.onSavePDF.bind(this));
|
||||
|
||||
// Disable and hide the save as PDF button on mobile
|
||||
if (Platform.isMobileApp) {
|
||||
|
|
@ -96,10 +80,8 @@ export class TimesheetExportActions extends Component {
|
|||
savePdfButton.hidden = true;
|
||||
}
|
||||
|
||||
const createCustomButtons =
|
||||
this.createCustomOutputFormatButtons.bind(this);
|
||||
const unsubscribeCustomFormats =
|
||||
this.customOutputFormats.subscribe(createCustomButtons);
|
||||
const createCustomButtons = this.createCustomOutputFormatButtons.bind(this);
|
||||
const unsubscribeCustomFormats = this.customOutputFormats.subscribe(createCustomButtons);
|
||||
this.register(unsubscribeCustomFormats);
|
||||
createCustomButtons();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,9 +98,7 @@ export class TimesheetRowContainer extends Component {
|
|||
*
|
||||
* @param content The new content to show
|
||||
*/
|
||||
private swapContent(
|
||||
content: TimesheetRowContent | TimesheetRowContentEditing | undefined
|
||||
) {
|
||||
private swapContent(content: TimesheetRowContent | TimesheetRowContentEditing | undefined) {
|
||||
if (this.#content) {
|
||||
this.removeChild(this.#content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,11 +84,7 @@ export class TimesheetRowContent extends Component {
|
|||
title: entry.name,
|
||||
});
|
||||
|
||||
this.registerDomEvent(
|
||||
nameEl,
|
||||
"click",
|
||||
this.onToggleCollapsed.bind(this)
|
||||
);
|
||||
this.registerDomEvent(nameEl, "click", this.onToggleCollapsed.bind(this));
|
||||
|
||||
if (entry.subEntries !== null && entry.folder) {
|
||||
createObsidianIcon(nameEl, "folder", "timekeep-folder-icon");
|
||||
|
|
@ -131,10 +127,7 @@ export class TimesheetRowContent extends Component {
|
|||
cls: ["timekeep-col", "timekeep-col--duration"],
|
||||
});
|
||||
|
||||
const duration = new TimesheetRowDurationComponent(
|
||||
durationColEl,
|
||||
entry
|
||||
);
|
||||
const duration = new TimesheetRowDurationComponent(durationColEl, entry);
|
||||
|
||||
this.addChild(duration);
|
||||
|
||||
|
|
@ -158,30 +151,18 @@ export class TimesheetRowContent extends Component {
|
|||
|
||||
createObsidianIcon(editButton, "edit", "button-icon");
|
||||
|
||||
this.registerDomEvent(
|
||||
startButton,
|
||||
"click",
|
||||
this.onClickStart.bind(this)
|
||||
);
|
||||
this.registerDomEvent(startButton, "click", this.onClickStart.bind(this));
|
||||
|
||||
this.registerDomEvent(editButton, "click", this.onBeginEditing);
|
||||
|
||||
this.updateTimes();
|
||||
this.updateState();
|
||||
|
||||
const unsubscribeSettings = this.settings.subscribe(
|
||||
this.updateTimes.bind(this)
|
||||
);
|
||||
const unsubscribeSettings = this.settings.subscribe(this.updateTimes.bind(this));
|
||||
|
||||
this.register(unsubscribeSettings);
|
||||
|
||||
this.#columns.push(
|
||||
nameColEl,
|
||||
startTimeColEl,
|
||||
endTimeColEl,
|
||||
durationColEl,
|
||||
actionsColEl
|
||||
);
|
||||
this.#columns.push(nameColEl, startTimeColEl, endTimeColEl, durationColEl, actionsColEl);
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
|
|
@ -203,9 +184,7 @@ export class TimesheetRowContent extends Component {
|
|||
? formatTimestamp(entry.startTime, settings)
|
||||
: "";
|
||||
|
||||
this.#endTimeEl.textContent = entry.endTime
|
||||
? formatTimestamp(entry.endTime, settings)
|
||||
: "";
|
||||
this.#endTimeEl.textContent = entry.endTime ? formatTimestamp(entry.endTime, settings) : "";
|
||||
}
|
||||
|
||||
updateState() {
|
||||
|
|
@ -213,12 +192,10 @@ export class TimesheetRowContent extends Component {
|
|||
|
||||
const entry = this.entry;
|
||||
|
||||
const isSelfRunning =
|
||||
entry.subEntries === null && isEntryRunning(entry);
|
||||
const isSelfRunning = entry.subEntries === null && isEntryRunning(entry);
|
||||
|
||||
const isRunningWithin =
|
||||
entry.subEntries !== null &&
|
||||
getRunningEntry(entry.subEntries) !== null;
|
||||
entry.subEntries !== null && getRunningEntry(entry.subEntries) !== null;
|
||||
|
||||
const isInvalidEntry =
|
||||
entry.startTime !== null &&
|
||||
|
|
@ -229,10 +206,7 @@ export class TimesheetRowContent extends Component {
|
|||
|
||||
rowEl.setAttribute("data-running", String(isSelfRunning));
|
||||
rowEl.setAttribute("data-running-within", String(isRunningWithin));
|
||||
rowEl.setAttribute(
|
||||
"data-sub-entires",
|
||||
String(this.entry.subEntries !== null)
|
||||
);
|
||||
rowEl.setAttribute("data-sub-entires", String(this.entry.subEntries !== null));
|
||||
rowEl.setAttribute("data-invalid", String(isInvalidEntry));
|
||||
}
|
||||
|
||||
|
|
@ -252,11 +226,7 @@ export class TimesheetRowContent extends Component {
|
|||
|
||||
this.timekeep.setState((timekeep) => {
|
||||
const currentTime = moment();
|
||||
const entries = startNewNestedEntry(
|
||||
currentTime,
|
||||
entry.id,
|
||||
timekeep.entries
|
||||
);
|
||||
const entries = startNewNestedEntry(currentTime, entry.id, timekeep.entries);
|
||||
|
||||
return {
|
||||
...timekeep,
|
||||
|
|
|
|||
|
|
@ -129,11 +129,7 @@ export class TimesheetRowContentEditing extends Component {
|
|||
createObsidianIcon(deleteButton, "trash", "text-button-icon");
|
||||
deleteButton.appendText("Delete");
|
||||
|
||||
this.registerDomEvent(
|
||||
deleteButton,
|
||||
"click",
|
||||
this.onConfirmDelete.bind(this)
|
||||
);
|
||||
this.registerDomEvent(deleteButton, "click", this.onConfirmDelete.bind(this));
|
||||
|
||||
const onUpdateState = this.onUpdateState.bind(this);
|
||||
const unsubscribeSettings = this.settings.subscribe(onUpdateState);
|
||||
|
|
@ -196,11 +192,7 @@ export class TimesheetRowContentEditing extends Component {
|
|||
}
|
||||
|
||||
onSubmit(event: Event) {
|
||||
if (
|
||||
!this.#nameInputEl ||
|
||||
!this.#startTimeInputEl ||
|
||||
!this.#endTimeInputEl
|
||||
) {
|
||||
if (!this.#nameInputEl || !this.#startTimeInputEl || !this.#endTimeInputEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -219,10 +211,7 @@ export class TimesheetRowContentEditing extends Component {
|
|||
// Update the start and end times for non groups
|
||||
if (newEntry.subEntries === null) {
|
||||
if (entry.startTime !== null) {
|
||||
const startTimeValue = parseEditableTimestamp(
|
||||
startTime,
|
||||
settings
|
||||
);
|
||||
const startTimeValue = parseEditableTimestamp(startTime, settings);
|
||||
if (startTimeValue.isValid()) {
|
||||
newEntry.startTime = startTimeValue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,10 +46,7 @@ export class TimesheetRowDurationComponent extends Component {
|
|||
|
||||
// Only schedule further updates if we are running
|
||||
if (isRunning) {
|
||||
const intervalID = window.setInterval(
|
||||
this.updateTime.bind(this),
|
||||
1000
|
||||
);
|
||||
const intervalID = window.setInterval(this.updateTime.bind(this), 1000);
|
||||
|
||||
this.currentContentInterval = intervalID;
|
||||
this.registerInterval(intervalID);
|
||||
|
|
|
|||
|
|
@ -57,11 +57,7 @@ export class TimesheetSaveError extends Component {
|
|||
text: "Copy Timekeep",
|
||||
});
|
||||
|
||||
this.registerDomEvent(
|
||||
retryButton,
|
||||
"click",
|
||||
this.onRetrySave.bind(this)
|
||||
);
|
||||
this.registerDomEvent(retryButton, "click", this.onRetrySave.bind(this));
|
||||
|
||||
this.registerDomEvent(copyButton, "click", this.onCopy.bind(this));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,9 +117,7 @@ export class TimesheetStart extends Component {
|
|||
*
|
||||
* @param content The new content to show
|
||||
*/
|
||||
private swapContent(
|
||||
content: TimesheetStartRunning | TimekeepStartEditing | undefined
|
||||
) {
|
||||
private swapContent(content: TimesheetStartRunning | TimekeepStartEditing | undefined) {
|
||||
if (this.#content) {
|
||||
this.removeChild(this.#content);
|
||||
}
|
||||
|
|
@ -149,12 +147,9 @@ export class TimesheetStart extends Component {
|
|||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
const isTimekeepRunning = currentEntry !== null;
|
||||
|
||||
this.#blockPauseWarningEl.hidden =
|
||||
currentEntry === null || currentEntry.startTime === null;
|
||||
this.#blockPauseWarningEl.hidden = currentEntry === null || currentEntry.startTime === null;
|
||||
|
||||
this.#startButtonEl.title = isTimekeepRunning
|
||||
? "Stop and start"
|
||||
: "Start";
|
||||
this.#startButtonEl.title = isTimekeepRunning ? "Stop and start" : "Start";
|
||||
}
|
||||
|
||||
setEmptyView() {
|
||||
|
|
|
|||
|
|
@ -139,10 +139,7 @@ export class TimesheetStartRunning extends Component {
|
|||
const timekeep = this.timekeep.getState();
|
||||
const settings = this.settings.getState();
|
||||
|
||||
this.#timeValueEl.textContent = formatTimestamp(
|
||||
currentEntry.startTime,
|
||||
settings
|
||||
);
|
||||
this.#timeValueEl.textContent = formatTimestamp(currentEntry.startTime, settings);
|
||||
|
||||
// Clear existing path
|
||||
this.#pathEl.empty();
|
||||
|
|
@ -151,9 +148,7 @@ export class TimesheetStartRunning extends Component {
|
|||
if (pathToEntry && pathToEntry.length > 0) {
|
||||
for (let i = 0; i < pathToEntry.length; i++) {
|
||||
const path = pathToEntry[i];
|
||||
const text = `${path.name} ${
|
||||
i < pathToEntry.length - 1 ? " >" : ""
|
||||
}`;
|
||||
const text = `${path.name} ${i < pathToEntry.length - 1 ? " >" : ""}`;
|
||||
this.#pathEl.createSpan({ text });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,15 +151,8 @@ export class TimesheetTable extends Component {
|
|||
this.addChild(row);
|
||||
this.#rows.push(row);
|
||||
|
||||
if (
|
||||
entry.subEntries &&
|
||||
!entry.collapsed &&
|
||||
entry.subEntries.length > 0
|
||||
) {
|
||||
const sortedEntries = getEntriesSorted(
|
||||
entry.subEntries,
|
||||
settings
|
||||
);
|
||||
if (entry.subEntries && !entry.collapsed && entry.subEntries.length > 0) {
|
||||
const sortedEntries = getEntriesSorted(entry.subEntries, settings);
|
||||
|
||||
for (let i = sortedEntries.length - 1; i >= 0; i--) {
|
||||
stack.push({
|
||||
|
|
|
|||
|
|
@ -25,9 +25,7 @@ export function createRawTable(
|
|||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
): RawTableRow[] {
|
||||
return entries.flatMap((entry) =>
|
||||
createRawTableEntries(entry, settings, currentTime)
|
||||
);
|
||||
return entries.flatMap((entry) => createRawTableEntries(entry, settings, currentTime));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -51,8 +49,7 @@ export function createRawTableEntries(
|
|||
entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
|
||||
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
|
||||
// Include duration for entries that are finished
|
||||
(entry.startTime !== null && entry.endTime !== null) ||
|
||||
entry.subEntries !== null
|
||||
(entry.startTime !== null && entry.endTime !== null) || entry.subEntries !== null
|
||||
? formatDuration(
|
||||
settings.exportDurationFormat,
|
||||
getEntryDuration(entry, currentTime)
|
||||
|
|
|
|||
|
|
@ -27,10 +27,7 @@ function createFooter(
|
|||
currentTime: Moment,
|
||||
durationFormat: DurationFormat
|
||||
): RawTableRow {
|
||||
const total: string = formatDuration(
|
||||
durationFormat,
|
||||
getTotalDuration(entries, currentTime)
|
||||
);
|
||||
const total: string = formatDuration(durationFormat, getTotalDuration(entries, currentTime));
|
||||
return ["**Total**", "", "", `**${total}**`];
|
||||
}
|
||||
|
||||
|
|
@ -73,11 +70,7 @@ export function createMarkdownTable(
|
|||
// Markdown raw table contents
|
||||
...createRawTable(timekeep.entries, settings, currentTime),
|
||||
// Markdown footer row
|
||||
createFooter(
|
||||
timekeep.entries,
|
||||
currentTime,
|
||||
settings.exportDurationFormat
|
||||
),
|
||||
createFooter(timekeep.entries, currentTime, settings.exportDurationFormat),
|
||||
];
|
||||
|
||||
// Array of indexes for all the columns (0 - TOTAL_COLUMNS)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import { Timekeep } from "@/timekeep/schema";
|
|||
import { mkdir, writeFile } from "fs/promises";
|
||||
import { TimekeepSettings, PdfExportBehavior } from "@/settings";
|
||||
|
||||
export async function exportPdf(
|
||||
timekeep: Timekeep,
|
||||
settings: TimekeepSettings
|
||||
) {
|
||||
export async function exportPdf(timekeep: Timekeep, settings: TimekeepSettings) {
|
||||
// Pdf exports don't work in mobile mode
|
||||
if (Platform.isMobileApp) return;
|
||||
|
||||
|
|
|
|||
51
src/main.ts
51
src/main.ts
|
|
@ -1,16 +1,8 @@
|
|||
import moment, { Moment } from "moment";
|
||||
import { Store, createStore } from "@/store";
|
||||
import { TimekeepSettingsTab } from "@/settings-tab";
|
||||
import {
|
||||
defaultSettings,
|
||||
TimekeepSettings,
|
||||
legacySettingsCompatibility,
|
||||
} from "@/settings";
|
||||
import {
|
||||
load,
|
||||
replaceTimekeepCodeblock,
|
||||
extractTimekeepCodeblocks,
|
||||
} from "@/timekeep/parser";
|
||||
import { defaultSettings, TimekeepSettings, legacySettingsCompatibility } from "@/settings";
|
||||
import { load, replaceTimekeepCodeblock, extractTimekeepCodeblocks } from "@/timekeep/parser";
|
||||
import {
|
||||
isKeepRunning,
|
||||
isEntryRunning,
|
||||
|
|
@ -53,11 +45,7 @@ export default class TimekeepPlugin extends Plugin {
|
|||
getEntryDuration: (entry: TimeEntry, currentTime: Moment) => number;
|
||||
getTotalDuration: (entries: TimeEntry[], currentTime: Moment) => number;
|
||||
stopAllTimekeeps: (vault: Vault, currentTime: Moment) => Promise<number>;
|
||||
stopFileTimekeeps: (
|
||||
vault: Vault,
|
||||
file: TFile,
|
||||
currentTime: Moment
|
||||
) => Promise<number>;
|
||||
stopFileTimekeeps: (vault: Vault, file: TFile, currentTime: Moment) => Promise<number>;
|
||||
|
||||
constructor(app: ObsidianApp, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
|
|
@ -103,11 +91,7 @@ export default class TimekeepPlugin extends Plugin {
|
|||
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"timekeep",
|
||||
(
|
||||
source: string,
|
||||
el: HTMLElement,
|
||||
context: MarkdownPostProcessorContext
|
||||
) => {
|
||||
(source: string, el: HTMLElement, context: MarkdownPostProcessorContext) => {
|
||||
const loadResult = load(source);
|
||||
|
||||
context.addChild(
|
||||
|
|
@ -140,23 +124,13 @@ export default class TimekeepPlugin extends Plugin {
|
|||
this.addCommand({
|
||||
id: `create-merged`,
|
||||
name: `Create Merged Tracker`,
|
||||
callback: () =>
|
||||
new TimekeepMergerModal(
|
||||
this.app,
|
||||
this.settingsStore,
|
||||
false
|
||||
).open(),
|
||||
callback: () => new TimekeepMergerModal(this.app, this.settingsStore, false).open(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: `export-merged-pdf`,
|
||||
name: `Export Merged Tracker PDF`,
|
||||
callback: () =>
|
||||
new TimekeepMergerModal(
|
||||
this.app,
|
||||
this.settingsStore,
|
||||
true
|
||||
).open(),
|
||||
callback: () => new TimekeepMergerModal(this.app, this.settingsStore, true).open(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
|
|
@ -186,10 +160,7 @@ export default class TimekeepPlugin extends Plugin {
|
|||
error = "Unknown error occurred";
|
||||
}
|
||||
|
||||
new Notice(
|
||||
"Failed to stop timekeeps: " + errorMessage,
|
||||
1500
|
||||
);
|
||||
new Notice("Failed to stop timekeeps: " + errorMessage, 1500);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -199,8 +170,7 @@ export default class TimekeepPlugin extends Plugin {
|
|||
name: `Stop All Running Trackers (Current File Only)`,
|
||||
callback: () => {
|
||||
const currentTime = moment();
|
||||
const currentFile =
|
||||
this.app.workspace.activeEditor?.file ?? null;
|
||||
const currentFile = this.app.workspace.activeEditor?.file ?? null;
|
||||
|
||||
if (currentFile === null) {
|
||||
new Notice("No active file detected", 1500);
|
||||
|
|
@ -229,10 +199,7 @@ export default class TimekeepPlugin extends Plugin {
|
|||
error = "Unknown error occurred";
|
||||
}
|
||||
|
||||
new Notice(
|
||||
"Failed to stop timekeeps: " + errorMessage,
|
||||
1500
|
||||
);
|
||||
new Notice("Failed to stop timekeeps: " + errorMessage, 1500);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,9 +17,5 @@ export interface CustomOutputFormat {
|
|||
* @param settings The timekeep settings
|
||||
* @param currentTime The current time to use for unfinished entries
|
||||
*/
|
||||
onExport(
|
||||
timekeep: Timekeep,
|
||||
settings: TimekeepSettings,
|
||||
currentTime: Moment
|
||||
): void;
|
||||
onExport(timekeep: Timekeep, settings: TimekeepSettings, currentTime: Moment): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,9 +43,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
t.setValue(String(settings.timestampFormat));
|
||||
t.onChange((v) => {
|
||||
// Only use a custom format if the value is not blank
|
||||
const newFormat = v.length
|
||||
? v
|
||||
: defaultSettings.timestampFormat;
|
||||
const newFormat = v.length ? v : defaultSettings.timestampFormat;
|
||||
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
|
|
@ -56,18 +54,13 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Primary duration format")
|
||||
.setDesc(
|
||||
"Format to show durations for the current and total timers"
|
||||
)
|
||||
.setDesc("Format to show durations for the current and total timers")
|
||||
|
||||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
[DurationFormat.LONG]:
|
||||
"Long - Format including all units (1h 30m 25s)",
|
||||
[DurationFormat.SHORT]:
|
||||
"Short - Format just including hours (1.5h)",
|
||||
[DurationFormat.DECIMAL]:
|
||||
"Decimal - Short format without units (1.5)",
|
||||
[DurationFormat.LONG]: "Long - Format including all units (1h 30m 25s)",
|
||||
[DurationFormat.SHORT]: "Short - Format just including hours (1.5h)",
|
||||
[DurationFormat.DECIMAL]: "Decimal - Short format without units (1.5)",
|
||||
});
|
||||
t.setValue(String(settings.primaryDurationFormat));
|
||||
t.onChange((v) => {
|
||||
|
|
@ -80,18 +73,13 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Secondary duration format")
|
||||
.setDesc(
|
||||
"Format to show a second durations under the current and total timers"
|
||||
)
|
||||
.setDesc("Format to show a second durations under the current and total timers")
|
||||
|
||||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
[DurationFormat.LONG]:
|
||||
"Long - Format including all units (1h 30m 25s)",
|
||||
[DurationFormat.SHORT]:
|
||||
"Short - Format just including hours (1.5h)",
|
||||
[DurationFormat.DECIMAL]:
|
||||
"Decimal - Short format without units (1.5)",
|
||||
[DurationFormat.LONG]: "Long - Format including all units (1h 30m 25s)",
|
||||
[DurationFormat.SHORT]: "Short - Format just including hours (1.5h)",
|
||||
[DurationFormat.DECIMAL]: "Decimal - Short format without units (1.5)",
|
||||
[DurationFormat.NONE]: "None - No time is displayed",
|
||||
});
|
||||
t.setValue(String(settings.secondaryDurationFormat));
|
||||
|
|
@ -105,9 +93,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Sort order")
|
||||
.setDesc(
|
||||
"How entries should be sorted both when viewing and exporting"
|
||||
)
|
||||
.setDesc("How entries should be sorted both when viewing and exporting")
|
||||
|
||||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
|
|
@ -175,12 +161,9 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
|
||||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
[DurationFormat.LONG]:
|
||||
"Long - Format including all units (1h 30m 25s)",
|
||||
[DurationFormat.SHORT]:
|
||||
"Short - Format just including hours (1.5h)",
|
||||
[DurationFormat.DECIMAL]:
|
||||
"Decimal - Short format without units (1.5)",
|
||||
[DurationFormat.LONG]: "Long - Format including all units (1h 30m 25s)",
|
||||
[DurationFormat.SHORT]: "Short - Format just including hours (1.5h)",
|
||||
[DurationFormat.DECIMAL]: "Decimal - Short format without units (1.5)",
|
||||
});
|
||||
t.setValue(String(settings.exportDurationFormat));
|
||||
t.onChange((v) => {
|
||||
|
|
@ -219,9 +202,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
t.setValue(String(settings.pdfFootnote));
|
||||
t.onChange((v) => {
|
||||
// Only use a custom format if the value is not blank
|
||||
const newPdfFootnote = v.length
|
||||
? v
|
||||
: defaultSettings.pdfFootnote;
|
||||
const newPdfFootnote = v.length ? v : defaultSettings.pdfFootnote;
|
||||
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
|
|
@ -237,10 +218,8 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
[PdfExportBehavior.NONE]: "Do nothing",
|
||||
[PdfExportBehavior.OPEN_FILE]:
|
||||
"Open exported file with default app",
|
||||
[PdfExportBehavior.OPEN_PATH]:
|
||||
"Open directory containing the exported file",
|
||||
[PdfExportBehavior.OPEN_FILE]: "Open exported file with default app",
|
||||
[PdfExportBehavior.OPEN_PATH]: "Open directory containing the exported file",
|
||||
});
|
||||
t.setValue(String(settings.pdfExportBehavior));
|
||||
t.onChange((v) => {
|
||||
|
|
@ -269,9 +248,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
t.setValue(String(settings.pdfDateFormat));
|
||||
t.onChange((v) => {
|
||||
// Only use a custom format if the value is not blank
|
||||
const newPdfDateFormat = v.length
|
||||
? v
|
||||
: defaultSettings.pdfDateFormat;
|
||||
const newPdfDateFormat = v.length ? v : defaultSettings.pdfDateFormat;
|
||||
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
|
|
@ -298,9 +275,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
t.setValue(String(settings.pdfRowDateFormat));
|
||||
t.onChange((v) => {
|
||||
// Only use a custom format if the value is not blank
|
||||
const newPdfRowDateFormat = v.length
|
||||
? v
|
||||
: defaultSettings.pdfRowDateFormat;
|
||||
const newPdfRowDateFormat = v.length ? v : defaultSettings.pdfRowDateFormat;
|
||||
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
|
|
@ -334,9 +309,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
|
||||
new Setting(this.containerEl)
|
||||
.setName("CSV heading row")
|
||||
.setDesc(
|
||||
"Whether to use the first row of generated CSV as a title row"
|
||||
)
|
||||
.setDesc("Whether to use the first row of generated CSV as a title row")
|
||||
.addToggle((t) => {
|
||||
t.setValue(settings.csvTitle);
|
||||
t.onChange((v) => {
|
||||
|
|
@ -355,9 +328,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
.addText((t) => {
|
||||
t.setValue(String(settings.csvDelimiter));
|
||||
t.onChange((v) => {
|
||||
const newCsvDelimiter = v.length
|
||||
? v
|
||||
: defaultSettings.csvDelimiter;
|
||||
const newCsvDelimiter = v.length ? v : defaultSettings.csvDelimiter;
|
||||
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
|
|
@ -371,9 +342,7 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Format copied JSON")
|
||||
.setDesc(
|
||||
"Whether to format the JSON contents before copying them to clipboard."
|
||||
)
|
||||
.setDesc("Whether to format the JSON contents before copying them to clipboard.")
|
||||
.addToggle((t) => {
|
||||
t.setValue(settings.formatCopiedJSON);
|
||||
t.onChange((v) => {
|
||||
|
|
|
|||
|
|
@ -24,32 +24,18 @@ describe("legacy settings compatibility conversion", () => {
|
|||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
{ reverseSegmentOrder: true },
|
||||
{ sortOrder: SortOrder.REVERSE_INSERTION },
|
||||
],
|
||||
[{ reverseSegmentOrder: true }, { sortOrder: SortOrder.REVERSE_INSERTION }],
|
||||
[{ reverseSegmentOrder: false }, { sortOrder: SortOrder.INSERTION }],
|
||||
[
|
||||
{ showDecimalHours: true },
|
||||
{ secondaryDurationFormat: DurationFormat.SHORT },
|
||||
],
|
||||
[
|
||||
{ showDecimalHours: false },
|
||||
{ secondaryDurationFormat: DurationFormat.NONE },
|
||||
],
|
||||
[{ showDecimalHours: true }, { secondaryDurationFormat: DurationFormat.SHORT }],
|
||||
[{ showDecimalHours: false }, { secondaryDurationFormat: DurationFormat.NONE }],
|
||||
])('for "%s" should expected "%s"', (legacySetting, expected) => {
|
||||
// Check the legacy setting gets replaced with the new setting
|
||||
const partialSetting = Object.assign(
|
||||
{},
|
||||
legacySetting
|
||||
) as TimekeepSettings;
|
||||
const partialSetting = Object.assign({}, legacySetting) as TimekeepSettings;
|
||||
legacySettingsCompatibility(partialSetting);
|
||||
expect(partialSetting).toStrictEqual(expected);
|
||||
|
||||
const setting = Object.assign({}, defaultSettings, legacySetting);
|
||||
legacySettingsCompatibility(setting);
|
||||
expect(setting).toStrictEqual(
|
||||
Object.assign({}, defaultSettings, expected)
|
||||
);
|
||||
expect(setting).toStrictEqual(Object.assign({}, defaultSettings, expected));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@
|
|||
* @param linesAfter Number of lines after the codeblock
|
||||
* @returns The generated codeblock
|
||||
*/
|
||||
export function createCodeBlock(
|
||||
json: string,
|
||||
linesBefore: number,
|
||||
linesAfter: number
|
||||
) {
|
||||
export function createCodeBlock(json: string, linesBefore: number, linesAfter: number) {
|
||||
let output = "";
|
||||
for (let i = 0; i < linesBefore; i++) {
|
||||
output += "\n";
|
||||
|
|
|
|||
|
|
@ -5,119 +5,99 @@ import { stripEntryRuntimeData, stripEntriesRuntimeData } from "./schema";
|
|||
import { expect, it, describe } from "vitest";
|
||||
|
||||
describe("createEntry", () => {
|
||||
it("creating entry should use current time", () => {
|
||||
const currentTime = moment();
|
||||
it("creating entry should use current time", () => {
|
||||
const currentTime = moment();
|
||||
|
||||
const entry = createEntry("Block 1", currentTime);
|
||||
expect(stripEntryRuntimeData(entry)).toStrictEqual({
|
||||
name: "Block 1",
|
||||
startTime: currentTime,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
});
|
||||
});
|
||||
const entry = createEntry("Block 1", currentTime);
|
||||
expect(stripEntryRuntimeData(entry)).toStrictEqual({
|
||||
name: "Block 1",
|
||||
startTime: currentTime,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("each created entry should have a unique id", () => {
|
||||
const currentTime = moment();
|
||||
it("each created entry should have a unique id", () => {
|
||||
const currentTime = moment();
|
||||
|
||||
const entry1 = createEntry("Block 1", currentTime);
|
||||
const entry2 = createEntry("Block 2", currentTime);
|
||||
expect(entry1.id).not.toBe(entry2.id);
|
||||
});
|
||||
const entry1 = createEntry("Block 1", currentTime);
|
||||
const entry2 = createEntry("Block 2", currentTime);
|
||||
expect(entry1.id).not.toBe(entry2.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withEntry", () => {
|
||||
it("should add new entry to entries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_entry/addNewEntry");
|
||||
const output = withEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
it("should add new entry to entries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_entry/addNewEntry");
|
||||
const output = withEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("should generate block name when empty", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_entry/addEmptyBlockName");
|
||||
it("should generate block name when empty", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_entry/addEmptyBlockName");
|
||||
|
||||
const output1 = withEntry(input, "", currentTime);
|
||||
expect(stripEntriesRuntimeData(output1)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
const output1 = withEntry(input, "", currentTime);
|
||||
expect(stripEntriesRuntimeData(output1)).toEqual(stripEntriesRuntimeData(expected));
|
||||
|
||||
// Empty whitespace string should also count as an empty name
|
||||
const output2 = withEntry(input, " ".repeat(5), currentTime);
|
||||
expect(stripEntriesRuntimeData(output2)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
// Empty whitespace string should also count as an empty name
|
||||
const output2 = withEntry(input, " ".repeat(5), currentTime);
|
||||
expect(stripEntriesRuntimeData(output2)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("should maintain existing entries when adding to a list", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_entry/addEntryToList");
|
||||
it("should maintain existing entries when adding to a list", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_entry/addEntryToList");
|
||||
|
||||
const output = withEntry(input, "New Test Entry", currentTime);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
const output = withEntry(input, "New Test Entry", currentTime);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
});
|
||||
|
||||
describe("withSubEntry", () => {
|
||||
it("adding first entry should convert to group", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addConvertToGroup");
|
||||
const output = withSubEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
it("adding first entry should convert to group", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addConvertToGroup");
|
||||
const output = withSubEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("adding first entry for folder should populate subentries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addFolderPopulateSubEntries");
|
||||
const output = withSubEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
it("adding first entry for folder should populate subentries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addFolderPopulateSubEntries");
|
||||
const output = withSubEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("adding first entry for folder should extend subentries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addFolderExtendSubEntries");
|
||||
const output = withSubEntry(input, "New Entry 2", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
it("adding first entry for folder should extend subentries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addFolderExtendSubEntries");
|
||||
const output = withSubEntry(input, "New Entry 2", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("adding to group should extend sub entries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addToGroupExtendSubEntries");
|
||||
it("adding to group should extend sub entries", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/addToGroupExtendSubEntries");
|
||||
|
||||
const output = withSubEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
const output = withSubEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("empty name should generate a part name (single)", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameSingle");
|
||||
it("empty name should generate a part name (single)", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameSingle");
|
||||
|
||||
const output = withSubEntry(input, "", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
const output = withSubEntry(input, "", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("empty name should generate a part name (group)", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameGroup");
|
||||
it("empty name should generate a part name (group)", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/manipulating/adding_sub_entry/emptyNameCreatePartNameGroup");
|
||||
|
||||
const output = withSubEntry(input, "", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
const output = withSubEntry(input, "", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(stripEntryRuntimeData(expected));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ export function createEntry(name: string, startTime: Moment): TimeEntry {
|
|||
* @param startTime The start time of the new entry
|
||||
* @returns The new collection of entries
|
||||
*/
|
||||
export function withEntry(
|
||||
entries: TimeEntry[],
|
||||
name: string,
|
||||
startTime: Moment
|
||||
): TimeEntry[] {
|
||||
export function withEntry(entries: TimeEntry[], name: string, startTime: Moment): TimeEntry[] {
|
||||
const entryName = getEntryName(name, entries);
|
||||
return [...entries, createEntry(entryName, startTime)];
|
||||
}
|
||||
|
|
@ -66,11 +62,7 @@ function getEntryName(name: string, entries: TimeEntry[]) {
|
|||
* @param startTime The start time for the new entry
|
||||
* @returns The updated/created entry
|
||||
*/
|
||||
export function withSubEntry(
|
||||
parent: TimeEntry,
|
||||
name: string,
|
||||
startTime: Moment
|
||||
): TimeEntry {
|
||||
export function withSubEntry(parent: TimeEntry, name: string, startTime: Moment): TimeEntry {
|
||||
const groupEntry = makeGroupEntry(parent);
|
||||
const entryName = getSubEntryName(name, groupEntry);
|
||||
const newEntry = createEntry(entryName, startTime);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import moment from "moment";
|
|||
|
||||
import { Timekeep, stripTimekeepRuntimeData } from "./schema";
|
||||
import {
|
||||
load,
|
||||
LoadError,
|
||||
LoadSuccess,
|
||||
replaceTimekeepCodeblock,
|
||||
extractTimekeepCodeblocks,
|
||||
load,
|
||||
LoadError,
|
||||
LoadSuccess,
|
||||
replaceTimekeepCodeblock,
|
||||
extractTimekeepCodeblocks,
|
||||
} from "./parser";
|
||||
import { expect, it, describe } from "vitest";
|
||||
|
||||
|
|
@ -19,183 +19,168 @@ import { expect, it, describe } from "vitest";
|
|||
* @param linesAfter Number of lines after the codeblock
|
||||
* @returns The generated codeblock
|
||||
*/
|
||||
const createCodeBlock = (
|
||||
json: string,
|
||||
linesBefore: number,
|
||||
linesAfter: number
|
||||
) => {
|
||||
let output = "";
|
||||
for (let i = 0; i < linesBefore; i++) {
|
||||
output += "\n";
|
||||
}
|
||||
output += "```timekeep\n";
|
||||
output += json;
|
||||
output += "\n```";
|
||||
for (let i = 0; i < linesAfter; i++) {
|
||||
output += "\n";
|
||||
}
|
||||
return output;
|
||||
const createCodeBlock = (json: string, linesBefore: number, linesAfter: number) => {
|
||||
let output = "";
|
||||
for (let i = 0; i < linesBefore; i++) {
|
||||
output += "\n";
|
||||
}
|
||||
output += "```timekeep\n";
|
||||
output += json;
|
||||
output += "\n```";
|
||||
for (let i = 0; i < linesAfter; i++) {
|
||||
output += "\n";
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
describe("extracting code blocks", () => {
|
||||
it("should extract codeblock contents", async () => {
|
||||
const { text, inputTimekeep1, inputTimekeep2 } =
|
||||
await import("./__fixtures__/extracting/codeblockContents");
|
||||
it("should extract codeblock contents", async () => {
|
||||
const { text, inputTimekeep1, inputTimekeep2 } =
|
||||
await import("./__fixtures__/extracting/codeblockContents");
|
||||
|
||||
const output = extractTimekeepCodeblocks(text);
|
||||
const output = extractTimekeepCodeblocks(text);
|
||||
|
||||
expect(stripTimekeepRuntimeData(output[0])).toStrictEqual(
|
||||
stripTimekeepRuntimeData(inputTimekeep1)
|
||||
);
|
||||
expect(stripTimekeepRuntimeData(output[1])).toStrictEqual(
|
||||
stripTimekeepRuntimeData(inputTimekeep2)
|
||||
);
|
||||
expect(output.length).toBe(2);
|
||||
});
|
||||
expect(stripTimekeepRuntimeData(output[0])).toStrictEqual(
|
||||
stripTimekeepRuntimeData(inputTimekeep1)
|
||||
);
|
||||
expect(stripTimekeepRuntimeData(output[1])).toStrictEqual(
|
||||
stripTimekeepRuntimeData(inputTimekeep2)
|
||||
);
|
||||
expect(output.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should ignore codeblocks that are not closed", async () => {
|
||||
const { text, inputTimekeep1 } =
|
||||
await import("./__fixtures__/extracting/unclosedCodeBlock");
|
||||
const output = extractTimekeepCodeblocks(text);
|
||||
it("should ignore codeblocks that are not closed", async () => {
|
||||
const { text, inputTimekeep1 } =
|
||||
await import("./__fixtures__/extracting/unclosedCodeBlock");
|
||||
const output = extractTimekeepCodeblocks(text);
|
||||
|
||||
expect(stripTimekeepRuntimeData(output[0])).toStrictEqual(
|
||||
stripTimekeepRuntimeData(inputTimekeep1)
|
||||
);
|
||||
expect(output.length).toBe(1);
|
||||
});
|
||||
expect(stripTimekeepRuntimeData(output[0])).toStrictEqual(
|
||||
stripTimekeepRuntimeData(inputTimekeep1)
|
||||
);
|
||||
expect(output.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replacing content", () => {
|
||||
it("should replace codeblock contents", () => {
|
||||
const lineStart = 4; // Line the codeblock should start on
|
||||
const lineEnd = lineStart + 2; // Line the codeblock should end on
|
||||
it("should replace codeblock contents", () => {
|
||||
const lineStart = 4; // Line the codeblock should start on
|
||||
const lineEnd = lineStart + 2; // Line the codeblock should end on
|
||||
|
||||
// Input data to replace
|
||||
const input = createCodeBlock(
|
||||
`{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
||||
4,
|
||||
4
|
||||
);
|
||||
// Input data to replace
|
||||
const input = createCodeBlock(
|
||||
`{"entries":[{"name":"Block 1","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
||||
4,
|
||||
4
|
||||
);
|
||||
|
||||
// Timekeep with a renamed block
|
||||
const inputTimekeep: Timekeep = {
|
||||
entries: [
|
||||
{
|
||||
id: "49b99108-b1ad-4355-baa9-89c49c342be2",
|
||||
name: "Block 2",
|
||||
startTime: moment("2024-03-17T01:33:51.630Z"),
|
||||
endTime: moment("2024-03-17T01:33:55.151Z"),
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
// Timekeep with a renamed block
|
||||
const inputTimekeep: Timekeep = {
|
||||
entries: [
|
||||
{
|
||||
id: "49b99108-b1ad-4355-baa9-89c49c342be2",
|
||||
name: "Block 2",
|
||||
startTime: moment("2024-03-17T01:33:51.630Z"),
|
||||
endTime: moment("2024-03-17T01:33:55.151Z"),
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Value with the renamed block
|
||||
const expected = createCodeBlock(
|
||||
`{"entries":[{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
||||
4,
|
||||
4
|
||||
);
|
||||
// Value with the renamed block
|
||||
const expected = createCodeBlock(
|
||||
`{"entries":[{"name":"Block 2","startTime":"2024-03-17T01:33:51.630Z","endTime":"2024-03-17T01:33:55.151Z","subEntries":null}]}`,
|
||||
4,
|
||||
4
|
||||
);
|
||||
|
||||
const output = replaceTimekeepCodeblock(
|
||||
inputTimekeep,
|
||||
input,
|
||||
lineStart,
|
||||
lineEnd
|
||||
);
|
||||
const output = replaceTimekeepCodeblock(inputTimekeep, input, lineStart, lineEnd);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
|
||||
it("should fail if codeblock is missing", () => {
|
||||
const input = createCodeBlock("", 4, 4);
|
||||
// Start not code fences
|
||||
expect(() =>
|
||||
replaceTimekeepCodeblock({ entries: [] }, input, 2, 4)
|
||||
).toThrow();
|
||||
it("should fail if codeblock is missing", () => {
|
||||
const input = createCodeBlock("", 4, 4);
|
||||
// Start not code fences
|
||||
expect(() => replaceTimekeepCodeblock({ entries: [] }, input, 2, 4)).toThrow();
|
||||
|
||||
// End not code fences
|
||||
expect(() =>
|
||||
replaceTimekeepCodeblock({ entries: [] }, input, 4, 8)
|
||||
).toThrow();
|
||||
});
|
||||
// End not code fences
|
||||
expect(() => replaceTimekeepCodeblock({ entries: [] }, input, 4, 8)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loading timekeep", () => {
|
||||
it("should give empty timekeep when given empty string", () => {
|
||||
const result = load("");
|
||||
it("should give empty timekeep when given empty string", () => {
|
||||
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
|
||||
expect(successResult.timekeep).toEqual({ entries: [] });
|
||||
});
|
||||
// Ensure the contents match
|
||||
expect(successResult.timekeep).toEqual({ entries: [] });
|
||||
});
|
||||
|
||||
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 expected = {
|
||||
entries: [
|
||||
{
|
||||
name: "Block 1",
|
||||
startTime: moment("2024-03-17T01:33:51.630Z"),
|
||||
endTime: moment("2024-03-17T01:33:55.151Z"),
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Block 2",
|
||||
startTime: moment("2024-03-17T01:33:51.630Z"),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Non Started Block",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
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 expected = {
|
||||
entries: [
|
||||
{
|
||||
name: "Block 1",
|
||||
startTime: moment("2024-03-17T01:33:51.630Z"),
|
||||
endTime: moment("2024-03-17T01:33:55.151Z"),
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Block 2",
|
||||
startTime: moment("2024-03-17T01:33:51.630Z"),
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Non Started Block",
|
||||
startTime: null,
|
||||
endTime: 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
|
||||
expect(stripTimekeepRuntimeData(successResult.timekeep)).toEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
// Ensure the contents match
|
||||
expect(stripTimekeepRuntimeData(successResult.timekeep)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should give error on invalid timekeep (JSON)", () => {
|
||||
const data = "{";
|
||||
it("should give error on invalid timekeep (JSON)", () => {
|
||||
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", () => {
|
||||
const input = `
|
||||
it("should tolerate a timekeep with leading or trailing whitespaces", () => {
|
||||
const input = `
|
||||
\`\`\`timekeep
|
||||
\`\`\`
|
||||
`;
|
||||
// Start not code fences
|
||||
replaceTimekeepCodeblock({ entries: [] }, input, 1, 2);
|
||||
});
|
||||
// Start not code fences
|
||||
replaceTimekeepCodeblock({ entries: [] }, input, 1, 2);
|
||||
});
|
||||
|
||||
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}]}`;
|
||||
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 result = load(data);
|
||||
const result = load(data);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { isEmptyString } from "@/utils";
|
||||
import {
|
||||
TIMEKEEP,
|
||||
Timekeep,
|
||||
stripTimekeepRuntimeData,
|
||||
} from "@/timekeep/schema";
|
||||
import { TIMEKEEP, Timekeep, stripTimekeepRuntimeData } from "@/timekeep/schema";
|
||||
|
||||
export type LoadResult = LoadSuccess | LoadError;
|
||||
|
||||
|
|
@ -18,40 +14,40 @@ export type LoadError = { success: false; error: string };
|
|||
* @return The load result
|
||||
*/
|
||||
export function load(value: string): LoadResult {
|
||||
// Empty string should create an empty timekeep
|
||||
if (isEmptyString(value)) {
|
||||
return { success: true, timekeep: { entries: [] } };
|
||||
}
|
||||
// Empty string should create an empty timekeep
|
||||
if (isEmptyString(value)) {
|
||||
return { success: true, timekeep: { entries: [] } };
|
||||
}
|
||||
|
||||
// Load the JSON value
|
||||
let parsedValue: unknown;
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to parse timekeep JSON",
|
||||
};
|
||||
}
|
||||
// Load the JSON value
|
||||
let parsedValue: unknown;
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to parse timekeep JSON",
|
||||
};
|
||||
}
|
||||
|
||||
// Parse the data against the schema
|
||||
const timekeepResult = TIMEKEEP.safeParse(parsedValue);
|
||||
if (!timekeepResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: timekeepResult.error.toString(),
|
||||
};
|
||||
}
|
||||
// Parse the data against the schema
|
||||
const timekeepResult = TIMEKEEP.safeParse(parsedValue);
|
||||
if (!timekeepResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: timekeepResult.error.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
const timekeep = timekeepResult.data;
|
||||
return { success: true, timekeep };
|
||||
const timekeep = timekeepResult.data;
|
||||
return { success: true, timekeep };
|
||||
}
|
||||
|
||||
export interface TimekeepWithPosition {
|
||||
timekeep: Timekeep;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
timekeep: Timekeep;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,45 +59,41 @@ export interface TimekeepWithPosition {
|
|||
* @param value The file text contents
|
||||
* @returns The extracted timekeep blocks
|
||||
*/
|
||||
export function extractTimekeepCodeblocksWithPosition(
|
||||
value: string
|
||||
): TimekeepWithPosition[] {
|
||||
const out: TimekeepWithPosition[] = [];
|
||||
const lines = value.replace("\n\r", "\n").split("\n");
|
||||
export function extractTimekeepCodeblocksWithPosition(value: string): TimekeepWithPosition[] {
|
||||
const out: TimekeepWithPosition[] = [];
|
||||
const lines = value.replace("\n\r", "\n").split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const startLine = lines[i];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const startLine = lines[i];
|
||||
|
||||
// Skip lines till a timekeep block is found
|
||||
if (!startLine.trim().startsWith("```timekeep")) {
|
||||
continue;
|
||||
}
|
||||
// Skip lines till a timekeep block is found
|
||||
if (!startLine.trim().startsWith("```timekeep")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find end of codeblock
|
||||
const endLineIndex = lines.findIndex(
|
||||
(line, index) => index > i && line.trim() === "```"
|
||||
);
|
||||
// Find end of codeblock
|
||||
const endLineIndex = lines.findIndex((line, index) => index > i && line.trim() === "```");
|
||||
|
||||
if (endLineIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
if (endLineIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = "";
|
||||
for (let lineIndex = i + 1; lineIndex < endLineIndex; lineIndex++) {
|
||||
content += lines[lineIndex] + "\n";
|
||||
}
|
||||
let content = "";
|
||||
for (let lineIndex = i + 1; lineIndex < endLineIndex; lineIndex++) {
|
||||
content += lines[lineIndex] + "\n";
|
||||
}
|
||||
|
||||
const result = load(content);
|
||||
if (result.success) {
|
||||
out.push({
|
||||
timekeep: result.timekeep,
|
||||
startLine: i,
|
||||
endLine: endLineIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
const result = load(content);
|
||||
if (result.success) {
|
||||
out.push({
|
||||
timekeep: result.timekeep,
|
||||
startLine: i,
|
||||
endLine: endLineIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,11 +104,11 @@ export function extractTimekeepCodeblocksWithPosition(
|
|||
* @returns The extracted timekeep blocks
|
||||
*/
|
||||
export function extractTimekeepCodeblocks(value: string): Timekeep[] {
|
||||
return (
|
||||
extractTimekeepCodeblocksWithPosition(value)
|
||||
//
|
||||
.map((value) => value.timekeep)
|
||||
);
|
||||
return (
|
||||
extractTimekeepCodeblocksWithPosition(value)
|
||||
//
|
||||
.map((value) => value.timekeep)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,37 +116,37 @@ export function extractTimekeepCodeblocks(value: string): Timekeep[] {
|
|||
* a file returning the modified contents to be saved
|
||||
*/
|
||||
export function replaceTimekeepCodeblock(
|
||||
timekeep: Timekeep,
|
||||
content: string,
|
||||
lineStart: number,
|
||||
lineEnd: number
|
||||
timekeep: Timekeep,
|
||||
content: string,
|
||||
lineStart: number,
|
||||
lineEnd: number
|
||||
): string {
|
||||
const timekeepJSON = JSON.stringify(stripTimekeepRuntimeData(timekeep));
|
||||
const timekeepJSON = JSON.stringify(stripTimekeepRuntimeData(timekeep));
|
||||
|
||||
// The actual JSON is the line after the code block start
|
||||
const contentStart = lineStart + 1;
|
||||
const contentLength = lineEnd - contentStart;
|
||||
// The actual JSON is the line after the code block start
|
||||
const contentStart = lineStart + 1;
|
||||
const contentLength = lineEnd - contentStart;
|
||||
|
||||
// Split the content into lines
|
||||
const lines = content.split("\n");
|
||||
// Split the content into lines
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Sanity checks to prevent overriding content
|
||||
if (!lines[lineStart].trim().startsWith("```")) {
|
||||
throw new Error(
|
||||
"Content timekeep out of sync, line number for codeblock start doesn't match: " +
|
||||
content[lineStart]
|
||||
);
|
||||
}
|
||||
// Sanity checks to prevent overriding content
|
||||
if (!lines[lineStart].trim().startsWith("```")) {
|
||||
throw new Error(
|
||||
"Content timekeep out of sync, line number for codeblock start doesn't match: " +
|
||||
content[lineStart]
|
||||
);
|
||||
}
|
||||
|
||||
if (!lines[lineEnd].trim().startsWith("```")) {
|
||||
throw new Error(
|
||||
"Content timekeep out of sync, line number for codeblock end doesn't match" +
|
||||
content[lineEnd]
|
||||
);
|
||||
}
|
||||
if (!lines[lineEnd].trim().startsWith("```")) {
|
||||
throw new Error(
|
||||
"Content timekeep out of sync, line number for codeblock end doesn't match" +
|
||||
content[lineEnd]
|
||||
);
|
||||
}
|
||||
|
||||
// Splice the new JSON content in between the codeblock, removing the old codeblock lines
|
||||
lines.splice(contentStart, contentLength, timekeepJSON);
|
||||
// Splice the new JSON content in between the codeblock, removing the old codeblock lines
|
||||
lines.splice(contentStart, contentLength, timekeepJSON);
|
||||
|
||||
return lines.join("\n");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,169 +1,164 @@
|
|||
import {
|
||||
getEntryById,
|
||||
isKeepRunning,
|
||||
getPathToEntry,
|
||||
isEntryRunning,
|
||||
getRunningEntry,
|
||||
getEntryDuration,
|
||||
getTotalDuration,
|
||||
getEntryById,
|
||||
isKeepRunning,
|
||||
getPathToEntry,
|
||||
isEntryRunning,
|
||||
getRunningEntry,
|
||||
getEntryDuration,
|
||||
getTotalDuration,
|
||||
} from "./queries";
|
||||
import { expect, it, describe } from "vitest";
|
||||
|
||||
describe("getEntryById", () => {
|
||||
it("find top level entry", async () => {
|
||||
const { input, targetEntry, targetEntryId } =
|
||||
await import("./__fixtures__/checking/findEntryById");
|
||||
it("find top level entry", async () => {
|
||||
const { input, targetEntry, targetEntryId } =
|
||||
await import("./__fixtures__/checking/findEntryById");
|
||||
|
||||
const output = getEntryById(targetEntryId, input);
|
||||
expect(output).toEqual(targetEntry);
|
||||
});
|
||||
const output = getEntryById(targetEntryId, input);
|
||||
expect(output).toEqual(targetEntry);
|
||||
});
|
||||
|
||||
it("find nested entry", async () => {
|
||||
const { input, targetEntry, targetEntryId } =
|
||||
await import("./__fixtures__/checking/findEntryByIdNested");
|
||||
it("find nested entry", async () => {
|
||||
const { input, targetEntry, targetEntryId } =
|
||||
await import("./__fixtures__/checking/findEntryByIdNested");
|
||||
|
||||
const output = getEntryById(targetEntryId, input);
|
||||
expect(output).toEqual(targetEntry);
|
||||
});
|
||||
const output = getEntryById(targetEntryId, input);
|
||||
expect(output).toEqual(targetEntry);
|
||||
});
|
||||
|
||||
it("find entry non existent", async () => {
|
||||
const { input, targetEntryId } =
|
||||
await import("./__fixtures__/checking/findEntryByIdMissing");
|
||||
it("find entry non existent", async () => {
|
||||
const { input, targetEntryId } =
|
||||
await import("./__fixtures__/checking/findEntryByIdMissing");
|
||||
|
||||
const output = getEntryById(targetEntryId, input);
|
||||
expect(output).toBeUndefined();
|
||||
});
|
||||
const output = getEntryById(targetEntryId, input);
|
||||
expect(output).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPathToEntry", () => {
|
||||
it("path not found", async () => {
|
||||
const { targetEntry, entries, expected } =
|
||||
await import("./__fixtures__/path/pathNotFound");
|
||||
const output = getPathToEntry(entries, targetEntry);
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
it("path not found", async () => {
|
||||
const { targetEntry, entries, expected } = await import("./__fixtures__/path/pathNotFound");
|
||||
const output = getPathToEntry(entries, targetEntry);
|
||||
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 () => {
|
||||
const { input, runningEntry, path } =
|
||||
await import("./__fixtures__/checking/findRunningEntryPath");
|
||||
it("should find running entry path", async () => {
|
||||
const { input, runningEntry, path } =
|
||||
await import("./__fixtures__/checking/findRunningEntryPath");
|
||||
|
||||
const output = getPathToEntry(input, runningEntry);
|
||||
expect(output).toEqual(path);
|
||||
});
|
||||
const output = getPathToEntry(input, runningEntry);
|
||||
expect(output).toEqual(path);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEntryRunning", () => {
|
||||
it("should determine entry running state", async () => {
|
||||
const { running, notRunning } =
|
||||
await import("./__fixtures__/checking/runningState");
|
||||
it("should determine entry running state", async () => {
|
||||
const { running, notRunning } = await import("./__fixtures__/checking/runningState");
|
||||
|
||||
expect(isEntryRunning(running)).toBe(true);
|
||||
expect(isEntryRunning(notRunning)).toBe(false);
|
||||
});
|
||||
expect(isEntryRunning(running)).toBe(true);
|
||||
expect(isEntryRunning(notRunning)).toBe(false);
|
||||
});
|
||||
|
||||
it("should determine entry running state (nested)", async () => {
|
||||
const { runningNested, stoppedNested } =
|
||||
await import("./__fixtures__/checking/runningState");
|
||||
it("should determine entry running state (nested)", async () => {
|
||||
const { runningNested, stoppedNested } =
|
||||
await import("./__fixtures__/checking/runningState");
|
||||
|
||||
expect(isEntryRunning(runningNested)).toBe(true);
|
||||
expect(isEntryRunning(stoppedNested)).toBe(false);
|
||||
});
|
||||
expect(isEntryRunning(runningNested)).toBe(true);
|
||||
expect(isEntryRunning(stoppedNested)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRunningEntry", () => {
|
||||
it("should find running entry", async () => {
|
||||
const { input, runningEntry } =
|
||||
await import("./__fixtures__/checking/shouldFindRunningEntry");
|
||||
it("should find running entry", async () => {
|
||||
const { input, runningEntry } =
|
||||
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 () => {
|
||||
const { input, runningEntry } =
|
||||
await import("./__fixtures__/checking/shouldFindRunningEntryNested");
|
||||
it("should find nested running entry", async () => {
|
||||
const { input, runningEntry } =
|
||||
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 () => {
|
||||
const { input } =
|
||||
await import("./__fixtures__/checking/shouldNotFindRunningEntry");
|
||||
it("should not find running entry", async () => {
|
||||
const { input } = await import("./__fixtures__/checking/shouldNotFindRunningEntry");
|
||||
|
||||
const output = getRunningEntry(input);
|
||||
const output = getRunningEntry(input);
|
||||
|
||||
expect(output).toBe(null);
|
||||
});
|
||||
expect(output).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isKeepRunning", () => {
|
||||
it("should show keep running", async () => {
|
||||
const { input } =
|
||||
await import("./__fixtures__/checking/shouldBeRunning");
|
||||
it("should show keep running", async () => {
|
||||
const { input } = await import("./__fixtures__/checking/shouldBeRunning");
|
||||
|
||||
expect(isKeepRunning(input)).toBe(true);
|
||||
});
|
||||
expect(isKeepRunning(input)).toBe(true);
|
||||
});
|
||||
|
||||
it("should show keep not running", async () => {
|
||||
const { input } =
|
||||
await import("./__fixtures__/checking/shouldNotBeRunning");
|
||||
it("should show keep not running", async () => {
|
||||
const { input } = await import("./__fixtures__/checking/shouldNotBeRunning");
|
||||
|
||||
expect(isKeepRunning(input)).toBe(false);
|
||||
});
|
||||
expect(isKeepRunning(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEntryDuration", () => {
|
||||
it("should get entry duration", async () => {
|
||||
const { input, currentTime, durationMs } =
|
||||
await import("./__fixtures__/duration/shouldGetEntryDuration");
|
||||
it("should get entry duration", async () => {
|
||||
const { input, currentTime, durationMs } =
|
||||
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 () => {
|
||||
const { input, currentTime, durationMs } =
|
||||
await import("./__fixtures__/duration/nonStartedZeroDuration");
|
||||
it("duration of non started entry should be zero", async () => {
|
||||
const { input, currentTime, durationMs } =
|
||||
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 () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/duration/durationIncludeChildren");
|
||||
it("duration should include children", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
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 () => {
|
||||
const { input, endTime, durationMs } =
|
||||
await import("./__fixtures__/duration/currentEndUnfinished");
|
||||
it("duration should use current as end for unfinished entries", async () => {
|
||||
const { input, endTime, durationMs } =
|
||||
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", () => {
|
||||
it("should get total duration", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/duration/totalDuration");
|
||||
it("should get total duration", async () => {
|
||||
const { input, currentTime, expected } =
|
||||
await import("./__fixtures__/duration/totalDuration");
|
||||
|
||||
const output = getTotalDuration(input, currentTime);
|
||||
const output = getTotalDuration(input, currentTime);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ import { Timekeep, TimeEntry } from "@/timekeep/schema";
|
|||
* @param entries List of entries to search
|
||||
* @returns The found entry or undefined
|
||||
*/
|
||||
export function getEntryById(
|
||||
entryId: string,
|
||||
entries: TimeEntry[]
|
||||
): TimeEntry | undefined {
|
||||
export function getEntryById(entryId: string, entries: TimeEntry[]): TimeEntry | undefined {
|
||||
for (const entry of entries) {
|
||||
if (entry.id === entryId) {
|
||||
return entry;
|
||||
|
|
@ -127,10 +124,7 @@ export function isKeepRunning(timekeep: Timekeep): boolean {
|
|||
* @param currentTime The current time to use for unfinished entries
|
||||
* @returns The duration in milliseconds
|
||||
*/
|
||||
export function getEntryDuration(
|
||||
entry: TimeEntry,
|
||||
currentTime: Moment
|
||||
): number {
|
||||
export function getEntryDuration(entry: TimeEntry, currentTime: Moment): number {
|
||||
if (entry.subEntries !== null) {
|
||||
return getTotalDuration(entry.subEntries, currentTime);
|
||||
}
|
||||
|
|
@ -153,13 +147,9 @@ export function getEntryDuration(
|
|||
* @param currentTime The current time to use for unfinished entries
|
||||
* @returns The total duration in milliseconds
|
||||
*/
|
||||
export function getTotalDuration(
|
||||
entries: TimeEntry[],
|
||||
currentTime: Moment
|
||||
): number {
|
||||
export function getTotalDuration(entries: TimeEntry[], currentTime: Moment): number {
|
||||
return entries.reduce(
|
||||
(totalDuration, entry) =>
|
||||
totalDuration + getEntryDuration(entry, currentTime),
|
||||
(totalDuration, entry) => totalDuration + getEntryDuration(entry, currentTime),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,18 +55,15 @@ const TIME_ENTRY_GROUP_BASE = z.object({
|
|||
});
|
||||
|
||||
// Schema for a time entry group
|
||||
const TIME_ENTRY_GROUP: z.ZodType<TimeEntryGroup> =
|
||||
TIME_ENTRY_GROUP_BASE.extend({
|
||||
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) => ({
|
||||
...entry,
|
||||
id: uuid(),
|
||||
}));
|
||||
const TIME_ENTRY_GROUP: z.ZodType<TimeEntryGroup> = TIME_ENTRY_GROUP_BASE.extend({
|
||||
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) => ({
|
||||
...entry,
|
||||
id: uuid(),
|
||||
}));
|
||||
|
||||
// Schema for time entries
|
||||
const TIME_ENTRY = z.union([TIME_ENTRY_SINGLE, TIME_ENTRY_GROUP]);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import {
|
||||
SortOrder,
|
||||
UnstartedOrder,
|
||||
defaultSettings,
|
||||
TimekeepSettings,
|
||||
} from "@/settings";
|
||||
import { SortOrder, UnstartedOrder, defaultSettings, TimekeepSettings } from "@/settings";
|
||||
import { expect, it, describe } from "vitest";
|
||||
import { getEntriesSorted } from "./sort";
|
||||
|
||||
describe("getEntriesSorted", () => {
|
||||
it("should be in reverse order", async () => {
|
||||
const { input, expected } =
|
||||
await import("./__fixtures__/ordering/reverseOrder");
|
||||
const { input, expected } = await import("./__fixtures__/ordering/reverseOrder");
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.REVERSE_INSERTION;
|
||||
|
|
@ -20,8 +14,7 @@ describe("getEntriesSorted", () => {
|
|||
});
|
||||
|
||||
it("should be in newest first order", async () => {
|
||||
const { input, expected } =
|
||||
await import("./__fixtures__/ordering/newestFirstOrder");
|
||||
const { input, expected } = await import("./__fixtures__/ordering/newestFirstOrder");
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.NEWEST_START;
|
||||
|
|
@ -31,8 +24,7 @@ describe("getEntriesSorted", () => {
|
|||
});
|
||||
|
||||
it("should be in newest last order", async () => {
|
||||
const { input, expected } =
|
||||
await import("./__fixtures__/ordering/newestLastOrder");
|
||||
const { input, expected } = await import("./__fixtures__/ordering/newestLastOrder");
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.OLDEST_START;
|
||||
|
|
@ -42,8 +34,7 @@ describe("getEntriesSorted", () => {
|
|||
});
|
||||
|
||||
it("should be in newest last order with nulls first", async () => {
|
||||
const { input, expected } =
|
||||
await import("./__fixtures__/ordering/newestLastNullsFirst");
|
||||
const { input, expected } = await import("./__fixtures__/ordering/newestLastNullsFirst");
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.OLDEST_START;
|
||||
|
|
@ -54,8 +45,7 @@ describe("getEntriesSorted", () => {
|
|||
});
|
||||
|
||||
it("order should not change", async () => {
|
||||
const { input, expected } =
|
||||
await import("./__fixtures__/ordering/orderShouldNotChange");
|
||||
const { input, expected } = await import("./__fixtures__/ordering/orderShouldNotChange");
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.INSERTION;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ import { getStartTime } from "./queries";
|
|||
|
||||
type TimeEntryWithIndex = TimeEntry & { index: number };
|
||||
|
||||
type EntriesComparator = (
|
||||
a: TimeEntryWithIndex,
|
||||
b: TimeEntryWithIndex
|
||||
) => number;
|
||||
type EntriesComparator = (a: TimeEntryWithIndex, b: TimeEntryWithIndex) => number;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns The sorted entries list
|
||||
*/
|
||||
export function getEntriesSorted(
|
||||
entries: TimeEntry[],
|
||||
settings: TimekeepSettings
|
||||
): TimeEntry[] {
|
||||
export function getEntriesSorted(entries: TimeEntry[], settings: TimekeepSettings): TimeEntry[] {
|
||||
// List order should be unchanged
|
||||
if (settings.sortOrder === SortOrder.INSERTION) {
|
||||
return entries;
|
||||
|
|
@ -56,9 +50,7 @@ function getEntriesSortedReverseInsertion(entries: TimeEntry[]): TimeEntry[] {
|
|||
if (entry.subEntries !== null) {
|
||||
return {
|
||||
...entry,
|
||||
subEntries: getEntriesSortedReverseInsertion(
|
||||
entry.subEntries
|
||||
),
|
||||
subEntries: getEntriesSortedReverseInsertion(entry.subEntries),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -86,10 +78,7 @@ function getEntriesSortedComparator(
|
|||
if (entry.subEntries !== null) {
|
||||
return {
|
||||
...entry,
|
||||
subEntries: getEntriesSortedComparator(
|
||||
entry.subEntries,
|
||||
comparator
|
||||
),
|
||||
subEntries: getEntriesSortedComparator(entry.subEntries, comparator),
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
|
@ -125,9 +114,7 @@ function createEntriesComparator(
|
|||
|
||||
// Sort newest when both have a start time
|
||||
if (aStartTime && bStartTime) {
|
||||
return newest
|
||||
? bStartTime.diff(aStartTime)
|
||||
: aStartTime.diff(bStartTime);
|
||||
return newest ? bStartTime.diff(aStartTime) : aStartTime.diff(bStartTime);
|
||||
}
|
||||
|
||||
if (aStartTime) {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ describe("startNewEntry", () => {
|
|||
expect(stoppedEntry).toBeDefined();
|
||||
expect(stoppedEntry!.endTime).not.toBeNull();
|
||||
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("starting a new entry should add a new entry", async () => {
|
||||
|
|
@ -23,9 +21,7 @@ describe("startNewEntry", () => {
|
|||
await import("./__fixtures__/manipulating/start_entry/startShouldStopRunning");
|
||||
|
||||
const output = startNewEntry(name, currentTime, input);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -35,9 +31,7 @@ describe("startNewNestedEntry", () => {
|
|||
await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry");
|
||||
|
||||
const output = startNewNestedEntry(currentTime, targetEntry.id, input);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
const output = startNewNestedEntry(currentTime, targetEntry.id, input);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("starting a new entry should stop any running entries", async () => {
|
||||
|
|
@ -55,9 +47,7 @@ describe("startNewNestedEntry", () => {
|
|||
await import("./__fixtures__/manipulating/start_entry/startNotStartedEntry");
|
||||
|
||||
const output = startNewNestedEntry(currentTime, targetEntry.id, input);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("starting a new entry should stop any running entries", async () => {
|
||||
|
|
@ -65,9 +55,7 @@ describe("startNewNestedEntry", () => {
|
|||
await import("./__fixtures__/manipulating/start_entry/startNestedNonExistent");
|
||||
|
||||
const output = startNewNestedEntry(currentTime, targetEntryId, input);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
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 outerEntry = output[0];
|
||||
const stoppedEntry = outerEntry.subEntries?.find(
|
||||
(entry) => entry.id === stopped
|
||||
);
|
||||
const stoppedEntry = outerEntry.subEntries?.find((entry) => entry.id === stopped);
|
||||
|
||||
expect(stoppedEntry).toBeDefined();
|
||||
expect(stoppedEntry!.endTime).not.toBeNull();
|
||||
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
|
||||
it("starting a new entry should add a new entry", async () => {
|
||||
|
|
@ -94,8 +78,6 @@ describe("startNewNestedEntry", () => {
|
|||
await import("./__fixtures__/manipulating/start_entry/startNestedShouldStopRunning");
|
||||
|
||||
const output = startNewNestedEntry(currentTime, targetEntry.id, input);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(stripEntriesRuntimeData(expected));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -60,9 +60,5 @@ export function startNewNestedEntry(
|
|||
}
|
||||
|
||||
// If the entry hasn't been started then start it
|
||||
return updateEntry(
|
||||
entries,
|
||||
currentEntry.id,
|
||||
createEntry(currentEntry.name, currentTime)
|
||||
);
|
||||
return updateEntry(entries, currentEntry.id, createEntry(currentEntry.name, currentTime));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,7 @@ export function updateEntry(
|
|||
if (entry.subEntries !== null) {
|
||||
return {
|
||||
...entry,
|
||||
subEntries: updateEntry(
|
||||
entry.subEntries,
|
||||
previousEntryId,
|
||||
newEntry
|
||||
),
|
||||
subEntries: updateEntry(entry.subEntries, previousEntryId, newEntry),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -43,10 +39,7 @@ export function updateEntry(
|
|||
* @param collapsed The collapse state
|
||||
* @returns The new updated entry
|
||||
*/
|
||||
export function setEntryCollapsed(
|
||||
entry: TimeEntry,
|
||||
collapsed: boolean
|
||||
): TimeEntry {
|
||||
export function setEntryCollapsed(entry: TimeEntry, collapsed: boolean): TimeEntry {
|
||||
// Entry cannot be collapsed
|
||||
if (entry.subEntries === null) {
|
||||
return entry;
|
||||
|
|
@ -69,10 +62,7 @@ export function setEntryCollapsed(
|
|||
* @param entries The entries to stop
|
||||
* @returns The new list of stopped entries
|
||||
*/
|
||||
export function stopRunningEntries(
|
||||
entries: TimeEntry[],
|
||||
endTime: Moment
|
||||
): TimeEntry[] {
|
||||
export function stopRunningEntries(entries: TimeEntry[], endTime: Moment): TimeEntry[] {
|
||||
return entries.map((entry) => {
|
||||
// Stop the sub entries
|
||||
if (entry.subEntries) {
|
||||
|
|
@ -105,10 +95,7 @@ export function stopRunningEntries(
|
|||
* @param target The target entry to remove
|
||||
* @returns The new list with the entry removed
|
||||
*/
|
||||
export function removeEntry(
|
||||
entries: TimeEntry[],
|
||||
target: TimeEntry
|
||||
): TimeEntry[] {
|
||||
export function removeEntry(entries: TimeEntry[], target: TimeEntry): TimeEntry[] {
|
||||
return entries.reduce((acc: TimeEntry[], entry: TimeEntry) => {
|
||||
if (entry.id !== target.id) {
|
||||
// Filter sub entries for matching entries
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ export class ConfirmModal extends Modal {
|
|||
// Whether an option was picked
|
||||
picked: boolean;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
message: string,
|
||||
callback: (choice: boolean) => void
|
||||
) {
|
||||
constructor(app: App, message: string, callback: (choice: boolean) => void) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
this.callback = callback;
|
||||
|
|
|
|||
|
|
@ -2,132 +2,128 @@ import { NameSegmentType, parseNameSegments } from "./name";
|
|||
import { expect, it, describe } from "vitest";
|
||||
|
||||
describe("parseNameSegments", () => {
|
||||
it("should parse plain text without modification", () => {
|
||||
const input = "Hello World";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello World" },
|
||||
]);
|
||||
});
|
||||
it("should parse plain text without modification", () => {
|
||||
const input = "Hello World";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([{ type: NameSegmentType.Text, text: "Hello World" }]);
|
||||
});
|
||||
|
||||
it("should parse a Wikilink with no other content", () => {
|
||||
const input = "[[Wikilink]]";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should parse a Wikilink with no other content", () => {
|
||||
const input = "[[Wikilink]]";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse text with a Wikilink", () => {
|
||||
const input = "Hello [[Wikilink]]";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should parse text with a Wikilink", () => {
|
||||
const input = "Hello [[Wikilink]]";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse a Markdown link with no other content", () => {
|
||||
const input = "[Text](https://example.com)";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should parse a Markdown link with no other content", () => {
|
||||
const input = "[Text](https://example.com)";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse text with a Markdown link", () => {
|
||||
const input = "Hello [Text](https://example.com)";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should parse text with a Markdown link", () => {
|
||||
const input = "Hello [Text](https://example.com)";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle malformed Wikilinks gracefully", () => {
|
||||
const input = "[[Wikilink";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "[[Wikilink" },
|
||||
]);
|
||||
});
|
||||
it("should handle malformed Wikilinks gracefully", () => {
|
||||
const input = "[[Wikilink";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([{ type: NameSegmentType.Text, text: "[[Wikilink" }]);
|
||||
});
|
||||
|
||||
it("should handle malformed Markdown links gracefully", () => {
|
||||
const input = "[Text](https://example.com";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "[Text](https://example.com" },
|
||||
]);
|
||||
});
|
||||
it("should handle malformed Markdown links gracefully", () => {
|
||||
const input = "[Text](https://example.com";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "[Text](https://example.com" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse a combination of Wikilinks and Markdown links", () => {
|
||||
const input = "Hello [[Wikilink]] and [Text](https://example.com)";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
{ type: NameSegmentType.Text, text: " and " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should parse a combination of Wikilinks and Markdown links", () => {
|
||||
const input = "Hello [[Wikilink]] and [Text](https://example.com)";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
{ type: NameSegmentType.Text, text: " and " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse a combination of Wikilinks and Markdown links with trailing text", () => {
|
||||
const input = "Hello [[Wikilink]] and [Text](https://example.com).";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
{ type: NameSegmentType.Text, text: " and " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
{ type: NameSegmentType.Text, text: "." },
|
||||
]);
|
||||
});
|
||||
it("should parse a combination of Wikilinks and Markdown links with trailing text", () => {
|
||||
const input = "Hello [[Wikilink]] and [Text](https://example.com).";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Text, text: "Hello " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "Wikilink",
|
||||
text: "Wikilink",
|
||||
},
|
||||
{ type: NameSegmentType.Text, text: " and " },
|
||||
{
|
||||
type: NameSegmentType.Link,
|
||||
url: "https://example.com",
|
||||
text: "Text",
|
||||
},
|
||||
{ type: NameSegmentType.Text, text: "." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle multiple Wikilinks correctly", () => {
|
||||
const input = "[[Link1]] and [[Link2]]";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Link, url: "Link1", text: "Link1" },
|
||||
{ type: NameSegmentType.Text, text: " and " },
|
||||
{ type: NameSegmentType.Link, url: "Link2", text: "Link2" },
|
||||
]);
|
||||
});
|
||||
it("should handle multiple Wikilinks correctly", () => {
|
||||
const input = "[[Link1]] and [[Link2]]";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([
|
||||
{ type: NameSegmentType.Link, url: "Link1", text: "Link1" },
|
||||
{ type: NameSegmentType.Text, text: " and " },
|
||||
{ type: NameSegmentType.Link, url: "Link2", text: "Link2" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle empty input gracefully", () => {
|
||||
const input = "";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
it("should handle empty input gracefully", () => {
|
||||
const input = "";
|
||||
const result = parseNameSegments(input);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ export function parseNameSegments(input: string): NameSegment[] {
|
|||
|
||||
// Handle the text before the current link
|
||||
const beforeLink = input.slice(lastIndex, index);
|
||||
if (beforeLink)
|
||||
segments.push({ type: NameSegmentType.Text, text: beforeLink });
|
||||
if (beforeLink) segments.push({ type: NameSegmentType.Text, text: beforeLink });
|
||||
|
||||
// Handle the matched link
|
||||
if (match[1]) {
|
||||
|
|
|
|||
|
|
@ -60,14 +60,11 @@ describe("format duration", () => {
|
|||
[1000 * 60 * 60 * 2, "2h 0s"],
|
||||
[1000 * 60 * 60 * 2.5, "2h 30m 0s"],
|
||||
[1000 * 60 * 60 * 2.505, "2h 30m 18s"],
|
||||
])(
|
||||
'for duration "%s" should expected formatted "%s"',
|
||||
(input, expected) => {
|
||||
const output = formatDurationLong(input);
|
||||
])('for duration "%s" should expected formatted "%s"', (input, expected) => {
|
||||
const output = formatDurationLong(input);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
);
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("format duration short", () => {
|
||||
|
|
@ -76,14 +73,11 @@ describe("format duration short", () => {
|
|||
[1000 * 60 * 60 * 25.25, "25.25h"],
|
||||
[1000 * 60 * 60 * 25.255, "25.25h"],
|
||||
[1000 * 60 * 60 * 50.5, "50.50h"],
|
||||
])(
|
||||
'for duration "%s" should expected formatted "%s"',
|
||||
(input, expected) => {
|
||||
const output = formatDurationShort(input);
|
||||
])('for duration "%s" should expected formatted "%s"', (input, expected) => {
|
||||
const output = formatDurationShort(input);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
);
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("format duration decimal", () => {
|
||||
|
|
@ -92,14 +86,11 @@ describe("format duration decimal", () => {
|
|||
[1000 * 60 * 60 * 25.25, "25.25"],
|
||||
[1000 * 60 * 60 * 25.255, "25.25"],
|
||||
[1000 * 60 * 60 * 50.5, "50.50"],
|
||||
])(
|
||||
'for duration "%s" should expected formatted "%s"',
|
||||
(input, expected) => {
|
||||
const output = formatDurationDecimal(input);
|
||||
])('for duration "%s" should expected formatted "%s"', (input, expected) => {
|
||||
const output = formatDurationDecimal(input);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
);
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("format duration with format", () => {
|
||||
|
|
@ -134,14 +125,11 @@ describe("format duration with format", () => {
|
|||
test.each([
|
||||
["ANY" 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) => {
|
||||
expect(() => {
|
||||
formatDuration(format, input);
|
||||
}).toThrow(error);
|
||||
}
|
||||
);
|
||||
])('for format "%s" with duration "%s" should thrown Error "%s"', (format, input, error) => {
|
||||
expect(() => {
|
||||
formatDuration(format, input);
|
||||
}).toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
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-09-24 18:34:45", "24/09/2024 18:34"],
|
||||
["2023-09-24 20:34:45", "24/09/2023 20:34"],
|
||||
])(
|
||||
'for date time "%s" should expected formatted "%s"',
|
||||
(input, expected) => {
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.pdfRowDateFormat = "DD/MM/YYYY HH:mm";
|
||||
const output = formatPdfRowDate(moment(input), settings);
|
||||
])('for date time "%s" should expected formatted "%s"', (input, expected) => {
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.pdfRowDateFormat = "DD/MM/YYYY HH:mm";
|
||||
const output = formatPdfRowDate(moment(input), settings);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
);
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ import { DurationFormat, TimekeepSettings } from "@/settings";
|
|||
* @param settings The timekeep settings
|
||||
* @returns The formatted timestamp
|
||||
*/
|
||||
export function formatTimestamp(
|
||||
timestamp: Moment,
|
||||
settings: TimekeepSettings
|
||||
): string {
|
||||
export function formatTimestamp(timestamp: Moment, settings: TimekeepSettings): string {
|
||||
return timestamp.format(settings.timestampFormat);
|
||||
}
|
||||
|
||||
|
|
@ -23,10 +20,7 @@ export function formatTimestamp(
|
|||
* @param settings The timekeep settings
|
||||
* @returns The formatted timestamp
|
||||
*/
|
||||
export function formatEditableTimestamp(
|
||||
timestamp: Moment,
|
||||
settings: TimekeepSettings
|
||||
): string {
|
||||
export function formatEditableTimestamp(timestamp: Moment, settings: TimekeepSettings): string {
|
||||
return timestamp.format(settings.editableTimestampFormat);
|
||||
}
|
||||
|
||||
|
|
@ -37,10 +31,7 @@ export function formatEditableTimestamp(
|
|||
* @param settings The timekeep settings
|
||||
* @returns The timestamp
|
||||
*/
|
||||
export function parseEditableTimestamp(
|
||||
formatted: string,
|
||||
settings: TimekeepSettings
|
||||
): Moment {
|
||||
export function parseEditableTimestamp(formatted: string, settings: TimekeepSettings): Moment {
|
||||
return moment(formatted, settings.editableTimestampFormat, true);
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +43,7 @@ export function parseEditableTimestamp(
|
|||
* @throws unknown duration format when format is not supported
|
||||
* @returns The formatted duration
|
||||
*/
|
||||
export function formatDuration(
|
||||
format: DurationFormat,
|
||||
durationMS: number
|
||||
): string {
|
||||
export function formatDuration(format: DurationFormat, durationMS: number): string {
|
||||
switch (format) {
|
||||
case DurationFormat.LONG:
|
||||
return formatDurationLong(durationMS);
|
||||
|
|
@ -124,10 +112,7 @@ export function formatDurationDecimal(durationMS: number): string {
|
|||
* @param settings The settings
|
||||
* @returns The formatted date
|
||||
*/
|
||||
export function formatPdfDate(
|
||||
value: Moment,
|
||||
settings: TimekeepSettings
|
||||
): string {
|
||||
export function formatPdfDate(value: Moment, settings: TimekeepSettings): string {
|
||||
return value.format(settings.pdfDateFormat);
|
||||
}
|
||||
|
||||
|
|
@ -140,9 +125,6 @@ export function formatPdfDate(
|
|||
* @param settings The settings
|
||||
* @returns The formatted timestamp
|
||||
*/
|
||||
export function formatPdfRowDate(
|
||||
value: Moment,
|
||||
settings: TimekeepSettings
|
||||
): string {
|
||||
export function formatPdfRowDate(value: Moment, settings: TimekeepSettings): string {
|
||||
return value.format(settings.pdfRowDateFormat);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ import { TimekeepSettings } from "@/settings";
|
|||
import { CustomOutputFormat } from "@/output";
|
||||
import { Timekeep, stripTimekeepRuntimeData } from "@/timekeep/schema";
|
||||
import { LoadResult, replaceTimekeepCodeblock } from "@/timekeep/parser";
|
||||
import {
|
||||
TFile,
|
||||
TAbstractFile,
|
||||
MarkdownRenderChild,
|
||||
MarkdownPostProcessorContext,
|
||||
} from "obsidian";
|
||||
import { TFile, TAbstractFile, MarkdownRenderChild, MarkdownPostProcessorContext } from "obsidian";
|
||||
import { Timesheet } from "@/components/timesheet";
|
||||
import { TimesheetLoadError } from "@/components/timesheetLoadError";
|
||||
|
||||
|
|
@ -53,17 +48,11 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
|
|||
onload(): void {
|
||||
// Hook file renaming to update the file we are saving to if its renamed
|
||||
this.registerEvent(
|
||||
this.app.vault.on(
|
||||
"rename",
|
||||
(file: TAbstractFile, oldName: string) => {
|
||||
if (
|
||||
file instanceof TFile &&
|
||||
oldName == this.fileSourcePath
|
||||
) {
|
||||
this.fileSourcePath = file.path;
|
||||
}
|
||||
this.app.vault.on("rename", (file: TAbstractFile, oldName: string) => {
|
||||
if (file instanceof TFile && oldName == this.fileSourcePath) {
|
||||
this.fileSourcePath = file.path;
|
||||
}
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
// Render the content
|
||||
|
|
@ -106,10 +95,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
|
|||
this.addChild(timesheet);
|
||||
this.timesheet = timesheet;
|
||||
} else {
|
||||
const timesheet = new TimesheetLoadError(
|
||||
this.containerEl,
|
||||
this.loadResult.error
|
||||
);
|
||||
const timesheet = new TimesheetLoadError(this.containerEl, this.loadResult.error);
|
||||
this.addChild(timesheet);
|
||||
this.timesheet = timesheet;
|
||||
}
|
||||
|
|
@ -134,10 +120,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
|
|||
const editor = activeEditor.editor;
|
||||
|
||||
if (!editor) return;
|
||||
editor.scrollTo(
|
||||
this.restoreScrollInfo.left,
|
||||
this.restoreScrollInfo.top
|
||||
);
|
||||
editor.scrollTo(this.restoreScrollInfo.left, this.restoreScrollInfo.top);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -166,10 +149,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
|
|||
return false;
|
||||
} finally {
|
||||
// Queue scroll restoration after 50ms (Should be enough for the codeblock to re-render)
|
||||
this.restoreScrollTimeout = setTimeout(
|
||||
this.restoreEditorScroll.bind(this),
|
||||
50
|
||||
);
|
||||
this.restoreScrollTimeout = setTimeout(this.restoreEditorScroll.bind(this), 50);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,8 +162,7 @@ export class TimekeepMarkdownView extends MarkdownRenderChild {
|
|||
const sectionInfo = this.context.getSectionInfo(this.containerEl);
|
||||
|
||||
// Ensure we actually have a section to write to
|
||||
if (sectionInfo === null)
|
||||
throw new Error("Section to write did not exist");
|
||||
if (sectionInfo === null) throw new Error("Section to write did not exist");
|
||||
|
||||
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`;
|
||||
|
||||
// Write to the backup file
|
||||
this.app.vault.create(
|
||||
backupFileName,
|
||||
JSON.stringify(stripTimekeepRuntimeData(timekeep))
|
||||
);
|
||||
this.app.vault.create(backupFileName, JSON.stringify(stripTimekeepRuntimeData(timekeep)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,11 +26,7 @@ export class TimekeepMergerModal extends Modal {
|
|||
private exportPdf: boolean;
|
||||
private settings: Store<TimekeepSettings>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
settings: Store<TimekeepSettings>,
|
||||
exportPdf = false
|
||||
) {
|
||||
constructor(app: App, settings: Store<TimekeepSettings>, exportPdf = false) {
|
||||
super(app);
|
||||
|
||||
this.exportPdf = exportPdf;
|
||||
|
|
@ -102,10 +98,7 @@ export class TimekeepMergerModal extends Modal {
|
|||
};
|
||||
|
||||
for (const result of this.selectedResults) {
|
||||
timekeep.entries = [
|
||||
...timekeep.entries,
|
||||
...result.timekeep.entries,
|
||||
];
|
||||
timekeep.entries = [...timekeep.entries, ...result.timekeep.entries];
|
||||
}
|
||||
|
||||
// Close after taking the results as closing resets the list
|
||||
|
|
@ -148,8 +141,7 @@ export class TimekeepMergerModal extends Modal {
|
|||
|
||||
updateSelectAll() {
|
||||
if (this.selectContainer) {
|
||||
this.selectContainer.style.display =
|
||||
this.filteredResults.length > 0 ? "flex" : "none";
|
||||
this.selectContainer.style.display = this.filteredResults.length > 0 ? "flex" : "none";
|
||||
}
|
||||
|
||||
const isAllSelected = this.isAllSelected();
|
||||
|
|
@ -168,9 +160,7 @@ export class TimekeepMergerModal extends Modal {
|
|||
}
|
||||
|
||||
for (const result of this.filteredResults) {
|
||||
const selected = this.selectedResults.find(
|
||||
(selected) => selected.id === result.id
|
||||
);
|
||||
const selected = this.selectedResults.find((selected) => selected.id === result.id);
|
||||
|
||||
if (selected === undefined) {
|
||||
return false;
|
||||
|
|
@ -192,9 +182,7 @@ export class TimekeepMergerModal extends Modal {
|
|||
if (checked) {
|
||||
// Find all results that aren't already selected
|
||||
const toBeSelected = this.filteredResults.filter((result) => {
|
||||
const selected = this.selectedResults.find(
|
||||
(selected) => selected.id === result.id
|
||||
);
|
||||
const selected = this.selectedResults.find((selected) => selected.id === result.id);
|
||||
return selected === undefined;
|
||||
});
|
||||
|
||||
|
|
@ -279,8 +267,7 @@ export class TimekeepMergerModal extends Modal {
|
|||
type: "checkbox",
|
||||
});
|
||||
checkbox.checked =
|
||||
this.selectedResults.find((other) => other.id === result.id) !==
|
||||
undefined;
|
||||
this.selectedResults.find((other) => other.id === result.id) !== undefined;
|
||||
checkbox.id = `timekeep-${result.id}`;
|
||||
|
||||
checkbox.onchange = () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
```dataviewjs
|
||||
// Get the currently open file
|
||||
const targetFile = this.app.vault.getFileByPath("Obsidian Timekeep.md");
|
||||
|
|
@ -32,7 +31,7 @@ function formatDuration(totalTime) {
|
|||
for (const timekeep of timekeeps) {
|
||||
// Create the entries
|
||||
const data = timekeep.entries
|
||||
.map((entry) => {
|
||||
.map((entry) => {
|
||||
let duration = timekeepPlugin.getEntryDuration(entry, currentTime);
|
||||
return [entry.name, duration];
|
||||
})
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*", "types/**/*"]
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*", "types/**/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,4 +38,4 @@
|
|||
"1.14.1": "1.5.12",
|
||||
"1.15.0": "1.5.12",
|
||||
"1.16.0": "1.5.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,52 +7,52 @@ const __filename = fileURLToPath(import.meta.url);
|
|||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig((env) => ({
|
||||
build: {
|
||||
outDir: "dist",
|
||||
sourcemap: env.mode === "production" ? false : "inline",
|
||||
target: "es2018",
|
||||
minify: true,
|
||||
lib: {
|
||||
entry: "src/main.ts",
|
||||
formats: ["cjs"],
|
||||
fileName: () => "main.js",
|
||||
},
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, "src/main.ts"),
|
||||
styles: path.resolve(__dirname, "src/styles.css"),
|
||||
},
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtinModules,
|
||||
],
|
||||
output: {
|
||||
exports: "auto",
|
||||
},
|
||||
},
|
||||
//
|
||||
cssCodeSplit: true,
|
||||
// Inline all assets (.ttf ...etc)
|
||||
assetsInlineLimit: Infinity,
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
sourcemap: env.mode === "production" ? false : "inline",
|
||||
target: "es2018",
|
||||
minify: true,
|
||||
lib: {
|
||||
entry: "src/main.ts",
|
||||
formats: ["cjs"],
|
||||
fileName: () => "main.js",
|
||||
},
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, "src/main.ts"),
|
||||
styles: path.resolve(__dirname, "src/styles.css"),
|
||||
},
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtinModules,
|
||||
],
|
||||
output: {
|
||||
exports: "auto",
|
||||
},
|
||||
},
|
||||
//
|
||||
cssCodeSplit: true,
|
||||
// Inline all assets (.ttf ...etc)
|
||||
assetsInlineLimit: Infinity,
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
|
||||
test: {},
|
||||
test: {},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in a new issue