Skip to content

Commit da71c84

Browse files
committed
Clean up lint and modal styling
1 parent 9d0665e commit da71c84

55 files changed

Lines changed: 551 additions & 259 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,4 @@ AGENTS.md
101101
/release-videos/_audio/*.wav
102102
/release-videos/_audio/__pycache__/
103103
/release-videos/4.5.2/*.mp4
104+
/release-videos

docs/releases/unreleased.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,8 @@ Example:
2828

2929
- Improved local lint checks so package and CSS issues reported by Obsidian's online review can be caught before submission.
3030
- Cleaned up stylesheet compatibility issues reported by Obsidian's online review without changing the intended TaskNotes appearance.
31+
- Improved spacing and alignment in task, timeblock, and webhook modals after the stylesheet cleanup.
32+
- Reworked optional calendar and ICS refresh scheduling to avoid `setInterval` around network requests while preserving the same refresh intervals.
33+
- Cleaned up additional source patterns reported by Obsidian's online review, including unsafe default object stringification and redundant external type unions.
34+
- Prevented a console error when plugin listeners are cleaned up after reloads.
35+
- Added a local lint check to prevent `setInterval` from being reintroduced in plugin source.

eslint.config.mjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,59 @@ const pluginReviewRules = {
124124
};
125125
},
126126
},
127+
"no-set-interval": {
128+
meta: {
129+
type: "problem",
130+
docs: {
131+
description:
132+
"Disallow setInterval usage so periodic work uses explicit rescheduling and avoids Obsidian review telemetry heuristics.",
133+
},
134+
messages: {
135+
noSetInterval:
136+
"Use a self-rescheduling setTimeout instead of setInterval.",
137+
noClearInterval:
138+
"Use clearTimeout with the matching self-rescheduling setTimeout.",
139+
},
140+
schema: [],
141+
},
142+
create(context) {
143+
function getCallName(callee) {
144+
if (callee.type === "Identifier") {
145+
return callee.name;
146+
}
147+
148+
if (
149+
callee.type === "MemberExpression" &&
150+
!callee.computed &&
151+
callee.property.type === "Identifier"
152+
) {
153+
return callee.property.name;
154+
}
155+
156+
return null;
157+
}
158+
159+
return {
160+
CallExpression(node) {
161+
const callName = getCallName(node.callee);
162+
163+
if (callName === "setInterval") {
164+
context.report({
165+
node: node.callee,
166+
messageId: "noSetInterval",
167+
});
168+
}
169+
170+
if (callName === "clearInterval") {
171+
context.report({
172+
node: node.callee,
173+
messageId: "noClearInterval",
174+
});
175+
}
176+
},
177+
};
178+
},
179+
},
127180
},
128181
};
129182

@@ -224,6 +277,7 @@ export default [
224277
"no-new-func": "error",
225278
"@microsoft/sdl/no-inner-html": "warn",
226279
"plugin-review/require-eslint-directive-description": "warn",
280+
"plugin-review/no-set-interval": "warn",
227281
"obsidianmd/no-static-styles-assignment": "warn",
228282
"obsidianmd/rule-custom-message": "warn",
229283
"obsidianmd/ui/sentence-case": "warn",

src/api/WebhookController.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -500,12 +500,11 @@ export class WebhookController extends BaseController {
500500
if (typeof value === "string") {
501501
return value;
502502
}
503-
if (
504-
typeof value === "number" ||
505-
typeof value === "boolean" ||
506-
typeof value === "bigint"
507-
) {
508-
return String(value);
503+
if (typeof value === "number" || typeof value === "bigint") {
504+
return value.toString();
505+
}
506+
if (typeof value === "boolean") {
507+
return value ? "true" : "false";
509508
}
510509

511510
const serialized = JSON.stringify(value);

src/api/httpTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export interface HTTPRequestLike {
22
method?: string;
33
url?: string;
44
headers: Record<string, string | string[] | undefined>;
5-
on(event: "data", listener: (chunk: Buffer | string) => void): void;
5+
on(event: "data", listener: (chunk: string | { toString(): string }) => void): void;
66
on(event: "end", listener: () => void): void;
77
on(event: "error", listener: (error: Error) => void): void;
88
}

src/api/httpUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ export function sendJSONResponse(res: HTTPResponseLike, statusCode: number, data
2222
export function parseJSONBody(req: HTTPRequestLike): Promise<Record<string, unknown>> {
2323
return new Promise((resolve, reject) => {
2424
let body = "";
25-
req.on("data", (chunk: Buffer | string) => {
26-
body += chunk.toString();
27-
});
25+
req.on("data", (chunk: string | { toString(): string }) => {
26+
body += chunk.toString();
27+
});
2828
req.on("end", () => {
2929
try {
3030
resolve(body ? JSON.parse(body) : {});

src/bases/BasesDataAdapter.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import type {
55
BasesPropertyId,
66
BasesView,
77
TFile,
8-
Value,
98
} from "obsidian";
109
import { stringifyUnknown } from "../utils/stringUtils";
1110

1211
type BasesViewDataSource = Pick<BasesView, "config" | "data">;
1312

14-
type BasesValueInternals = Value & {
13+
type BasesValueInternals = {
1514
data?: unknown;
1615
date?: Date;
1716
file?: TFile;
@@ -25,7 +24,7 @@ type BasesValueInternals = Value & {
2524
};
2625
};
2726

28-
type BasesEntryInternals = BasesEntry & {
27+
type BasesEntryInternals = {
2928
frontmatter?: Record<string, unknown>;
3029
properties?: Record<string, unknown>;
3130
};
@@ -47,7 +46,7 @@ export class BasesDataAdapter {
4746
*/
4847
extractDataItems(): BasesDataItem[] {
4948
const entries = this.basesView.data.data;
50-
return entries.map((entry: BasesEntry) => ({
49+
return entries.map((entry) => ({
5150
key: entry.file.path,
5251
data: entry,
5352
file: entry.file,
@@ -133,8 +132,12 @@ export class BasesDataAdapter {
133132
* Handles: PrimitiveValue, ListValue, DateValue, FileValue, NullValue, etc.
134133
*/
135134
private convertValueToNative(value: unknown): unknown {
136-
const basesValue = value as BasesValueInternals | null | undefined;
137-
if (basesValue == null || basesValue.constructor?.name === "NullValue") {
135+
if (value === null || value === undefined) {
136+
return null;
137+
}
138+
139+
const basesValue = value as BasesValueInternals;
140+
if (basesValue.constructor?.name === "NullValue") {
138141
return null;
139142
}
140143

@@ -174,14 +177,15 @@ export class BasesDataAdapter {
174177
// FileValue
175178
if (basesValue.file) {
176179
return basesValue.file.path;
177-
}
178-
179-
// Fallback: try to extract raw data
180-
if (typeof basesValue.toString === "function") {
181-
const stringValue = basesValue.toString();
182-
if (stringValue !== "[object Object]") {
183-
return stringValue;
184180
}
181+
182+
// Fallback: try to extract raw data
183+
const toString = Reflect.get(basesValue, "toString");
184+
if (typeof toString === "function" && toString !== Object.prototype.toString) {
185+
const stringValue = Reflect.apply(toString, basesValue, []);
186+
if (stringValue !== "[object Object]") {
187+
return stringValue;
188+
}
185189
}
186190

187191
return value;
@@ -193,11 +197,15 @@ export class BasesDataAdapter {
193197
* For FileValue (links), returns the file path which can be rendered as a clickable link.
194198
*/
195199
convertGroupKeyToString(key: unknown): string {
196-
const basesKey = key as BasesValueInternals | null | undefined;
197-
// Check if key exists and is valid
198-
if (basesKey == null || basesKey.constructor?.name === "NullValue") {
199-
return "Unknown";
200-
}
200+
// Check if key exists and is valid
201+
if (key === null || key === undefined) {
202+
return "Unknown";
203+
}
204+
205+
const basesKey = key as BasesValueInternals;
206+
if (basesKey.constructor?.name === "NullValue") {
207+
return "Unknown";
208+
}
201209

202210
// Extract the actual value from Bases Value object
203211
let actualValue: unknown;
@@ -291,14 +299,16 @@ export class BasesDataAdapter {
291299
* Call this during rendering for visible items only - NOT during bulk extraction.
292300
* This is much more efficient for expensive properties like backlinks.
293301
*/
294-
getComputedProperty(basesEntry: BasesEntry | null | undefined, propertyId: string): unknown {
295-
if (!basesEntry) return null;
296-
297-
try {
298-
const value = basesEntry.getValue(propertyId as BasesPropertyId);
299-
return this.convertValueToNative(value);
300-
} catch {
301-
return null;
302+
getComputedProperty(basesEntry: unknown, propertyId: string): unknown {
303+
if (!basesEntry || typeof basesEntry !== "object") return null;
304+
305+
try {
306+
const getValue = (basesEntry as { getValue?: (id: BasesPropertyId) => unknown }).getValue;
307+
if (typeof getValue !== "function") return null;
308+
const value = getValue.call(basesEntry, propertyId);
309+
return this.convertValueToNative(value);
310+
} catch {
311+
return null;
302312
}
303313
}
304314

src/bases/BasesViewBase.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Component, App, setIcon } from "obsidian";
2-
import type { BasesPropertyId, BasesQueryResult, BasesView, BasesViewConfig } from "obsidian";
3-
import type { EventRef } from "obsidian";
2+
import type {
3+
BasesPropertyId,
4+
BasesQueryResult,
5+
BasesView,
6+
BasesViewConfig,
7+
EventRef,
8+
} from "obsidian";
49
import TaskNotesPlugin from "../main";
510
import { BasesDataAdapter } from "./BasesDataAdapter";
611
import { PropertyMappingService } from "./PropertyMappingService";
@@ -54,7 +59,7 @@ export abstract class BasesViewBase extends Component implements BasesView {
5459
protected propertyMapper: PropertyMappingService;
5560
protected containerEl: HTMLElement;
5661
protected rootElement: HTMLElement | null = null;
57-
protected taskUpdateListener: EventRef | null = null;
62+
protected taskUpdateListener: unknown = null;
5863
protected updateDebounceTimer: number | null = null;
5964
protected dataUpdateDebounceTimer: number | null = null;
6065
protected relevantPathsCache: Set<string> = new Set();
@@ -350,7 +355,7 @@ export abstract class BasesViewBase extends Component implements BasesView {
350355
// Register cleanup using Component lifecycle
351356
this.register(() => {
352357
if (this.taskUpdateListener) {
353-
this.plugin.emitter.offref(this.taskUpdateListener);
358+
this.plugin.emitter.offref(this.taskUpdateListener as EventRef);
354359
this.taskUpdateListener = null;
355360
}
356361
});

src/bases/CalendarView.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BasesViewBase } from "./BasesViewBase";
33
import { TaskInfo } from "../types";
44
import { identifyTaskNotesFromBasesData } from "./helpers";
55
import {
6-
Calendar,
6+
Calendar as FullCalendar,
77
CalendarOptions,
88
type DateSelectArg,
99
type EventClickArg,
@@ -76,6 +76,22 @@ type CalendarEphemeralState = {
7676
calendarView?: unknown;
7777
};
7878

79+
const Calendar = FullCalendar;
80+
81+
type Calendar = {
82+
updateSize(): void;
83+
getDate(): Date;
84+
destroy(): void;
85+
render(): void;
86+
refetchEvents(): void;
87+
unselect(): void;
88+
changeView(viewType: string): void;
89+
gotoDate(date: Date): void;
90+
view?: {
91+
type?: string;
92+
};
93+
};
94+
7995
function isRecord(value: unknown): value is Record<string, unknown> {
8096
return typeof value === "object" && value !== null;
8197
}
@@ -1079,7 +1095,10 @@ export class CalendarView extends BasesViewBase {
10791095
}
10801096

10811097
private applyTodayColumnWidth(): void {
1082-
if (!this.calendarEl || !this.calendar) return;
1098+
const calendar = this.calendar;
1099+
if (!this.calendarEl || !calendar) return;
1100+
const viewType = calendar.view?.type;
1101+
if (!viewType) return;
10831102

10841103
const headerCells = Array.from(
10851104
this.calendarEl.querySelectorAll<HTMLElement>(".fc-col-header-cell[data-date]")
@@ -1089,12 +1108,12 @@ export class CalendarView extends BasesViewBase {
10891108
.filter((date): date is string => Boolean(date));
10901109
this.resetTodayColumnWidths(dateKeys);
10911110

1092-
if (
1093-
!shouldWidenTodayColumn(
1094-
this.calendar.view.type,
1095-
this.viewOptions.todayColumnWidthMultiplier
1096-
)
1097-
) {
1111+
if (
1112+
!shouldWidenTodayColumn(
1113+
viewType,
1114+
this.viewOptions.todayColumnWidthMultiplier
1115+
)
1116+
) {
10981117
return;
10991118
}
11001119

src/bases/KanbanView.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,19 +1915,24 @@ export class KanbanView extends BasesViewBase {
19151915
this.stopAutoScroll();
19161916
this.autoScrollDirection = newDirection;
19171917
if (newDirection !== 0) {
1918-
this.autoScrollTimer = window.setInterval(() => {
1918+
const scroll = () => {
19191919
if (this.boardEl) {
19201920
this.boardEl.scrollLeft +=
19211921
this.autoScrollDirection * this.AUTO_SCROLL_SPEED;
19221922
}
1923-
}, 16);
1923+
if (this.autoScrollDirection !== 0) {
1924+
this.autoScrollTimer = window.setTimeout(scroll, 16);
1925+
}
1926+
};
1927+
1928+
this.autoScrollTimer = window.setTimeout(scroll, 16);
19241929
}
19251930
}
19261931
}
19271932

19281933
private stopAutoScroll(): void {
19291934
if (this.autoScrollTimer) {
1930-
window.clearInterval(this.autoScrollTimer);
1935+
window.clearTimeout(this.autoScrollTimer);
19311936
this.autoScrollTimer = null;
19321937
}
19331938
this.autoScrollDirection = 0;

0 commit comments

Comments
 (0)