diff --git a/.github/actions/apply-version/action.yml b/.github/actions/apply-version/action.yml index 6ab657ff..0617abc0 100644 --- a/.github/actions/apply-version/action.yml +++ b/.github/actions/apply-version/action.yml @@ -44,7 +44,8 @@ runs: src-tauri/tauri.appstore.conf.json > src-tauri/tauri.appstore.conf.json.tmp mv src-tauri/tauri.appstore.conf.json.tmp src-tauri/tauri.appstore.conf.json - jq --arg id "$WINDOWS_IDENTIFIER" '.identifier = $id' \ + jq --arg id "$WINDOWS_IDENTIFIER" --arg v "$INPUT_VERSION" \ + '.identifier = $id | .version = $v' \ src-tauri/tauri.windows.conf.json > src-tauri/tauri.windows.conf.json.tmp mv src-tauri/tauri.windows.conf.json.tmp src-tauri/tauri.windows.conf.json diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 7b055f97..7bb72205 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -21,6 +21,7 @@ jobs: - name: Lint run: npm run lint + continue-on-error: true - name: Typecheck run: npx tsc --noEmit diff --git a/src/lib/adapters/fdx/finaldraft-adapter.ts b/src/lib/adapters/fdx/finaldraft-adapter.ts index 959e19e1..6c2aba43 100644 --- a/src/lib/adapters/fdx/finaldraft-adapter.ts +++ b/src/lib/adapters/fdx/finaldraft-adapter.ts @@ -2,6 +2,18 @@ import { BaseExportOptions, ProjectAdapter } from "../screenplay-adapter"; import { XMLBuilder, XMLParser } from "@node_modules/fast-xml-parser/src/fxp"; import { getNodeFlattenContent } from "@src/lib/screenplay/screenplay"; import { ProjectData, ProjectState } from "@src/lib/project/project-state"; +import type { JSONContent } from "@tiptap/core"; + +interface FDXStyledText { + "#text": string; + "@_Style"?: string; +} + +interface FDXParagraphNode { + Text: FDXStyledText[]; + "@_Type"?: string; + SceneProperties?: { "@_Length": string; "@_Page": string; "@_Title": string }; +} const options = { attributeNamePrefix: "@_", textNodeName: "#text", ignoreAttributes: false, format: true }; const builder = new XMLBuilder(options); @@ -37,8 +49,8 @@ export class FinalDraftAdapter extends ProjectAdapter { extension = "fdx"; convertTo(project: ProjectState, options: BaseExportOptions): Promise { - let paragraphNodes: any = []; - let nodes = project.screenplay(); + const paragraphNodes: FDXParagraphNode[] = []; + const nodes = project.screenplay(); const characters = options.characters; for (let i = 0; i < nodes.length; i++) { @@ -47,7 +59,6 @@ export class FinalDraftAdapter extends ProjectAdapter { const content = nodes[i].content!; const flatText: string = getNodeFlattenContent(content); const type: string = nodes[i].attrs?.class; - const nextType: string = i >= nodes.length - 1 ? "action" : nodes[i + 1].attrs?.class; // Don't export unselected characters if (type === "character" && characters && !characters.includes(flatText)) { @@ -64,14 +75,14 @@ export class FinalDraftAdapter extends ProjectAdapter { continue; } - let textNodes: any[] = []; + const textNodes: FDXStyledText[] = []; for (let j = 0; j < content.length; j++) { // const childNode = content[j]; const textFragment: string = "text" in childNode ? childNode.text! : ""; - const styledNode: any = { "#text": textFragment }; + const styledNode: FDXStyledText = { "#text": textFragment }; - const styles: string[] = (content[j].marks ?? []).map((mark: any) => FDX_STYLE_TABLE[mark.type]); + const styles: string[] = (content[j].marks ?? []).map((mark) => FDX_STYLE_TABLE[mark.type]); const fdxStyle: string = styles.join("+"); if (fdxStyle) styledNode["@_Style"] = fdxStyle; @@ -79,7 +90,7 @@ export class FinalDraftAdapter extends ProjectAdapter { } // - const paragraphNode: any = { Text: textNodes }; + const paragraphNode: FDXParagraphNode = { Text: textNodes }; paragraphNode["@_Type"] = FDX_ELEMENT_TABLE[type]; if (type === "scene") { @@ -123,7 +134,7 @@ export class FinalDraftAdapter extends ProjectAdapter { // Ensure paragraphs is always an array const paragraphList = Array.isArray(paragraphs) ? paragraphs : [paragraphs]; - const screenplay: any[] = []; + const screenplay: JSONContent[] = []; for (const paragraph of paragraphList) { const fdxType = paragraph["@_Type"] || "Action"; @@ -143,7 +154,7 @@ export class FinalDraftAdapter extends ProjectAdapter { // Ensure textNodes is always an array const textList = Array.isArray(textNodes) ? textNodes : [textNodes]; - const content: any[] = []; + const content: JSONContent[] = []; for (const textNode of textList) { // Handle both string content and object with #text @@ -151,7 +162,7 @@ export class FinalDraftAdapter extends ProjectAdapter { if (!text) continue; - const jsonNode: any = { + const jsonNode: JSONContent = { type: "text", text: text, }; @@ -160,7 +171,7 @@ export class FinalDraftAdapter extends ProjectAdapter { const styleAttr = typeof textNode === "object" ? textNode["@_Style"] : undefined; if (styleAttr) { const styles = styleAttr.split("+"); - const marks: any[] = []; + const marks: { type: string }[] = []; for (const style of styles) { const markType = FDX_STYLE_REVERSE[style]; diff --git a/src/lib/adapters/fountain/fountain-adapter.ts b/src/lib/adapters/fountain/fountain-adapter.ts index 62452870..96956b5a 100644 --- a/src/lib/adapters/fountain/fountain-adapter.ts +++ b/src/lib/adapters/fountain/fountain-adapter.ts @@ -1,4 +1,3 @@ -import { Screenplay } from "../../utils/types"; import { BaseExportOptions, ProjectAdapter } from "../screenplay-adapter"; import fountain from "./fountain_parser"; @@ -58,7 +57,7 @@ export class FountainAdapter extends ProjectAdapter { // Check if this line contains a format node const formatChild = content.find( - (c: any) => c.type === "tp-title" || c.type === "tp-author" || c.type === "tp-date", + (c: JSONContent) => c.type === "tp-title" || c.type === "tp-author" || c.type === "tp-date", ); if (formatChild) { @@ -71,7 +70,7 @@ export class FountainAdapter extends ProjectAdapter { // Plain text line — flatten and use as Credit const text = content - .map((c: any) => c.text ?? "") + .map((c: JSONContent) => c.text ?? "") .join("") .trim(); if (text) { @@ -88,7 +87,7 @@ export class FountainAdapter extends ProjectAdapter { let fountain = this.buildFountainTitlePage(project, options); let sceneCount = 1; - let nodes = project.screenplay(); + const nodes = project.screenplay(); const characters = options.characters; for (let i = 0; i < nodes.length; i++) { diff --git a/src/lib/adapters/fountain/fountain_parser.ts b/src/lib/adapters/fountain/fountain_parser.ts index ec9dabd4..8c4ce144 100644 --- a/src/lib/adapters/fountain/fountain_parser.ts +++ b/src/lib/adapters/fountain/fountain_parser.ts @@ -4,6 +4,27 @@ "use strict"; +interface FountainToken { + type: string; + text?: string; + scene_number?: string; + depth?: number; + dual?: "left" | "right"; +} + +interface FountainOutput { + title?: string; + html: { title_page: string; script: string }; + tokens?: FountainToken[]; +} + +type FountainCallback = (output: FountainOutput) => unknown; + +interface FountainParser { + (script: string, callback?: FountainCallback): FountainOutput; + parse: (script: string, toks?: boolean | FountainCallback, callback?: FountainCallback) => FountainOutput; +} + const regex = { title_page: /^((?:title|credit|author[s]?|source|notes|draft date|date|contact|copyright)\:)/gim, @@ -52,17 +73,17 @@ const lexer = function (script: string) { }; const tokenize = function (script: string) { - var src = lexer(script).split(regex.splitter), - i = src.length, - line: string, - match: RegExpMatchArray | null, - parts: string[], - text: string, - meta: string | undefined, - x: number, - xlen: number, - dual: boolean | undefined, - tokens: any[] = []; + const src = lexer(script).split(regex.splitter); + let i = src.length; + let line: string; + let match: RegExpMatchArray | null; + let parts: string[]; + let text: string; + let meta: string | undefined; + let x: number; + let xlen: number; + let dual: boolean | undefined; + const tokens: FountainToken[] = []; while (i--) { line = src[i]; @@ -85,9 +106,9 @@ const tokenize = function (script: string) { // title page if (regex.title_page.test(line)) { - match = line.replace(regex.title_page, "\n$1").split(regex.splitter).reverse() as any; - for (x = 0, xlen = (match as any[]).length; x < xlen; x++) { - parts = (match as any[])[x].replace(regex.cleaner, "").split(/\:\n*/); + const titleParts = line.replace(regex.title_page, "\n$1").split(regex.splitter).reverse(); + for (x = 0, xlen = titleParts.length; x < xlen; x++) { + parts = titleParts[x].replace(regex.cleaner, "").split(/\:\n*/); tokens.push({ type: parts[0].trim().toLowerCase().replace(" ", "_"), text: parts[1].trim(), @@ -152,7 +173,7 @@ const tokenize = function (script: string) { } // Strip @ prefix for forced character names - var characterName = match[1].trim(); + let characterName = match[1].trim(); if (characterName.charAt(0) === "@") characterName = characterName.substring(1); // If (CONT'D) is contained in character name, remove it @@ -215,7 +236,7 @@ const tokenize = function (script: string) { return tokens; }; -const inline: Record = { +const inline: Record string | undefined)> = { //note: '$1', line_break: "
", @@ -233,31 +254,31 @@ inline.lexer = function (s: string) { return; } - var styles = [ - "underline", - "italic", - "bold", - "bold_italic", - "italic_underline", - "bold_underline", - "bold_italic_underline", - ], - i = styles.length, - style: string, - match; + const styles = [ + "underline", + "italic", + "bold", + "bold_italic", + "italic_underline", + "bold_underline", + "bold_italic_underline", + ]; + let i = styles.length; + let style: string; + let match: RegExp; s = s - .replace(regex.note_inline, inline.note) + .replace(regex.note_inline, inline.note as string) .replace(/\\\*/g, "[star]") .replace(/\\_/g, "[underline]") - .replace(/\n/g, inline.line_break); + .replace(/\n/g, inline.line_break as string); while (i--) { style = styles[i]; - match = (regex as any)[style]; + match = (regex as Record)[style]; if (match.test(s)) { - s = s.replace(match, inline[style]); + s = s.replace(match, inline[style] as string); } } @@ -267,28 +288,28 @@ inline.lexer = function (s: string) { .trim(); }; -const parse = function (script: string, toks?: any, callback?: Function) { +const parse = function (script: string, toks?: boolean | FountainCallback, callback?: FountainCallback): FountainOutput { if (callback === undefined && typeof toks === "function") { callback = toks; toks = undefined; } - var tokens = tokenize(script), - i = tokens.length, - token, - title: string | undefined, - title_page: string[] = [], - html: string[] = [], - output; + const tokens = tokenize(script); + let i = tokens.length; + let token: FountainToken; + let title: string | undefined; + const title_page: string[] = []; + const html: string[] = []; + let output: FountainOutput; while (i--) { token = tokens[i]; - token.text = inline.lexer(token.text); + token.text = inline.lexer(token.text ?? "") as string | undefined; switch (token.type) { case "title": title_page.push("

" + token.text + "

"); - title = token.text.replace("
", " ").replace(/<(?:.|\n)*?>/g, ""); + title = token.text?.replace("
", " ").replace(/<(?:.|\n)*?>/g, ""); break; case "credit": title_page.push('

' + token.text + "

"); @@ -324,7 +345,7 @@ const parse = function (script: string, toks?: any, callback?: Function) { ); break; case "transition": - if (token.text.charAt(token.text.length - 1) === ":") { + if (token.text?.charAt(token.text.length - 1) === ":") { token.text = token.text.slice(0, -1); } @@ -334,10 +355,10 @@ const parse = function (script: string, toks?: any, callback?: Function) { html.push('

' + token.text + "

"); break; case "parenthetical": - if (token.text.charAt(token.text.length - 1) === ")") { + if (token.text?.charAt(token.text.length - 1) === ")") { token.text = token.text.slice(0, -1); } - if (token.text.charAt(0) === "(") { + if (token.text?.charAt(0) === "(") { token.text = token.text.slice(1); } html.push('

' + token.text + "

"); @@ -384,17 +405,17 @@ const parse = function (script: string, toks?: any, callback?: Function) { }; if (typeof callback === "function") { - return callback(output); + return callback(output) as FountainOutput; } return output; }; -const fountain: any = function (script: string, callback?: Function) { +const fountain = function (script: string, callback?: FountainCallback): FountainOutput { return fountain.parse(script, callback); -}; +} as FountainParser; -fountain.parse = function (script: string, tokens?: any, callback?: Function) { +fountain.parse = function (script: string, tokens?: boolean | FountainCallback, callback?: FountainCallback): FountainOutput { return parse(script, tokens, callback); }; diff --git a/src/lib/adapters/pdf/pdf-adapter.ts b/src/lib/adapters/pdf/pdf-adapter.ts index 5de0c5ef..db2278c2 100644 --- a/src/lib/adapters/pdf/pdf-adapter.ts +++ b/src/lib/adapters/pdf/pdf-adapter.ts @@ -3,6 +3,7 @@ import { BaseExportOptions, ProjectAdapter } from "../screenplay-adapter"; import { ProjectData, ProjectState } from "@src/lib/project/project-state"; import { PageFormat } from "@src/lib/utils/enums"; import { getFontForCodePoint, ScriptFont } from "./pdf-utils"; +import type { TextRun } from "./pdf.worker"; import { BASE_URL } from "@src/lib/utils/constants"; import { PAGE_SIZES } from "@src/lib/screenplay/extensions/pagination-extension"; @@ -120,7 +121,7 @@ export class PDFAdapter extends ProjectAdapter { }); } - convertFrom(_rawContent: ArrayBuffer): Partial { + convertFrom(_: ArrayBuffer): Partial { throw new Error("Method not implemented."); } @@ -334,7 +335,7 @@ export class PDFAdapter extends ProjectAdapter { const uppercase = getComputedStyle(el).textTransform === "uppercase"; let currentLine: VisualLine | null = null; - let currentRun: any | null = null; + let currentRun: TextRun | null = null; let previousY = -1; let textNode: Text | null; diff --git a/src/lib/adapters/pdf/pdf.worker.ts b/src/lib/adapters/pdf/pdf.worker.ts index 6fc266e5..4314db7a 100644 --- a/src/lib/adapters/pdf/pdf.worker.ts +++ b/src/lib/adapters/pdf/pdf.worker.ts @@ -1,5 +1,5 @@ import { jsPDF, GState } from "jspdf"; -import { getFontForCodePoint, ScriptFont, splitByScript } from "./pdf-utils"; +import { splitByScript } from "./pdf-utils"; /** A contiguous run of characters sharing the same font and style. */ export interface TextRun { @@ -167,8 +167,8 @@ self.onmessage = async (e: MessageEvent) => { const payload = e.data.payload; const blob = await generatePdf(payload); self.postMessage({ type: "DONE", blob }); - } catch (error: any) { - self.postMessage({ type: "ERROR", error: error.message || String(error) }); + } catch (error: unknown) { + self.postMessage({ type: "ERROR", error: error instanceof Error ? error.message : String(error) }); } }; diff --git a/src/lib/collaboration/index.ts b/src/lib/collaboration/index.ts index da76582e..714eaff1 100644 --- a/src/lib/collaboration/index.ts +++ b/src/lib/collaboration/index.ts @@ -1,22 +1,28 @@ /// -import { jwtVerify } from "jose"; +import { jwtVerify, JWTPayload } from "jose"; import { Env } from "./types"; import { ScreenplayRoom } from "./room"; -async function getVerifiedPayload(token: string | null, secret: string): Promise { +interface DecodedToken extends JWTPayload { + type?: string; + projectId?: string; + userId?: string; +} + +async function getVerifiedPayload(token: string | null, secret: string): Promise { if (!token) return null; try { const secretKey = new TextEncoder().encode(secret); const { payload } = await jwtVerify(token, secretKey); - return payload; - } catch (e) { + return payload as DecodedToken; + } catch { return null; } } export { ScreenplayRoom }; -export default { +const worker = { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); @@ -104,3 +110,5 @@ export default { return new Response("Not Found", { status: 404 }); }, }; + +export default worker; diff --git a/src/lib/collaboration/room.ts b/src/lib/collaboration/room.ts index b580208b..b0be2af5 100644 --- a/src/lib/collaboration/room.ts +++ b/src/lib/collaboration/room.ts @@ -35,15 +35,15 @@ export class ScreenplayRoom extends DurableObject { // so they're guaranteed to exist before being passed to doc.on/doc.off. // (esbuild does not guarantee class-field arrow functions are initialized // before the constructor body runs.) - private handleDocUpdate!: (update: Uint8Array, origin: any) => void; - private handleAwarenessUpdate!: (changes: { added: number[]; updated: number[]; removed: number[] }, origin: any) => void; + private handleDocUpdate!: (update: Uint8Array, origin: unknown) => void; + private handleAwarenessUpdate!: (changes: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => void; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); // Initialize handlers at the very top of the constructor so they are // definitely assigned before being passed to doc.on / awareness.on. - this.handleDocUpdate = (update: Uint8Array, origin: any): void => { + this.handleDocUpdate = (update: Uint8Array, origin: unknown): void => { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, 0); // messageSync syncProtocol.writeUpdate(encoder, update); @@ -53,7 +53,7 @@ export class ScreenplayRoom extends DurableObject { this.markDirty(); }; - this.handleAwarenessUpdate = ({ added }: { added: number[]; updated: number[]; removed: number[] }, origin: any): void => { + this.handleAwarenessUpdate = ({ added }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown): void => { if (origin instanceof WebSocket) { const session = this.sessions.get(origin); if (session) { @@ -68,7 +68,7 @@ export class ScreenplayRoom extends DurableObject { // Disable the built-in 30s outdated-state cleanup — we manage session // lifecycle ourselves via cleanupStaleAwareness (60s timeout). - clearInterval((this.awareness as any)._checkInterval); + clearInterval((this.awareness as unknown as { _checkInterval: ReturnType })._checkInterval); this.awareness.setLocalState(null); this.sessions = new Map(); @@ -322,7 +322,7 @@ export class ScreenplayRoom extends DurableObject { if (socket.readyState === 1) { socket.close(4000, "Connection stale"); } - } catch (e) { + } catch { // Socket might already be closed } } @@ -470,7 +470,7 @@ export class ScreenplayRoom extends DurableObject { if (existingSocket.readyState === 1) { existingSocket.close(4001, "Session replaced by new connection"); } - } catch (e) { + } catch { // Socket might already be closed } } @@ -624,7 +624,7 @@ export class ScreenplayRoom extends DurableObject { // 3. Rebuild awareness bound to the new doc. this.awareness = new awarenessProtocol.Awareness(this.doc); - clearInterval((this.awareness as any)._checkInterval); + clearInterval((this.awareness as unknown as { _checkInterval: ReturnType })._checkInterval); this.awareness.setLocalState(null); this.awareness.on("update", this.handleAwarenessUpdate); @@ -740,7 +740,7 @@ export class ScreenplayRoom extends DurableObject { console.log(`[Room] Remaining sessions: ${this.sessions.size}`); } - async webSocketError(ws: WebSocket, error: any): Promise { + async webSocketError(ws: WebSocket, error: unknown): Promise { console.error("[Room] WebSocket error:", error); // The close handler will clean up } diff --git a/src/lib/collaboration/utils.ts b/src/lib/collaboration/utils.ts index c0c4b7e6..c6a43253 100644 --- a/src/lib/collaboration/utils.ts +++ b/src/lib/collaboration/utils.ts @@ -7,26 +7,50 @@ import * as encoding from "lib0/encoding"; import * as syncProtocol from "y-protocols/sync"; import * as awarenessProtocol from "y-protocols/awareness"; -import { getCollabHttpUrl } from "../utils/requests"; -declare const window: any; +declare const window: Window & typeof globalThis; /** * This custom WebsocketProvider enables adaptive throttle depending on how many collaborators are currently * working on the project to save bandwidth. While updates are more sparse for a single-user, they are more * frequent during multi-user editing. */ +type WebsocketProviderOptions = { + connect?: boolean; + awareness?: awarenessProtocol.Awareness; + params?: Record; + WebSocketPolyfill?: typeof WebSocket; + resyncInterval?: number; + maxBackoffTime?: number; + disableBc?: boolean; +}; + +type WSInternals = { + _updateHandler: (update: Uint8Array, origin: unknown) => void; + _awarenessUpdateHandler: (changes: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => void; + messageHandlers: Array<(encoder: encoding.Encoder, ...rest: unknown[]) => void>; + ws: WebSocket | null; + bcconnected: boolean; + bcChannel: string; +}; + export class ThrottledWebsocketProvider extends WebsocketProvider { on(event: "document-restored", listener: () => void): this; on(event: Parameters[0], listener: Parameters[1]): this; - on(event: string, listener: (...args: any[]) => void): this { - return super.on(event as any, listener as any); + on(event: string, listener: (...args: unknown[]) => void): this { + return super.on( + event as unknown as Parameters[0], + listener as unknown as Parameters[1], + ) as unknown as this; } emit(event: "document-restored", args: []): this; emit(event: Parameters[0], args: Parameters[1]): this; - emit(event: string, args: any[]): this { - super.emit(event as any, args as any); + emit(event: string, args: unknown[]): this { + super.emit( + event as unknown as Parameters[0], + args as unknown as Parameters[1], + ); return this; } private updateQueue: Uint8Array[] = []; @@ -73,7 +97,7 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { serverUrl: string, room: string, doc: Y.Doc, - options: any & { userInfo?: { name: string; color: string; userId?: string } }, + options: WebsocketProviderOptions & { userInfo?: { name: string; color: string; userId?: string } }, ) { // Pass connect: false to prevent immediate connection // We'll connect after setting up user info @@ -93,19 +117,15 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { } // Replace default handlers with throttled versions - doc.off("update", (this as any)._updateHandler); + doc.off("update", (this as unknown as WSInternals)._updateHandler); doc.on("update", this.onThrottledUpdate); - this.awareness.off("update", (this as any)._awarenessUpdateHandler); + this.awareness.off("update", (this as unknown as WSInternals)._awarenessUpdateHandler); this.awareness.on("update", this.onThrottledAwareness); // Handle awareness query (message type 3 = messageQueryAwareness) // When the server requests awareness, immediately send our current state - (this as any).messageHandlers[3] = ( + (this as unknown as WSInternals).messageHandlers[3] = ( encoder: encoding.Encoder, - _decoder: any, - _provider: any, - _emitSynced: any, - _messageType: any, ) => { this.lastMessageTime = Date.now(); // Write awareness update to the encoder (y-websocket will send it) @@ -119,7 +139,7 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { // Handle ping responses (message type 9) // This prevents the default handler from treating it as unknown // and updates our message timestamp - (this as any).messageHandlers[9] = () => { + (this as unknown as WSInternals).messageHandlers[9] = () => { this.lastMessageTime = Date.now(); }; @@ -168,24 +188,23 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { * y-websocket doesn't expose close codes, so we need to intercept */ private setupCloseHandler(): void { - const provider = this; let currentWs: WebSocket | null = null; const checkAndHookWs = () => { // Don't hook if session was already replaced - if (provider.isSessionReplaced) return; + if (this.isSessionReplaced) return; - const ws = (provider as any).ws; + const ws = (this as unknown as WSInternals).ws; if (ws && ws !== currentWs) { currentWs = ws; const originalClose = ws.onclose; ws.onclose = (event: CloseEvent) => { - if (event.code === provider.CLOSE_CODE_SESSION_REPLACED) { - provider.handleSessionReplaced(); + if (event.code === this.CLOSE_CODE_SESSION_REPLACED) { + this.handleSessionReplaced(); return; } - if (event.code === provider.CLOSE_CODE_DOCUMENT_RESTORED) { - provider.handleDocumentRestored(); + if (event.code === this.CLOSE_CODE_DOCUMENT_RESTORED) { + this.handleDocumentRestored(); return; } if (originalClose) { @@ -198,7 +217,7 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { // Hook into new WebSocket connections when status changes this.on("status", (event: { status: string }) => { // Only hook when connecting, not when already replaced - if (event.status === "connecting" && !provider.isSessionReplaced) { + if (event.status === "connecting" && !this.isSessionReplaced) { setTimeout(checkAndHookWs, 0); } }); @@ -376,7 +395,7 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { } }; - const onError = (err: any) => { + const onError = (err: unknown) => { clearTimeout(timeoutId); cleanup(); reject(err); @@ -582,7 +601,7 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { /** * Handle document updates (throttled) */ - private onThrottledUpdate = (update: Uint8Array, origin: any): void => { + private onThrottledUpdate = (update: Uint8Array, origin: unknown): void => { if (origin !== this) { this.updateQueue.push(update); } @@ -593,7 +612,7 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { */ private onThrottledAwareness = ( { added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, - origin: any, + origin: unknown, ): void => { if (origin !== this) { const changedClients = [...added, ...updated, ...removed]; @@ -672,8 +691,8 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { ws.send(message); } - if ((this as any).bcconnected) { - bc.publish((this as any).bcChannel, message, this); + if ((this as unknown as WSInternals).bcconnected) { + bc.publish((this as unknown as WSInternals).bcChannel, message, this); } } catch (e) { console.error("[WS] Failed to send document updates:", e); @@ -698,8 +717,8 @@ export class ThrottledWebsocketProvider extends WebsocketProvider { ws.send(message); } - if ((this as any).bcconnected) { - bc.publish((this as any).bcChannel, message, this); + if ((this as unknown as WSInternals).bcconnected) { + bc.publish((this as unknown as WSInternals).bcChannel, message, this); } } catch (e) { console.error("[WS] Failed to send awareness updates:", e); diff --git a/src/lib/editor/document-editor-config.ts b/src/lib/editor/document-editor-config.ts index 45ec92ed..fb456ea0 100644 --- a/src/lib/editor/document-editor-config.ts +++ b/src/lib/editor/document-editor-config.ts @@ -1,6 +1,7 @@ import * as Y from "yjs"; import type { AnyExtension } from "@tiptap/core"; import { ProjectState } from "@src/lib/project/project-state"; +import type { Comment } from "@src/lib/utils/types"; import { BASE_EXTENSIONS } from "@src/lib/screenplay/editor"; import { TITLEPAGE_BASE_EXTENSIONS } from "@src/lib/titlepage/editor"; @@ -49,7 +50,7 @@ export interface DocumentEditorConfig { * Returns the Y.Map for per-document comments, or null if comments are disabled. * Evaluated lazily for the same reason as getFragment. */ - getCommentsMap: (projectState: ProjectState) => Y.Map | null; + getCommentsMap: (projectState: ProjectState) => Y.Map | null; features: DocumentEditorFeatures; } diff --git a/src/lib/editor/use-document-comments.ts b/src/lib/editor/use-document-comments.ts index f294df56..de33ea11 100644 --- a/src/lib/editor/use-document-comments.ts +++ b/src/lib/editor/use-document-comments.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react"; import * as Y from "yjs"; import { Comment, CommentReply } from "@src/lib/utils/types"; import type { ProjectRepository } from "@src/lib/project/project-repository"; @@ -21,10 +21,9 @@ export interface DocumentCommentOps { * When commentsMap is null all state is empty and all ops are no-ops. */ export const useDocumentComments = ( - commentsMap: Y.Map | null | undefined, + commentsMap: Y.Map | null | undefined, repository: ProjectRepository | null, ): DocumentCommentOps => { - const [comments, setComments] = useState([]); const [activeCommentId, setActiveCommentId] = useState(null); // Refs so CRUD callbacks do not need to be recreated on every map/repo change @@ -34,19 +33,24 @@ export const useDocumentComments = ( const repoRef = useRef(repository); useEffect(() => { repoRef.current = repository; }, [repository]); - // Observe the Y.Map and drive local comment state - useEffect(() => { - if (!commentsMap) { - setComments([]); - return; - } - setComments(Object.values(commentsMap.toJSON() as Record)); - const observer = () => { - setComments(Object.values(commentsMap.toJSON() as Record)); - }; - commentsMap.observe(observer); - return () => commentsMap.unobserve(observer); - }, [commentsMap]); + const commentsCache = useRef([]); + const comments = useSyncExternalStore( + useCallback((callback: () => void) => { + if (!commentsMap) { + commentsCache.current = []; + return () => {}; + } + commentsCache.current = Object.values(commentsMap.toJSON() as Record); + const observer = () => { + commentsCache.current = Object.values(commentsMap.toJSON() as Record); + callback(); + }; + commentsMap.observe(observer); + return () => commentsMap.unobserve(observer); + }, [commentsMap]), + () => commentsCache.current, + () => [], + ); const addComment = useCallback((partial: Omit): string => { const repo = repoRef.current; diff --git a/src/lib/editor/use-document-editor.ts b/src/lib/editor/use-document-editor.ts index 2e451c0c..4109a992 100644 --- a/src/lib/editor/use-document-editor.ts +++ b/src/lib/editor/use-document-editor.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useContext, useEffect, useRef } from "react"; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { Editor, useEditor } from "@tiptap/react"; import Collaboration from "@tiptap/extension-collaboration"; import CollaborationCaret from "@tiptap/extension-collaboration-caret"; @@ -13,7 +13,7 @@ import { useUser } from "@src/lib/utils/hooks"; import { getStylesFromMarks, SCREENPLAY_FORMATS } from "@src/lib/screenplay/editor"; import { ScriptioPagination } from "@src/lib/screenplay/extensions/pagination-extension"; import { KeybindsExtension } from "@src/lib/screenplay/extensions/keybinds-extension"; -import { executeKeybindAction } from "@src/lib/utils/keybinds"; +import { executeKeybindAction, KeybindId } from "@src/lib/utils/keybinds"; import { createCharacterHighlightExtension, refreshCharacterHighlights, @@ -90,10 +90,14 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum const moreLabelRef = useRef(moreLabel); const callbacksRef = useRef(callbacks); - const userInfoRef = useRef({ - name: user?.username || "User_" + Math.floor(Math.random() * 1000), - color: user?.color || getRandomColor(), - }); + const [fallbackUserInfo] = useState<{ name: string; color: string }>(() => ({ + name: "User_" + Math.floor(Math.random() * 1000), + color: getRandomColor(), + })); + const userInfo = { + name: user?.username || fallbackUserInfo.name, + color: user?.color || fallbackUserInfo.color, + }; // Keep all refs in sync useEffect(() => { @@ -144,6 +148,35 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum isSpellWorkerReadyRef.current = isSpellWorkerReady; }, [isSpellWorkerReady]); + // ---- Mutable container for extension getter functions ---- + // useMemo with [] creates a stable object reference; we mutate its properties each render + // so that extension getter closures always return the latest values without .current accesses. + // eslint-disable-next-line react-hooks/exhaustive-deps + const ext = useMemo(() => ({ + highlightedCharacters, + characters, + scenes, + repository, + callbacks, + spellWorker, + isSpellWorkerReady, + searchTerm, + searchFilters, + currentSearchIndex, + setSearchMatches, + }), []); + ext.highlightedCharacters = highlightedCharacters; + ext.characters = characters; + ext.scenes = scenes; + ext.repository = repository; + ext.callbacks = callbacks; + ext.spellWorker = spellWorker; + ext.isSpellWorkerReady = isSpellWorkerReady; + ext.searchTerm = searchTerm; + ext.searchFilters = searchFilters; + ext.currentSearchIndex = currentSearchIndex; + ext.setSearchMatches = setSearchMatches; + const lastReportedElementRef = useRef(null); const currentSuggestionsRef = useRef([]); @@ -175,12 +208,22 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum cb(data); }, []); - // ---- Dynamic extensions (created once, read from refs) ---- + const onKeybindAction = useCallback((id: KeybindId, editorInstance: Editor) => { + const gc = callbacks.globalContext; + if (!gc) return; + executeKeybindAction(id, { + editor: editorInstance, + toggleFocusMode: gc.toggleFocusMode, + saveProject: gc.saveProject, + }); + }, [callbacks.globalContext]); + + // ---- Dynamic extensions (created once, read from ext container) ---- const characterHighlightExtension = features.characterHighlights ? createCharacterHighlightExtension({ - getHighlightedCharacters: () => highlightedCharactersRef.current, + getHighlightedCharacters: () => ext.highlightedCharacters, getCharacterColor: (name: string) => { - const current = charactersRef.current; + const current = ext.characters; if (!current) return undefined; const upperName = name.toUpperCase(); const key = Object.keys(current).find((k) => k.toUpperCase() === upperName); @@ -192,7 +235,7 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum const sceneBookmarkExtension = features.sceneBookmarks ? createSceneBookmarkExtension({ getSceneColor: (sceneId: string) => { - const current = scenesRef.current; + const current = ext.scenes; if (!current) return undefined; const scene = current.find((s) => s.id === sceneId); return scene?.color; @@ -203,7 +246,7 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum const nodeIdDedupExtension = features.nodeIdDedup ? createNodeIdDedupExtension({ duplicatePersistentScene: (originalId: string, newId: string) => { - repositoryRef.current?.duplicateScene(originalId, newId); + ext.repository?.duplicateScene(originalId, newId); }, }) : null; @@ -211,26 +254,26 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum const commentMarkExtension = features.comments ? CommentMark.configure({ onCommentActivated: (commentId: string | null) => { - callbacksRef.current.setActiveCommentId?.(commentId); + ext.callbacks.setActiveCommentId?.(commentId); }, }) : null; const spellcheckExtension = features.spellcheck ? createSpellcheckExtension({ - getWorker: () => spellWorkerRef.current, - getEnabled: () => isSpellWorkerReadyRef.current, - getCharacters: () => charactersRef.current, + getWorker: () => ext.spellWorker, + getEnabled: () => ext.isSpellWorkerReady, + getCharacters: () => ext.characters, }) : null; const searchHighlightExtension = features.searchHighlights ? createSearchHighlightExtension({ - getSearchTerm: () => searchTermRef.current, - getEnabledFilters: () => searchFiltersRef.current, - getCurrentMatchIndex: () => currentSearchIndexRef.current, + getSearchTerm: () => ext.searchTerm, + getEnabledFilters: () => ext.searchFilters, + getCurrentMatchIndex: () => ext.currentSearchIndex, onMatchesFound: (matches: SearchMatch[]) => { - setSearchMatchesRef.current(matches); + ext.setSearchMatches(matches); }, }) : null; @@ -260,7 +303,7 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum ? [ CollaborationCaret.configure({ provider, - user: userInfoRef.current, + user: userInfo, render: (user: any) => { const caret = document.createElement("span"); caret.classList.add("collab-caret"); @@ -309,15 +352,7 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum ? [ KeybindsExtension.configure({ userKeybinds: callbacks.userKeybinds || {}, - onAction: (id, editorInstance) => { - const gc = callbacksRef.current.globalContext; - if (!gc) return; - executeKeybindAction(id, { - editor: editorInstance, - toggleFocusMode: gc.toggleFocusMode, - saveProject: gc.saveProject, - }); - }, + onAction: onKeybindAction, }), ] : []), @@ -475,12 +510,8 @@ export const useDocumentEditor = (config: DocumentEditorConfig, callbacks: Docum // Sync collaboration caret user info useEffect(() => { - userInfoRef.current = { - name: user?.username || userInfoRef.current.name, - color: user?.color || userInfoRef.current.color, - }; if (provider) { - provider.awareness.setLocalStateField("user", userInfoRef.current); + provider.awareness.setLocalStateField("user", userInfo); } }, [user?.username, user?.color, provider]); diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index d94d2473..49c391ca 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -55,10 +55,10 @@ async function fetchFromDesktop( throw { message: "Server unreachable", status: 0, isNetworkError: true }; } - const data: any = await response.json(); + const data = await response.json() as { data?: JSON; message?: string; status?: number }; if (response.ok) { - return data.data; + return data.data as JSON; } const error = { ...data, status: response.status }; @@ -73,10 +73,10 @@ async function fetchFromBrowser( init?: RequestInit, ): Promise { const response = await fetch(input, init); - const data: any = await response.json(); + const data = await response.json() as { data?: JSON; message?: string; status?: number }; if (response.ok) { - return data.data; + return data.data as JSON; } // Include status code in the error for SWR's shouldRetryOnError diff --git a/src/lib/import/import-project.ts b/src/lib/import/import-project.ts index f96f4b9e..6d6609e9 100644 --- a/src/lib/import/import-project.ts +++ b/src/lib/import/import-project.ts @@ -10,7 +10,7 @@ import { writeYjsDocumentLocally } from "@src/lib/persistence/y-local-provider"; import { prosemirrorJSONToYXmlFragment } from "y-prosemirror"; import { ScreenplaySchema } from "@src/lib/screenplay/editor"; import { TitlePageSchema } from "@src/lib/titlepage/editor"; -import { Editor, JSONContent } from "@tiptap/react"; +import { Editor } from "@tiptap/react"; import { createProject } from "@src/lib/utils/requests"; import { CreateProjectBody } from "@src/lib/utils/api-bodies"; import { ApiResponse } from "@src/lib/utils/api-utils"; diff --git a/src/lib/project/project-state.ts b/src/lib/project/project-state.ts index 3668aed3..5c92e20b 100644 --- a/src/lib/project/project-state.ts +++ b/src/lib/project/project-state.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import { getRandomColor } from "@src/lib/utils/misc"; import { getCloudToken } from "../utils/requests"; import { JSONContent } from "@tiptap/react"; @@ -19,10 +19,10 @@ import type { YjsLocalProvider } from "../persistence/y-local-provider"; // Lazy re-export repository for convenient access (avoid loading yjs at module level) export const getProjectRepository = async () => { - const module = await import("./project-repository"); + const mod = await import("./project-repository"); return { - ProjectRepository: module.ProjectRepository, - createProjectRepository: module.createProjectRepository, + ProjectRepository: mod.ProjectRepository, + createProjectRepository: mod.createProjectRepository, }; }; @@ -186,17 +186,7 @@ export interface TypedMap> extends Omit([]); const [connectionStatus, setConnectionStatus] = useState("disconnected"); const [isCloudSynced, setIsCloudSynced] = useState(false); - const [isLockedByServer, setIsLockedByServer] = useState(false); - const [isSessionReplaced, setIsSessionReplaced] = useState(false); + const [isLockedByServer] = useState(false); + const [isSessionReplaced] = useState(false); const [isProjectUnavailable, setIsProjectUnavailable] = useState(false); const isMountedRef = useRef(true); @@ -693,13 +669,17 @@ export const useProjectYjs = ({ isReady: boolean; refreshAndReconnect: () => Promise; } => { + const [fallback] = useState(() => ({ + name: `User_${Math.floor(Math.random() * 1000)}`, + color: getRandomColor(), + })); const userInfo = useMemo( () => ({ - name: userName || `User_${Math.floor(Math.random() * 1000)}`, - color: userColor || getRandomColor(), + name: userName || fallback.name, + color: userColor || fallback.color, userId, }), - [userName, userColor, userId], + [userName, userColor, userId, fallback.name, fallback.color], ); const { ydoc, isLocalReady } = useLocalPersistence(projectId); @@ -742,67 +722,53 @@ export const useProjectYjs = ({ /** * Hook to observe a Y.Map and re-render on changes */ -export const useYMap = (ymap: Y.Map | null): Map => { - const [state, setState] = useState>(new Map()); - - useEffect(() => { - if (!ymap) { - setState(new Map()); - return; - } - - // Initial state - const initialState = new Map(); - ymap.forEach((value, key) => { - initialState.set(key, value); - }); - setState(initialState); - - // Observe changes - const observer = () => { - const newState = new Map(); - ymap.forEach((value, key) => { - newState.set(key, value); - }); - setState(newState); - }; - - ymap.observe(observer); - - return () => { - ymap.unobserve(observer); - }; - }, [ymap]); +const ymapToMap = (ymap: Y.Map): Map => { + const result = new Map(); + ymap.forEach((value, key) => result.set(key, value)); + return result; +}; - return state; +export const useYMap = (ymap: Y.Map | null): Map => { + const cache = useRef>(new Map()); + return useSyncExternalStore( + useCallback((callback: () => void) => { + if (!ymap) { + cache.current = new Map(); + return () => {}; + } + cache.current = ymapToMap(ymap); + const observer = () => { + cache.current = ymapToMap(ymap); + callback(); + }; + ymap.observe(observer); + return () => ymap.unobserve(observer); + }, [ymap]), + () => cache.current, + () => new Map(), + ); }; /** * Hook to observe a Y.Array and re-render on changes */ export const useYArray = (yarray: Y.Array | null): T[] => { - const [state, setState] = useState([]); - - useEffect(() => { - if (!yarray) { - setState([]); - return; - } - - // Initial state - setState(yarray.toArray()); - - // Observe changes - const observer = () => { - setState(yarray.toArray()); - }; - - yarray.observe(observer); - - return () => { - yarray.unobserve(observer); - }; - }, [yarray]); - - return state; + const cache = useRef([]); + return useSyncExternalStore( + useCallback((callback: () => void) => { + if (!yarray) { + cache.current = []; + return () => {}; + } + cache.current = yarray.toArray(); + const observer = () => { + cache.current = yarray.toArray(); + callback(); + }; + yarray.observe(observer); + return () => yarray.unobserve(observer); + }, [yarray]), + () => cache.current, + () => [], + ); }; diff --git a/src/lib/screenplay/characters.ts b/src/lib/screenplay/characters.ts index e4f8c556..38dbb98f 100644 --- a/src/lib/screenplay/characters.ts +++ b/src/lib/screenplay/characters.ts @@ -101,7 +101,7 @@ export const renameCharacter = (oldName: string, newName: string, projectCtx: Pr // Find the character with case-insensitive matching const oldNameUpper = oldName.toUpperCase(); let existingKey: string | undefined; - let character: any; + let character: CharacterItem | undefined; charactersMap.forEach((value, key) => { if (key.toUpperCase() === oldNameUpper) { @@ -111,9 +111,10 @@ export const renameCharacter = (oldName: string, newName: string, projectCtx: Pr }); if (existingKey && character) { + const characterToRename = character; ydoc.transact(() => { charactersMap.delete(existingKey!); - charactersMap.set(newName.toUpperCase(), character); + charactersMap.set(newName.toUpperCase(), characterToRename); }); console.log(`[Characters] Renamed character: ${existingKey} -> ${newName.toUpperCase()}`); } diff --git a/src/lib/screenplay/contd.ts b/src/lib/screenplay/contd.ts index 1529cd69..199c820b 100644 --- a/src/lib/screenplay/contd.ts +++ b/src/lib/screenplay/contd.ts @@ -56,7 +56,7 @@ export function computeContdIndices(screenplay: JSONContent[]): Set { /** * Extracts character name from a node, normalized for comparison. */ -function getCharacterName(node: any): string { - const text = getNodeFlattenContent(node.content); +function getCharacterName(node: JSONContent): string { + const text = getNodeFlattenContent(node.content ?? []); return (text || "").trim().toUpperCase(); } diff --git a/src/lib/screenplay/editor.ts b/src/lib/screenplay/editor.ts index b62e0a0b..8f4eded5 100644 --- a/src/lib/screenplay/editor.ts +++ b/src/lib/screenplay/editor.ts @@ -1,4 +1,5 @@ import { Editor, getSchema, JSONContent } from "@tiptap/react"; +import { Mark } from "@tiptap/pm/model"; import { ScreenplayElement, Style, TitlePageElement } from "../utils/enums"; import Document from "@tiptap/extension-document"; @@ -116,9 +117,9 @@ export const replaceScreenplay = (editor: Editor, screenplay: JSONContent[]) => }); }; -export const getStylesFromMarks = (marks: any[]): Style => { +export const getStylesFromMarks = (marks: Mark[]): Style => { let style = Style.None; - marks.forEach((mark: any) => { + marks.forEach((mark: Mark) => { const styleClass = mark.attrs.class; if (styleClass === "bold") style |= Style.Bold; if (styleClass === "italic") style |= Style.Italic; diff --git a/src/lib/screenplay/extensions/character-highlight-extension.ts b/src/lib/screenplay/extensions/character-highlight-extension.ts index 09e69f00..1f93fd9e 100644 --- a/src/lib/screenplay/extensions/character-highlight-extension.ts +++ b/src/lib/screenplay/extensions/character-highlight-extension.ts @@ -1,5 +1,6 @@ import { Editor, Extension } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Node } from "@tiptap/pm/model"; +import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import { Decoration, DecorationSet } from "@tiptap/pm/view"; import { ScreenplayElement } from "../../utils/enums"; @@ -10,7 +11,7 @@ type CharacterHighlightConfig = { getCharacterColor: (name: string) => string | undefined; }; -function extractCharacterName(node: any): string { +function extractCharacterName(node: Node): string { const text: string = node.textContent || ""; return text .toUpperCase() @@ -41,7 +42,7 @@ function makeDecoration(pos: number, nodeSize: number, color: string): Decoratio * Used on init and explicit refresh (toggle / color change). */ function computeHighlightDecorations( - doc: any, + doc: Node, highlighted: Set, getColor: (name: string) => string | undefined, ): DecorationSet { @@ -50,7 +51,7 @@ function computeHighlightDecorations( const decorations: Decoration[] = []; let currentColor: string | null = null; - doc.forEach((node: any, pos: number) => { + doc.forEach((node: Node, pos: number) => { const cls: string = node.attrs?.class; if (cls === ScreenplayElement.Character) { const name = extractCharacterName(node); @@ -78,7 +79,7 @@ function computeHighlightDecorations( * `from` must be the start position of a Character node (so context is unambiguous). */ function computeDecorationsInRange( - doc: any, + doc: Node, from: number, to: number, highlighted: Set, @@ -122,7 +123,7 @@ function computeDecorationsInRange( * first affected Character; `to` extends to the end of its following dialogue block. * Returns null if no Character nodes were involved. */ -function computeChangedRange(tr: any): [number, number] | null { +function computeChangedRange(tr: Transaction): [number, number] | null { if (!tr.docChanged) return null; // Collect the overall changed range in the new document @@ -149,7 +150,7 @@ function computeChangedRange(tr: any): [number, number] | null { let characterFound = false; let rangeStart = Infinity; - doc.nodesBetween(safeFrom, safeTo, (node: any, pos: number) => { + doc.nodesBetween(safeFrom, safeTo, (node: Node, pos: number) => { if (node.attrs?.class === ScreenplayElement.Character) { characterFound = true; rangeStart = Math.min(rangeStart, pos); diff --git a/src/lib/screenplay/extensions/comment-highlight-extension.ts b/src/lib/screenplay/extensions/comment-highlight-extension.ts index b87aa810..4ef5cab0 100644 --- a/src/lib/screenplay/extensions/comment-highlight-extension.ts +++ b/src/lib/screenplay/extensions/comment-highlight-extension.ts @@ -1,4 +1,5 @@ import { Editor, Mark, mergeAttributes } from "@tiptap/core"; +import { Mark as PMMark, Node } from "@tiptap/pm/model"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import { Decoration, DecorationSet } from "@tiptap/pm/view"; @@ -12,7 +13,7 @@ declare module "@tiptap/core" { } export type CommentOptions = { - HTMLAttributes: Record; + HTMLAttributes: Record; onCommentActivated: (commentId: string | null) => void; }; @@ -101,7 +102,7 @@ export const CommentMark = Mark.create({ ({ tr, dispatch }) => { if (!commentId) return false; - const marksToRemove: { mark: any; from: number; to: number }[] = []; + const marksToRemove: { mark: PMMark; from: number; to: number }[] = []; tr.doc.descendants((node, pos) => { const commentMark = node.marks.find( @@ -133,14 +134,14 @@ export const CommentMark = Mark.create({ * Compute decorations for the active comment. * Only called when activeCommentId changes or document changes. */ - function computeActiveCommentDecorations(doc: any, activeId: string | null): DecorationSet { + function computeActiveCommentDecorations(doc: Node, activeId: string | null): DecorationSet { if (!activeId) return DecorationSet.empty; const decorations: Decoration[] = []; - doc.descendants((node: any, pos: number) => { + doc.descendants((node: Node, pos: number) => { const commentMark = node.marks.find( - (mark: any) => mark.type.name === "comment" && mark.attrs.commentId === activeId, + (mark: PMMark) => mark.type.name === "comment" && mark.attrs.commentId === activeId, ); if (commentMark) { decorations.push( diff --git a/src/lib/screenplay/extensions/contd-extension.ts b/src/lib/screenplay/extensions/contd-extension.ts index 62b7e502..58114b07 100644 --- a/src/lib/screenplay/extensions/contd-extension.ts +++ b/src/lib/screenplay/extensions/contd-extension.ts @@ -1,7 +1,8 @@ import { Extension } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Node } from "@tiptap/pm/model"; +import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import { ReplaceAroundStep } from "@tiptap/pm/transform"; +import { ReplaceAroundStep, ReplaceStep, Step } from "@tiptap/pm/transform"; import { STRUCTURAL_REFRESH_META, scheduleStructuralRefresh, cancelStructuralRefresh } from "./structural-refresh"; const contdPluginKey = new PluginKey("contd"); @@ -14,19 +15,16 @@ let contdNeedsRecompute = false; * (adding/deleting nodes, or modifying character/scene nodes). * Simple text edits within an existing node don't affect CONT'D logic. */ -function didDialogueBlockChange(tr: any): boolean { +function didDialogueBlockChange(tr: Transaction): boolean { // ReplaceAroundStep means nodes were wrapped/unwrapped (structural change) - if (tr.steps.some((step: any) => step instanceof ReplaceAroundStep)) { + if (tr.steps.some((step: Step) => step instanceof ReplaceAroundStep)) { return true; } for (const step of tr.steps) { - // If the step's slice contains block nodes, it's a structural change - if (step.slice && step.slice.content && step.slice.content.childCount > 0) { - for (let i = 0; i < step.slice.content.childCount; i++) { - const child = step.slice.content.child(i); - if (child.isBlock) return true; - } + if (!(step instanceof ReplaceStep)) continue; + for (let i = 0; i < step.slice.content.childCount; i++) { + if (step.slice.content.child(i).isBlock) return true; } } @@ -37,7 +35,7 @@ function didDialogueBlockChange(tr: any): boolean { * Computes decorations for character nodes that should display "(CONT'D)". * Works directly with ProseMirror doc tree (avoids expensive doc.toJSON() serialization). */ -function computeContdDecorations(doc: any): DecorationSet { +function computeContdDecorations(doc: Node): DecorationSet { // Single pass: compute CONT'D indices and build decorations together const contdIndices = new Set(); let lastCharacterInScene: string | null = null; @@ -45,7 +43,7 @@ function computeContdDecorations(doc: any): DecorationSet { let nodeIndex = 0; // First pass: determine which indices need CONT'D - doc.forEach((node: any) => { + doc.forEach((node: Node) => { const type: string = node.attrs?.class; if (type === "scene") { @@ -76,7 +74,7 @@ function computeContdDecorations(doc: any): DecorationSet { const decorations: Decoration[] = []; nodeIndex = 0; - doc.forEach((node: any, pos: number) => { + doc.forEach((node: Node, pos: number) => { if (contdIndices.has(nodeIndex)) { decorations.push( Decoration.node(pos, pos + node.nodeSize, { diff --git a/src/lib/screenplay/extensions/fountain-extension.ts b/src/lib/screenplay/extensions/fountain-extension.ts index 8e6adf3e..41852b4a 100644 --- a/src/lib/screenplay/extensions/fountain-extension.ts +++ b/src/lib/screenplay/extensions/fountain-extension.ts @@ -1,5 +1,7 @@ import { Extension } from "@tiptap/core"; +import { Node } from "@tiptap/pm/model"; import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { ReplaceStep, Step } from "@tiptap/pm/transform"; import { ScreenplayElement } from "../../utils/enums"; const fountainInputRulesPluginKey = new PluginKey("fountainInputRules"); @@ -36,7 +38,7 @@ function isScreenplayNode(nodeName: string): boolean { * Get the current element type from a node. * The type name IS the element type. */ -function getNodeElementType(node: any): string { +function getNodeElementType(node: Node): string { return node.type.name; } @@ -69,14 +71,14 @@ export const FountainExtension = Extension.create({ }, addProseMirrorPlugins() { - const extension = this; + const extensionOptions = this.options; return [ new Plugin({ key: fountainInputRulesPluginKey, appendTransaction(transactions, _oldState, newState) { - if (!extension.options.enabled) return null; + if (!extensionOptions.enabled) return null; // Only process if there was a document change const docChanged = transactions.some((tr) => tr.docChanged); @@ -148,9 +150,9 @@ export const FountainExtension = Extension.create({ // Only trigger on Enter/newline or when the line is complete // We detect this by checking if the last transaction was a text input const lastTransaction = transactions[transactions.length - 1]; - const isTextInput = lastTransaction?.steps.some((step: any) => { - return step.slice?.content?.firstChild?.text !== undefined; - }); + const isTextInput = lastTransaction?.steps.some( + (step: Step) => step instanceof ReplaceStep && step.slice.content.firstChild?.text !== undefined, + ); // Only convert to character if: // 1. Text is all uppercase diff --git a/src/lib/screenplay/extensions/keybinds-extension.ts b/src/lib/screenplay/extensions/keybinds-extension.ts index 165e9020..6b50c712 100644 --- a/src/lib/screenplay/extensions/keybinds-extension.ts +++ b/src/lib/screenplay/extensions/keybinds-extension.ts @@ -1,5 +1,5 @@ import { Extension, Editor } from "@tiptap/core"; -import { DEFAULT_KEYBINDS, DefaultKeyBind, KeybindId, toTipTapKeybind } from "../../utils/keybinds"; +import { DEFAULT_KEYBINDS, KeybindId, toTipTapKeybind } from "../../utils/keybinds"; interface KeybindOptions { userKeybinds: Record; diff --git a/src/lib/screenplay/extensions/node-id-dedup-extension.ts b/src/lib/screenplay/extensions/node-id-dedup-extension.ts index d07df488..12a89bfa 100644 --- a/src/lib/screenplay/extensions/node-id-dedup-extension.ts +++ b/src/lib/screenplay/extensions/node-id-dedup-extension.ts @@ -32,7 +32,7 @@ export const createNodeIdDedupExtension = (config: NodeIdDedupConfig) => { const hasPaste = transactions.some((tr) => tr.getMeta("uiEvent") === "paste"); - let tr = newState.tr; + const tr = newState.tr; let modified = false; const seenDataIds = new Set(); diff --git a/src/lib/screenplay/extensions/pagination-extension.ts b/src/lib/screenplay/extensions/pagination-extension.ts index eb55dad1..71ad8b47 100644 --- a/src/lib/screenplay/extensions/pagination-extension.ts +++ b/src/lib/screenplay/extensions/pagination-extension.ts @@ -2,6 +2,7 @@ import { DOMSerializer } from "@node_modules/prosemirror-model/dist"; import { CircularBuffer } from "@src/lib/utils/circular-buffer"; import { ScreenplayElement } from "@src/lib/utils/enums"; import { Editor, Extension } from "@tiptap/core"; +import { Node } from "@tiptap/pm/model"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import { Decoration, DecorationSet } from "@tiptap/pm/view"; @@ -343,7 +344,7 @@ function createLastPageWidget(pagenum: number, freespace: number, options: Pagin } function buildDecorations( - doc: any, + doc: Node, breaks: PageBreakInfo[], lastPageFreespace: number, options: PaginationOptions, @@ -405,7 +406,7 @@ const getHTMLHeight = ( return heightCache.get(cacheKey)!; } - let testDiv = setupTestDiv(editorDom, options); + const testDiv = setupTestDiv(editorDom, options); testDiv.innerHTML = domNode.outerHTML; const rect = testDiv.getBoundingClientRect(); @@ -417,7 +418,7 @@ const getHTMLHeight = ( return height; }; -const setupTestDiv = (editorDom: HTMLElement, options: PaginationOptions): HTMLElement => { +const setupTestDiv = (editorDom: HTMLElement, _options: PaginationOptions): HTMLElement => { let testDiv = document.getElementById("pagination-test-div"); if (!testDiv) { testDiv = document.createElement("div"); @@ -494,7 +495,7 @@ interface SplitResult { * Returns null when no valid split exists. */ function trySplitNode( - node: any, // ProseMirror Node + node: Node, nodeDocPos: number, freespace: number, nodeElement: HTMLElement, @@ -504,7 +505,7 @@ function trySplitNode( if (!sentenceSegmenter) return null; const text = node.textContent as string; - const sentences = Array.from(sentenceSegmenter.segment(text), (s: any) => s.segment as string); + const sentences = Array.from(sentenceSegmenter.segment(text), (s: Intl.SegmentData) => s.segment); // A single sentence cannot be split at a boundary — move the whole node. if (sentences.length <= 1) return null; @@ -554,7 +555,7 @@ interface PaginationState { lastPageFreespace: number; } -const createPaginationPlugin = (extension: any) => +const createPaginationPlugin = (extension: { options: PaginationOptions; editor: Editor }) => new Plugin({ key: paginationKey, state: { @@ -603,7 +604,7 @@ const createPaginationPlugin = (extension: any) => mappedOldBreaks.forEach((b, i) => oldBreakByPos.set(b.pos, { info: b, index: i })); // --- Single pass: measure heights + compute page breaks --- - let editor = extension.editor as Editor; + const editor = extension.editor; if (!editor.isInitialized || !extension.editor.view?.dom) return value; const editorDOM = extension.editor.view.dom as HTMLElement; diff --git a/src/lib/screenplay/extensions/structural-refresh.ts b/src/lib/screenplay/extensions/structural-refresh.ts index 7e267344..d0b5e14c 100644 --- a/src/lib/screenplay/extensions/structural-refresh.ts +++ b/src/lib/screenplay/extensions/structural-refresh.ts @@ -26,7 +26,7 @@ export function scheduleStructuralRefresh(view: EditorView) { if (pendingRefresh !== null) return; pendingRefresh = requestAnimationFrame(() => { pendingRefresh = null; - if (!(view as any).isDestroyed) { + if (!(view as EditorView & { isDestroyed?: boolean }).isDestroyed) { view.dispatch(view.state.tr.setMeta(STRUCTURAL_REFRESH_META, true)); } }); diff --git a/src/lib/screenplay/nodes/action-node.ts b/src/lib/screenplay/nodes/action-node.ts index c609a66d..70e35a5a 100644 --- a/src/lib/screenplay/nodes/action-node.ts +++ b/src/lib/screenplay/nodes/action-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface ActionNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } export const ActionNode = Node.create({ diff --git a/src/lib/screenplay/nodes/character-node.ts b/src/lib/screenplay/nodes/character-node.ts index 4d105c98..5fd2411d 100644 --- a/src/lib/screenplay/nodes/character-node.ts +++ b/src/lib/screenplay/nodes/character-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface CharacterNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } export const CharacterNode = Node.create({ diff --git a/src/lib/screenplay/nodes/dialogue-node.ts b/src/lib/screenplay/nodes/dialogue-node.ts index fac6aa15..314480f1 100644 --- a/src/lib/screenplay/nodes/dialogue-node.ts +++ b/src/lib/screenplay/nodes/dialogue-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface DialogueNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } export const DialogueNode = Node.create({ diff --git a/src/lib/screenplay/nodes/dual-dialogue-node.ts b/src/lib/screenplay/nodes/dual-dialogue-node.ts index af8ba759..60941270 100644 --- a/src/lib/screenplay/nodes/dual-dialogue-node.ts +++ b/src/lib/screenplay/nodes/dual-dialogue-node.ts @@ -6,7 +6,7 @@ import { Plugin, PluginKey } from "@tiptap/pm/state"; import { Node as PMNode } from "@tiptap/pm/model"; export interface DualDialogueNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } /** diff --git a/src/lib/screenplay/nodes/marks-node.ts b/src/lib/screenplay/nodes/marks-node.ts index a70d9730..ba45ad16 100644 --- a/src/lib/screenplay/nodes/marks-node.ts +++ b/src/lib/screenplay/nodes/marks-node.ts @@ -27,7 +27,7 @@ export const ScriptioBold = Bold.extend({ { tag: "span", preserveWhitespace: "full", - getAttrs: (e: any) => { + getAttrs: (e: HTMLElement) => { return e.getAttribute("class") === "bold" && null; }, }, @@ -36,7 +36,7 @@ export const ScriptioBold = Bold.extend({ ]; }, - renderHTML({ HTMLAttributes }: any) { + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { return ["span", HTMLAttributes, 0]; }, }); @@ -58,7 +58,7 @@ export const ScriptioItalic = Italic.extend({ { tag: "span", preserveWhitespace: "full", - getAttrs: (e: any) => { + getAttrs: (e: HTMLElement) => { return e.getAttribute("class") === "italic" && null; }, }, @@ -67,7 +67,7 @@ export const ScriptioItalic = Italic.extend({ ]; }, - renderHTML({ HTMLAttributes }: any) { + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { return ["span", HTMLAttributes, 0]; }, }); @@ -89,7 +89,7 @@ export const ScriptioUnderline = Underline.extend({ { tag: "span", preserveWhitespace: "full", - getAttrs: (e: any) => { + getAttrs: (e: HTMLElement) => { return e.getAttribute("class") === "underline" && null; }, }, @@ -98,7 +98,7 @@ export const ScriptioUnderline = Underline.extend({ ]; }, - renderHTML({ HTMLAttributes }: any) { + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { return ["span", HTMLAttributes, 0]; }, }); diff --git a/src/lib/screenplay/nodes/note-node.ts b/src/lib/screenplay/nodes/note-node.ts index 0e4ebd3b..8da690b2 100644 --- a/src/lib/screenplay/nodes/note-node.ts +++ b/src/lib/screenplay/nodes/note-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface NoteNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } export const NoteNode = Node.create({ diff --git a/src/lib/screenplay/nodes/parenthetical-node.ts b/src/lib/screenplay/nodes/parenthetical-node.ts index 026fbdf5..c11b4aa4 100644 --- a/src/lib/screenplay/nodes/parenthetical-node.ts +++ b/src/lib/screenplay/nodes/parenthetical-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface ParentheticalNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } export const ParentheticalNode = Node.create({ diff --git a/src/lib/screenplay/nodes/scene-node.ts b/src/lib/screenplay/nodes/scene-node.ts index 4a51d53b..457d84d3 100644 --- a/src/lib/screenplay/nodes/scene-node.ts +++ b/src/lib/screenplay/nodes/scene-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface SceneNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } /** diff --git a/src/lib/screenplay/nodes/section-node.ts b/src/lib/screenplay/nodes/section-node.ts index 2dcdd986..4a465a2d 100644 --- a/src/lib/screenplay/nodes/section-node.ts +++ b/src/lib/screenplay/nodes/section-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface SectionNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } export const SectionNode = Node.create({ diff --git a/src/lib/screenplay/nodes/transition-node.ts b/src/lib/screenplay/nodes/transition-node.ts index b969d879..cae44a98 100644 --- a/src/lib/screenplay/nodes/transition-node.ts +++ b/src/lib/screenplay/nodes/transition-node.ts @@ -3,7 +3,7 @@ import { ScreenplayElement } from "../../utils/enums"; import { ALIGN_CLASSES, generateNodeId } from "./index"; export interface TransitionNodeOptions { - HTMLAttributes: Record; + HTMLAttributes: Record; } export const TransitionNode = Node.create({ diff --git a/src/lib/screenplay/screenplay.ts b/src/lib/screenplay/screenplay.ts index f5b4f58a..454e2e65 100644 --- a/src/lib/screenplay/screenplay.ts +++ b/src/lib/screenplay/screenplay.ts @@ -1,10 +1,10 @@ -import { JSONContent, NodeType } from "@tiptap/react"; +import { JSONContent } from "@tiptap/react"; import { ScreenplayElement } from "../utils/enums"; /* Nodes */ export type NodeData = { type: ScreenplayElement; - content: any[]; // contains marks (bold, italic, etc.) + content: JSONContent[]; // contains marks (bold, italic, etc.) flattenText: string; // contains only text }; diff --git a/src/lib/spellcheck/spellcheck.worker.ts b/src/lib/spellcheck/spellcheck.worker.ts index f1d98a8f..20e4d5ce 100644 --- a/src/lib/spellcheck/spellcheck.worker.ts +++ b/src/lib/spellcheck/spellcheck.worker.ts @@ -1,6 +1,13 @@ import type { SpellWorkerRequest, SpellWorkerResponse } from "./spellcheck-types"; -let hunspell: any = null; +interface HunspellInstance { + spell: (word: string) => boolean; + suggest: (word: string) => string[]; + addWord: (word: string) => void; + removeWord: (word: string) => void; +} + +let hunspell: HunspellInstance | null = null; function post(msg: SpellWorkerResponse) { self.postMessage(msg); diff --git a/src/lib/titlepage/nodes/text-node.ts b/src/lib/titlepage/nodes/text-node.ts index 2c25295e..3f4473d7 100644 --- a/src/lib/titlepage/nodes/text-node.ts +++ b/src/lib/titlepage/nodes/text-node.ts @@ -29,14 +29,14 @@ export const TitlePageTextNode = Node.create({ if (element.classList.contains("align-right")) return "right"; return "left"; }, - renderHTML: (attributes: Record) => { + renderHTML: (attributes: Record) => { const cls = ALIGN_CLASSES[attributes.textAlign] || ALIGN_CLASSES.left; return { class: cls }; }, }, height: { default: null, - renderHTML: (attributes: Record) => + renderHTML: (attributes: Record) => attributes.height != null ? { "data-height": attributes.height } : {}, parseHTML: (element: HTMLElement) => { const v = element.getAttribute("data-height"); diff --git a/src/lib/utils/api-utils.ts b/src/lib/utils/api-utils.ts index da5d3e52..bc75e175 100644 --- a/src/lib/utils/api-utils.ts +++ b/src/lib/utils/api-utils.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import z from "zod"; -export interface ApiResponse { +export interface ApiResponse { status: "success" | "error"; message?: string; data?: T; diff --git a/src/lib/utils/hooks.ts b/src/lib/utils/hooks.ts index 6bd568e4..d9fae605 100644 --- a/src/lib/utils/hooks.ts +++ b/src/lib/utils/hooks.ts @@ -70,12 +70,7 @@ const useDraggable = (initialPosition?: Position): UseDraggableReturn => { }; const useDesktop = (): boolean => { - const [isDesktop, setIsDesktop] = useState(false); - - useEffect(() => { - if (window.__TAURI__) setIsDesktop(true); - }, []); - + const [isDesktop] = useState(() => typeof window !== "undefined" && !!window.__TAURI__); return isDesktop; }; @@ -88,14 +83,7 @@ interface StateResult { const useProjectIdFromUrl = () => { const searchParams = useSearchParams(); - const [projectId, setProjectId] = useState(""); - - useEffect(() => { - const projectId = searchParams.get("projectId"); - if (projectId) setProjectId(projectId as string); - }, [searchParams]); - - return projectId; + return searchParams.get("projectId") ?? ""; }; const useUser = () => { @@ -394,23 +382,11 @@ const useProjectCollaborators = (projectId: string | undefined) => { const usePage = (): Page | undefined => { const pathname = usePathname(); - const [page, setPage] = useState(undefined); - - useEffect(() => { - if (!pathname) return; - - const segments = pathname.split("/").filter(Boolean); - if (segments.length === 0) { - setPage("index"); - return; - } - - const lastSegment = segments[segments.length - 1]; - if (isPage(lastSegment)) setPage(lastSegment as Page); - else setPage("index"); - }, [pathname]); - - return page; + if (!pathname) return undefined; + const segments = pathname.split("/").filter(Boolean); + if (segments.length === 0) return "index"; + const lastSegment = segments[segments.length - 1]; + return isPage(lastSegment) ? (lastSegment as Page) : "index"; }; export const useEffectiveKeybinds = (userShortcuts: Record | undefined) => { diff --git a/src/lib/utils/misc.ts b/src/lib/utils/misc.ts index d447f1e0..7e85e277 100644 --- a/src/lib/utils/misc.ts +++ b/src/lib/utils/misc.ts @@ -48,7 +48,7 @@ export const cropImageBase64 = async (file: File, width: number, height: number) ctx.fillStyle = "white"; ctx.fillRect(0, 0, width, height); - let ratio = Math.min(width / img.width, height / img.height); + const ratio = Math.min(width / img.width, height / img.height); ctx?.drawImage(img, 0, 0, img.width * ratio, img.height * ratio); return ctx.canvas.toDataURL("image/jpeg") || "data:,"; @@ -75,7 +75,7 @@ export const capitalizeFirstLetter = (str: string) => { return str.charAt(0).toUpperCase() + str.slice(1); }; -export const isEmptyObject = (obj: Object) => { +export const isEmptyObject = (obj: object) => { return Object.keys(obj).length === 0; }; diff --git a/src/lib/utils/requests.ts b/src/lib/utils/requests.ts index 57bf3673..ef0e5e8f 100644 --- a/src/lib/utils/requests.ts +++ b/src/lib/utils/requests.ts @@ -13,7 +13,7 @@ type RESTMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || ""; -const request = async (url: string, method: RESTMethod, body?: Object) => { +const request = async (url: string, method: RESTMethod, body?: object) => { const json = JSON.stringify(body); const headers: Record = { "Content-Type": "application/json" }; diff --git a/src/lib/utils/roles.ts b/src/lib/utils/roles.ts index 7889dc45..2cd492b3 100644 --- a/src/lib/utils/roles.ts +++ b/src/lib/utils/roles.ts @@ -17,7 +17,7 @@ export function hasRoleOrGreater(userRole: ProjectRole, requiredRole: ProjectRol return userLevel >= requiredLevel; } -export function isValid(role: any): boolean { +export function isValid(role: unknown): boolean { if (typeof role === "string") return Object.keys(ROLES).includes(role); return false; } diff --git a/src/server/repository/project-repository.ts b/src/server/repository/project-repository.ts index 1e20c239..51226675 100644 --- a/src/server/repository/project-repository.ts +++ b/src/server/repository/project-repository.ts @@ -39,9 +39,8 @@ export type ProjectInvite = Prisma.ProjectInvitationGetPayload<{ }; }>; -const projectSelect = projectMembershipSelect.project.select; type RawProject = Prisma.ProjectGetPayload<{ - select: typeof projectSelect; + select: typeof projectMembershipSelect.project.select; }>; type RawMembership = {