mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Fix create task footer activation
This commit is contained in:
parent
ab907e6142
commit
0888e14e0c
4 changed files with 171 additions and 2 deletions
|
|
@ -103,6 +103,7 @@ Example:
|
|||
- ([#1326](https://github.com/callumalpass/tasknotes/issues/1326)) Treated empty task frontmatter values as missing, so empty titles fall back to the filename and blank date, priority, and recurrence fields are ignored. Thanks to @craziedde for suggesting this.
|
||||
- ([#1316](https://github.com/callumalpass/tasknotes/issues/1316)) Stopped stale Workspace state from overriding the configured Calendar view mode, so switching Workspaces no longer forces an old Day/Week/List choice over the view default. Thanks to @kmaustral for reporting this and following up.
|
||||
- ([#1302](https://github.com/callumalpass/tasknotes/issues/1302)) Read task frontmatter directly from the task file when Obsidian has not indexed its metadata yet, so visible tasks can still be opened, updated, and selected instead of reporting "Task not found". Thanks to @WorldTeacher for reporting this and @Steven-AA for confirming it.
|
||||
- ([#1297](https://github.com/callumalpass/tasknotes/issues/1297)) Made the Create or open task footer tappable, so mobile users can create the typed task without needing the Shift+Enter shortcut. Thanks to @slipstyle for reporting this.
|
||||
- Made TaskNotes Kanban drags show a lightweight held-card preview, so the dragged card remains easy to track while the source column opens the drop slot.
|
||||
- ([#1423](https://github.com/callumalpass/tasknotes/issues/1423)) Refreshed TaskNotes Bases views when subtasks are deleted, so expanded project cards drop removed subtasks and stop showing project controls when the last subtask is gone. Thanks to @normenmueller for reporting this.
|
||||
- ([#1419](https://github.com/callumalpass/tasknotes/issues/1419)) Kept custom user field name, key, and default-value edits in Task Properties settings when closing settings without first blurring the field. Thanks to @s33a for reporting this and @kacoroski for the follow-up.
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
private createFooterEl: HTMLElement | null = null;
|
||||
private currentQuery = "";
|
||||
private resultHandled = false;
|
||||
private isCreatingTask = false;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
|
|
@ -146,7 +147,12 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
// We want to append our footer inside the modalEl, after .prompt
|
||||
const modalContentEl = this.modalEl.querySelector(".prompt")?.parentElement || this.modalEl;
|
||||
|
||||
this.createFooterEl = createDiv({ cls: "task-selector-create-footer" });
|
||||
this.createFooterEl = modalContentEl.createDiv({ cls: "task-selector-create-footer" });
|
||||
this.createFooterEl.setAttribute("role", "button");
|
||||
this.createFooterEl.setAttribute("aria-hidden", "true");
|
||||
this.createFooterEl.tabIndex = -1;
|
||||
this.createFooterEl.addEventListener("click", this.handleCreateFooterActivation);
|
||||
this.createFooterEl.addEventListener("keydown", this.handleCreateFooterKeydown);
|
||||
this.createFooterEl.classList.remove(
|
||||
"tn-static-display-block-2a1b75c9",
|
||||
"tn-static-display-flex-4d51fc62",
|
||||
|
|
@ -158,9 +164,33 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
"tn-static-min-height-800px-997b4c8c"
|
||||
);
|
||||
this.createFooterEl.classList.add("tn-static-display-none-6b99de8b");
|
||||
modalContentEl.appendChild(this.createFooterEl);
|
||||
|
||||
if (this.currentQuery) {
|
||||
this.updateCreateFooter(this.currentQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCreateFooterActivation = (event: MouseEvent | KeyboardEvent): void => {
|
||||
if (
|
||||
!this.createFooterEl ||
|
||||
this.createFooterEl.classList.contains("tn-static-display-none-6b99de8b")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void this.createNewTask();
|
||||
};
|
||||
|
||||
private handleCreateFooterKeydown = (event: KeyboardEvent): void => {
|
||||
if (event.key !== "Enter" && event.key !== " ") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleCreateFooterActivation(event);
|
||||
};
|
||||
|
||||
private handleInputChange = (): void => {
|
||||
const query = this.inputEl.value.trim();
|
||||
this.currentQuery = query;
|
||||
|
|
@ -182,6 +212,9 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
"tn-static-min-height-800px-997b4c8c"
|
||||
);
|
||||
this.createFooterEl.classList.add("tn-static-display-none-6b99de8b");
|
||||
this.createFooterEl.setAttribute("aria-hidden", "true");
|
||||
this.createFooterEl.removeAttribute("aria-label");
|
||||
this.createFooterEl.tabIndex = -1;
|
||||
this.createFooterEl.empty();
|
||||
return;
|
||||
}
|
||||
|
|
@ -201,6 +234,12 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
"tn-static-min-height-800px-997b4c8c"
|
||||
);
|
||||
this.createFooterEl.classList.add("tn-static-display-flex-75816cae");
|
||||
this.createFooterEl.setAttribute("aria-hidden", "false");
|
||||
this.createFooterEl.setAttribute(
|
||||
"aria-label",
|
||||
`${this.translate("modals.taskSelectorWithCreate.footer.createLabel").trim()} ${parsed.title}`.trim()
|
||||
);
|
||||
this.createFooterEl.tabIndex = 0;
|
||||
|
||||
// Icon
|
||||
const iconDiv = this.createFooterEl.createDiv({
|
||||
|
|
@ -263,6 +302,9 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
"tn-static-min-height-800px-997b4c8c"
|
||||
);
|
||||
this.createFooterEl.classList.add("tn-static-display-none-6b99de8b");
|
||||
this.createFooterEl.setAttribute("aria-hidden", "true");
|
||||
this.createFooterEl.removeAttribute("aria-label");
|
||||
this.createFooterEl.tabIndex = -1;
|
||||
this.createFooterEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -367,12 +409,18 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
}
|
||||
|
||||
private async createNewTask(): Promise<void> {
|
||||
if (this.isCreatingTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = this.inputEl.value.trim();
|
||||
if (!query) {
|
||||
new Notice(this.translate("modals.taskSelectorWithCreate.notices.emptyQuery"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCreatingTask = true;
|
||||
|
||||
try {
|
||||
// Parse the query using NLP
|
||||
const parsed = this.nlParser.parseInput(query);
|
||||
|
|
@ -402,6 +450,8 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
console.error("Failed to create task:", error);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new Notice(this.translate("modals.taskCreation.notices.failure", { message }));
|
||||
} finally {
|
||||
this.isCreatingTask = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -478,6 +528,8 @@ export class TaskSelectorWithCreateModal extends SuggestModal<TaskInfo> {
|
|||
|
||||
// Clean up footer element
|
||||
if (this.createFooterEl) {
|
||||
this.createFooterEl.removeEventListener("click", this.handleCreateFooterActivation);
|
||||
this.createFooterEl.removeEventListener("keydown", this.handleCreateFooterKeydown);
|
||||
this.createFooterEl.remove();
|
||||
this.createFooterEl = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,22 @@
|
|||
border-top: 1px solid var(--background-modifier-border);
|
||||
margin-top: auto;
|
||||
min-height: 60px;
|
||||
cursor: pointer;
|
||||
transition: background-color 120ms ease;
|
||||
}
|
||||
|
||||
.task-selector-create-footer:hover,
|
||||
.task-selector-create-footer:focus-visible {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.task-selector-create-footer:focus-visible {
|
||||
outline: 2px solid var(--background-modifier-border-focus);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.task-selector-create-footer:active {
|
||||
background: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.task-selector-create-footer__icon {
|
||||
|
|
|
|||
100
tests/unit/issues/issue-1297-task-selector-create-footer.test.ts
Normal file
100
tests/unit/issues/issue-1297-task-selector-create-footer.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { TaskSelectorWithCreateModal } from "../../../src/modals/TaskSelectorWithCreateModal";
|
||||
import { NLPSuggest } from "../../../src/modals/taskCreationSuggest";
|
||||
import { MockObsidian } from "../../__mocks__/obsidian";
|
||||
import type { App } from "obsidian";
|
||||
|
||||
jest.mock("obsidian");
|
||||
|
||||
const mockClose = jest.fn();
|
||||
|
||||
jest.mock("../../../src/modals/taskCreationSuggest", () => ({
|
||||
NLPSuggest: jest.fn().mockImplementation(() => ({
|
||||
close: mockClose,
|
||||
})),
|
||||
}));
|
||||
|
||||
const createMockApp = (mockApp: unknown): App => mockApp as App;
|
||||
|
||||
describe("Issue #1297: Create or open task footer activation", () => {
|
||||
let mockApp: App;
|
||||
let mockPlugin: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllMocks();
|
||||
MockObsidian.reset();
|
||||
|
||||
mockApp = createMockApp(MockObsidian.createMockApp());
|
||||
mockPlugin = {
|
||||
app: mockApp,
|
||||
settings: {
|
||||
customStatuses: [],
|
||||
customPriorities: [],
|
||||
defaultTaskStatus: "open",
|
||||
defaultTaskPriority: "normal",
|
||||
nlpDefaultToScheduled: true,
|
||||
nlpLanguage: "en",
|
||||
nlpTriggers: { enabled: true, triggers: [] },
|
||||
userFields: [],
|
||||
calendarViewSettings: { locale: "en" },
|
||||
},
|
||||
i18n: {
|
||||
translate: jest.fn((key: string) => key),
|
||||
},
|
||||
statusManager: {
|
||||
isCompletedStatus: jest.fn(() => false),
|
||||
getStatusConfig: jest.fn(() => null),
|
||||
},
|
||||
taskService: {
|
||||
createTask: jest.fn().mockResolvedValue({
|
||||
taskInfo: {
|
||||
title: "Buy milk",
|
||||
status: "open",
|
||||
priority: "normal",
|
||||
path: "Tasks/Buy milk.md",
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("creates the typed task when the footer is tapped", async () => {
|
||||
const onResult = jest.fn();
|
||||
const modal = new TaskSelectorWithCreateModal(mockApp, mockPlugin, [], {
|
||||
onResult,
|
||||
});
|
||||
|
||||
modal.onOpen();
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(NLPSuggest).toHaveBeenCalledWith(mockApp, modal.inputEl, mockPlugin);
|
||||
|
||||
modal.inputEl.value = "Buy milk";
|
||||
modal.inputEl.dispatchEvent(new Event("input"));
|
||||
|
||||
const footer = modal.modalEl.querySelector(".task-selector-create-footer") as HTMLElement;
|
||||
expect(footer).not.toBeNull();
|
||||
expect(footer.getAttribute("role")).toBe("button");
|
||||
expect(footer.tabIndex).toBe(0);
|
||||
|
||||
footer.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
|
||||
expect(mockPlugin.taskService.createTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Buy milk",
|
||||
status: "open",
|
||||
priority: "normal",
|
||||
})
|
||||
);
|
||||
expect(onResult).toHaveBeenCalledWith({
|
||||
type: "created",
|
||||
task: expect.objectContaining({ title: "Buy milk" }),
|
||||
});
|
||||
expect(mockClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue