mirror of
https://github.com/savar-g/taskline.git
synced 2026-07-22 08:33:55 +00:00
Address Obsidian review feedback
This commit is contained in:
parent
b3d1bceb89
commit
082f318bf7
19 changed files with 4669 additions and 57 deletions
39
.github/workflows/release.yml
vendored
39
.github/workflows/release.yml
vendored
|
|
@ -6,10 +6,10 @@ on:
|
|||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
|
|
@ -29,6 +29,9 @@ jobs:
|
|||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Run Obsidian lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
|
||||
|
|
@ -37,6 +40,38 @@ jobs:
|
|||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: node -e 'const manifest = require("./manifest.json"); if (manifest.version !== process.env.RELEASE_TAG) { throw new Error(`Tag ${process.env.RELEASE_TAG} does not match manifest ${manifest.version}`); }'
|
||||
|
||||
- name: Stage release assets
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: taskline-release
|
||||
path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
attestations: write
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download release assets
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: taskline-release
|
||||
|
||||
- name: Attest release assets
|
||||
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Taskline does not connect to meeting or messaging services itself. Your capture
|
|||
|
||||
## Requirements
|
||||
|
||||
- Obsidian 1.5.0 or later
|
||||
- Obsidian 1.7.2 or later
|
||||
- Markdown task notes that use `- [ ] Task` syntax
|
||||
- The [Tasks plugin](https://github.com/obsidian-tasks-group/obsidian-tasks) only if you complete recurring tasks through Taskline
|
||||
|
||||
|
|
|
|||
20
eslint.config.mjs
Normal file
20
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import tsParser from "@typescript-eslint/parser";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
|
||||
export default defineConfig([
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: { project: "./tsconfig.json" },
|
||||
},
|
||||
rules: {
|
||||
"obsidianmd/ui/sentence-case": ["warn", { brands: ["Taskline"], acronyms: ["JSON", "ID"] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["esbuild.config.mjs", "main.js", "node_modules/**", "tests/**"],
|
||||
},
|
||||
]);
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "vault-tasks",
|
||||
"name": "Taskline",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Configurable task capture and Today view for Obsidian",
|
||||
"version": "0.1.1",
|
||||
"minAppVersion": "1.7.2",
|
||||
"description": "Configurable task capture and Today view.",
|
||||
"author": "Taskline contributors",
|
||||
"authorUrl": "https://github.com/Savar-G/taskline",
|
||||
"authorUrl": "https://github.com/Savar-G",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
4556
package-lock.json
generated
4556
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "taskline",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"description": "Configurable task capture and Today view for Obsidian",
|
||||
"description": "Configurable task capture and Today view.",
|
||||
"main": "main.js",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"lint": "eslint . --max-warnings 2",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
@ -25,7 +26,10 @@
|
|||
"productivity"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"esbuild": "0.28.1",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-plugin-obsidianmd": "^0.4.1",
|
||||
"obsidian": "^1.13.1",
|
||||
"tslib": "2.6.2",
|
||||
"typescript": "5.9.3",
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ function parseProtectedTitle(input: string): ProtectedTitle | null {
|
|||
if (char !== '"') continue;
|
||||
if (i + 1 < input.length && !/\s/.test(input[i + 1])) return null;
|
||||
try {
|
||||
const value = JSON.parse(input.slice(0, i + 1));
|
||||
const value: unknown = JSON.parse(input.slice(0, i + 1));
|
||||
return typeof value === "string" ? { value, end: i + 1 } : null;
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const SIGNIFIER_TOKEN =
|
|||
"|✅\\s*\\d{4}-\\d{2}-\\d{2}" +
|
||||
"|❌\\s*\\d{4}-\\d{2}-\\d{2}";
|
||||
|
||||
const TRAILING_RUN_RE = new RegExp(`(?:\\s*(?:${SIGNIFIER_TOKEN}))+$`);
|
||||
const TRAILING_RUN_RE = new RegExp(`(?:\\s*(?:${SIGNIFIER_TOKEN}))+$`, "u");
|
||||
|
||||
/** Inserts annotation text immediately before the trailing signifier run (or at the end of
|
||||
* the line if there is no such run), preserving the invariant. */
|
||||
|
|
|
|||
16
src/main.ts
16
src/main.ts
|
|
@ -26,7 +26,7 @@ export default class TasklinePlugin extends Plugin {
|
|||
|
||||
async onload(): Promise<void> {
|
||||
this.loaded = true;
|
||||
const rawSettings = await this.loadData();
|
||||
const rawSettings: unknown = await this.loadData();
|
||||
const loadedWorkspace = compileWorkspace(rawSettings);
|
||||
this.settingsLoadIssues = loadedWorkspace.issues.filter((issue) => issue.level === "error");
|
||||
this.rejectedSettingsRaw = this.settingsLoadIssues.length > 0 ? rawSettings : null;
|
||||
|
|
@ -37,8 +37,14 @@ export default class TasklinePlugin extends Plugin {
|
|||
this.addSettingTab(new TasklineSettingTab(this));
|
||||
|
||||
this.registerView(VIEW_TYPE_TODAY, (leaf: WorkspaceLeaf) => new TodayView(leaf, this));
|
||||
this.addRibbonIcon("check-circle", "Open Taskline Today", () => void this.openTodayView());
|
||||
this.addCommand({ id: "open-today", name: "Open Today", callback: () => void this.openTodayView() });
|
||||
this.addRibbonIcon("check-circle", "Open Taskline today", () => {
|
||||
this.openTodayView().catch((error: unknown) => console.error("taskline: could not open Today view", error));
|
||||
});
|
||||
this.addCommand({
|
||||
id: "open-today",
|
||||
name: "Open today",
|
||||
callback: () => this.openTodayView().catch((error: unknown) => console.error("taskline: could not open Today view", error)),
|
||||
});
|
||||
this.addCommand({ id: "add-task", name: "Add task", callback: () => this.openCapture() });
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
|
|
@ -173,11 +179,11 @@ export default class TasklinePlugin extends Plugin {
|
|||
async openTodayView(): Promise<void> {
|
||||
const existingLeaf = this.app.workspace.getLeavesOfType(VIEW_TYPE_TODAY)[0];
|
||||
if (existingLeaf) {
|
||||
this.app.workspace.revealLeaf(existingLeaf);
|
||||
await this.app.workspace.revealLeaf(existingLeaf);
|
||||
return;
|
||||
}
|
||||
const leaf = this.app.workspace.getLeaf("tab");
|
||||
await leaf.setViewState({ type: VIEW_TYPE_TODAY, active: true });
|
||||
this.app.workspace.revealLeaf(leaf);
|
||||
await this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export function parseTaskLine(line: string, ctx: ParseCtx): VtTask | null {
|
|||
|
||||
// recurrence - everything after 🔁 up to the next signifier emoji or end of string
|
||||
let recurrence: string | undefined;
|
||||
const recRe = new RegExp(`🔁\\s*([^${SIGNIFIER_CLASS}]*)`);
|
||||
const recRe = new RegExp(`🔁\\s*([^${SIGNIFIER_CLASS}]*)`, "u");
|
||||
const recMatch = work.match(recRe);
|
||||
if (recMatch) {
|
||||
recurrence = recMatch[1].trim();
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ export class TasklineSettingTab extends PluginSettingTab {
|
|||
const draft = createSettingsDraft(this.taskline.settings, this.taskline.rejectedSettingsRaw);
|
||||
const parseErrors = new Map<string, string>();
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "Taskline settings" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Changes remain a draft until Apply. Taskline never creates or changes task files from settings.",
|
||||
text: "Changes remain a draft until you apply them. Taskline never creates or changes task files from settings.",
|
||||
});
|
||||
|
||||
this.renderIssues(
|
||||
|
|
@ -47,7 +46,7 @@ export class TasklineSettingTab extends PluginSettingTab {
|
|||
}
|
||||
draftIssues.show();
|
||||
draftIssues.setAttr("role", "alert");
|
||||
draftIssues.createEl("h3", { text: "Draft validation" });
|
||||
new Setting(draftIssues).setName("Draft validation").setHeading();
|
||||
const list = draftIssues.createEl("ul");
|
||||
for (const message of messages) list.createEl("li", { text: message });
|
||||
};
|
||||
|
|
@ -95,7 +94,7 @@ export class TasklineSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(containerEl)
|
||||
.setName("Fallback capture destination")
|
||||
.setDesc("JSON object with sourceId and a level-two heading, or null.")
|
||||
.setDesc("JSON object with source ID and a level-two heading, or null.")
|
||||
.addTextArea((text) => {
|
||||
text.inputEl.rows = 3;
|
||||
text.setValue(JSON.stringify(draft.fallbackCaptureDestination, null, 2));
|
||||
|
|
@ -143,7 +142,7 @@ export class TasklineSettingTab extends PluginSettingTab {
|
|||
if (issues.length === 0) return;
|
||||
const block = this.containerEl.createDiv({ cls: "vt-settings-issues" });
|
||||
block.setAttr("role", "status");
|
||||
block.createEl("h3", { text: title });
|
||||
new Setting(block).setName(title).setHeading();
|
||||
const list = block.createEl("ul");
|
||||
for (const issue of issues) {
|
||||
list.createEl("li", { text: `${issue.level === "error" ? "Error" : "Warning"}: ${issue.path} - ${issue.message}` });
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ export class TodayView extends ItemView {
|
|||
const tasks = this.filtered(group.tasks);
|
||||
if (tasks.length === 0) continue;
|
||||
anyVisible = true;
|
||||
if (group.mode === "by-heading") this.buildGroupedSourceSection(parent, group.label, tasks, today, group.color);
|
||||
if (group.mode === "by-heading") this.buildGroupedSourceSection(parent, group.label, tasks, today);
|
||||
else this.buildTaskSection(parent, group.label, this.sortTasks(tasks), today, { dot: areaColor(group.key, this.plugin.workspace) });
|
||||
}
|
||||
|
||||
|
|
@ -536,7 +536,7 @@ export class TodayView extends ItemView {
|
|||
}
|
||||
|
||||
/** A by-heading source group renders one group header with source headings as sub-labels. */
|
||||
private buildGroupedSourceSection(parent: HTMLElement, label: string, tasks: VtTask[], today: Date, color?: string): void {
|
||||
private buildGroupedSourceSection(parent: HTMLElement, label: string, tasks: VtTask[], today: Date): void {
|
||||
const section = parent.createDiv({ cls: "vt-section" });
|
||||
this.buildSectionHead(section, label, tasks.length, { dot: areaColor(label, this.plugin.workspace) });
|
||||
|
||||
|
|
@ -620,7 +620,6 @@ export class TodayView extends ItemView {
|
|||
row.removeAttribute("tabindex");
|
||||
const ring = row.querySelector<HTMLElement>(".vt-ring");
|
||||
if (ring) {
|
||||
ring.style.pointerEvents = "none";
|
||||
ring.dataset.status = task.status;
|
||||
ring.removeAttribute("role");
|
||||
ring.setAttr("aria-hidden", "true");
|
||||
|
|
|
|||
|
|
@ -295,8 +295,8 @@ export abstract class CaptureModalBase extends Modal {
|
|||
|
||||
private syncInputHeight(): void {
|
||||
if (!this.wrapsInput()) return;
|
||||
this.input.style.height = "auto";
|
||||
this.input.style.height = `${Math.min(this.input.scrollHeight, 160)}px`;
|
||||
this.input.setCssStyles({ height: "auto" });
|
||||
this.input.setCssStyles({ height: `${Math.min(this.input.scrollHeight, 160)}px` });
|
||||
}
|
||||
|
||||
/** Repaints the highlight backdrop from the current parse. Synchronous and cheap so typing
|
||||
|
|
@ -376,7 +376,6 @@ export abstract class CaptureModalBase extends Modal {
|
|||
ring.removeAttribute("aria-checked");
|
||||
ring.removeAttribute("aria-label");
|
||||
ring.removeAttribute("tabindex");
|
||||
ring.style.pointerEvents = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ export interface AnimToken {
|
|||
cancelled: boolean;
|
||||
}
|
||||
|
||||
const CANCELLED = Symbol("vt-anim-cancelled");
|
||||
class AnimationCancelledError extends Error {
|
||||
constructor() {
|
||||
super("Taskline animation cancelled");
|
||||
this.name = "AnimationCancelledError";
|
||||
}
|
||||
}
|
||||
|
||||
export function prefersReducedMotion(): boolean {
|
||||
return (
|
||||
|
|
@ -21,28 +26,26 @@ function sleep(ms: number): Promise<void> {
|
|||
}
|
||||
|
||||
function checkpoint(token?: AnimToken): void {
|
||||
if (token?.cancelled) throw CANCELLED;
|
||||
if (token?.cancelled) throw new AnimationCancelledError();
|
||||
}
|
||||
|
||||
export function isCancellation(err: unknown): boolean {
|
||||
return err === CANCELLED;
|
||||
return err instanceof AnimationCancelledError;
|
||||
}
|
||||
|
||||
/** Collapses a row to zero height then resolves. One-shot max-height animation (allowed off
|
||||
* the typing path per DESIGN section 4). Reduced motion -> instant. */
|
||||
async function collapseRow(row: HTMLElement, reduced: boolean): Promise<void> {
|
||||
if (reduced) {
|
||||
row.style.display = "none";
|
||||
row.setCssStyles({ display: "none" });
|
||||
return;
|
||||
}
|
||||
const start = row.scrollHeight;
|
||||
row.style.maxHeight = `${start}px`;
|
||||
row.style.overflow = "hidden";
|
||||
row.setCssStyles({ maxHeight: `${start}px`, overflow: "hidden" });
|
||||
// Force reflow so the transition has a start value to animate from.
|
||||
void row.offsetHeight;
|
||||
row.classList.add("vt-collapsing");
|
||||
row.style.maxHeight = "0px";
|
||||
row.style.opacity = "0";
|
||||
row.setCssStyles({ maxHeight: "0px", opacity: "0" });
|
||||
await sleep(160);
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +114,7 @@ export function revertRow(row: HTMLElement): void {
|
|||
"vt-confirming",
|
||||
"vt-reduced"
|
||||
);
|
||||
row.style.removeProperty("max-height");
|
||||
row.style.removeProperty("overflow");
|
||||
row.style.removeProperty("opacity");
|
||||
row.style.removeProperty("display");
|
||||
row.setCssStyles({ maxHeight: "", overflow: "", opacity: "", display: "" });
|
||||
const ring = row.querySelector(".vt-ring");
|
||||
ring?.setAttribute("aria-checked", "false");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import {
|
|||
assertSameFileProposal,
|
||||
blockLength,
|
||||
completeLine,
|
||||
cancelLine,
|
||||
editLineText,
|
||||
joinText,
|
||||
locateLine,
|
||||
moveTaskWithinText,
|
||||
rollbackInsertedBlockText,
|
||||
|
|
@ -20,7 +18,6 @@ import {
|
|||
TasksPluginApiV1,
|
||||
VtRecurrenceUnavailableError,
|
||||
InsertedBlockReceipt,
|
||||
VtPartialMoveError,
|
||||
VtStaleError,
|
||||
} from "./writerCore";
|
||||
|
||||
|
|
|
|||
|
|
@ -118,10 +118,10 @@ export async function runCrossFileMove<T>(operations: {
|
|||
const receipt = await operations.appendDestination();
|
||||
try {
|
||||
await operations.removeSource();
|
||||
} catch (sourceError) {
|
||||
} catch (sourceError: unknown) {
|
||||
try {
|
||||
await operations.rollbackDestination(receipt);
|
||||
} catch (rollbackError) {
|
||||
} catch (rollbackError: unknown) {
|
||||
throw new VtPartialMoveError(
|
||||
"taskline: source removal failed and the exact destination insertion could not be rolled back. The task may now exist in both files; verify both files and remove only the extra destination copy.",
|
||||
{ sourceError, rollbackError }
|
||||
|
|
@ -198,7 +198,7 @@ function todayIso(): string {
|
|||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
}
|
||||
|
||||
export function cancelLine(task: VtTask, line: string, source?: string): string {
|
||||
export function cancelLine(line: string, source?: string): string {
|
||||
const annotated = source ? insertAnnotation(line, `(reconciled from ${source})`) : line;
|
||||
return insertAnnotation(setStatusChar(annotated, "-"), `❌ ${todayIso()}`);
|
||||
}
|
||||
|
|
@ -211,7 +211,7 @@ export function applyProposalText(
|
|||
): string {
|
||||
const source = proposal.source ?? "proposal";
|
||||
const mutated = editLineText(content, task, (line) => proposal.action === "cancel"
|
||||
? cancelLine(task, line, source)
|
||||
? cancelLine(line, source)
|
||||
: completeLine(api, task, insertAnnotation(line, `(reconciled from ${source})`)));
|
||||
return editLineText(mutated, { rawLine: proposal.rawLine }, () => []);
|
||||
}
|
||||
|
|
|
|||
12
styles.css
12
styles.css
|
|
@ -898,8 +898,8 @@
|
|||
.vt-capture-modal .vt-capture-input:hover,
|
||||
.vt-capture-modal .vt-capture-input:focus,
|
||||
.vt-capture-modal .vt-capture-input:active {
|
||||
color: transparent !important;
|
||||
background: transparent !important;
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
|
|
@ -1416,8 +1416,7 @@
|
|||
.vt-tok--date { --vt-tok-base: var(--vt-tok-date); }
|
||||
.vt-tok--scheduled {
|
||||
--vt-tok-base: var(--vt-tok-date);
|
||||
text-decoration: underline dotted var(--vt-tok-date);
|
||||
text-underline-offset: 3px;
|
||||
border-bottom: 1px dotted var(--vt-tok-date);
|
||||
}
|
||||
.vt-tok--area { --vt-tok-base: var(--vt-tok-area); }
|
||||
.vt-tok--recurrence { --vt-tok-base: var(--vt-tok-recur); }
|
||||
|
|
@ -1448,6 +1447,7 @@
|
|||
/* the preview ring is display-only; drop the pointer affordance */
|
||||
.vt-capture-preview .vt-ring {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* destination indicator */
|
||||
|
|
@ -1696,6 +1696,7 @@
|
|||
.vt-row--done .vt-ring {
|
||||
--vt-ring-color: var(--vt-text-faint);
|
||||
background: color-mix(in srgb, var(--vt-text-faint) 30%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vt-row--done:hover {
|
||||
|
|
@ -1756,8 +1757,7 @@
|
|||
.vt-task-row {
|
||||
display: grid;
|
||||
grid-template-columns: var(--vt-ring-size) minmax(0, 1fr);
|
||||
column-gap: var(--vt-space-3);
|
||||
row-gap: var(--vt-space-1);
|
||||
gap: var(--vt-space-1) var(--vt-space-3);
|
||||
}
|
||||
|
||||
.vt-ring {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ describe("atomic text mutations", () => {
|
|||
});
|
||||
|
||||
it("serializes cancellation without completion semantics", () => {
|
||||
expect(cancelLine(makeTask({ title: "Drop" }), "- [ ] Drop")).toMatch(/^- \[-\] Drop ❌ \d{4}-\d{2}-\d{2}$/);
|
||||
expect(cancelLine("- [ ] Drop")).toMatch(/^- \[-\] Drop ❌ \d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
|
||||
it("preserves CRLF while editing and appending", () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"0.1.0": "1.5.0"
|
||||
"0.1.0": "1.5.0",
|
||||
"0.1.1": "1.7.2"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue