diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 4f43eb16c..9cc597b1a 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -22,9 +22,9 @@ jobs: run: npm ci - name: Publish to Chromatic uses: chromaui/action@v1 - continue-on-error: true # remove once Svue errors are out with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + exitOnceUploaded: true # onlyChanged: true # externals: | # - 'public/**' diff --git a/jsconfig.json b/jsconfig.json index 97939e152..63c97a632 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -9,7 +9,8 @@ "skipLibCheck": true, "sourceMap": true, "strict": false, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["@testing-library/jest-dom"] }, // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files // diff --git a/src/api/embed.js b/src/api/embed.js index abc73dbb7..ba610f07b 100644 --- a/src/api/embed.js +++ b/src/api/embed.js @@ -1,4 +1,4 @@ -import session from "./session.js"; +import session from "./session"; import { apiUrl } from "./base.js"; import { queryBuilder } from "@/util/url.js"; diff --git a/src/api/session.js b/src/api/session.ts similarity index 88% rename from src/api/session.js rename to src/api/session.ts index cf7e7837f..3ed07a6e6 100644 --- a/src/api/session.js +++ b/src/api/session.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { type AxiosInstance } from "axios"; import axiosRetry from "axios-retry"; import { DC_BASE } from "../config/config.js"; @@ -30,11 +30,12 @@ export function getCsrfToken() { return token; } -const session = axios.create({ - xsrfCookieName: CSRF_COOKIE_NAME, - xsrfHeaderName: CSRF_HEADER_NAME, - withCredentials: cookiesEnabled, -}); +const session: AxiosInstance & { getStatic?: (url: string) => any } = + axios.create({ + xsrfCookieName: CSRF_COOKIE_NAME, + xsrfHeaderName: CSRF_HEADER_NAME, + withCredentials: cookiesEnabled, + }); session.interceptors.response.use( (response) => { @@ -55,6 +56,11 @@ session.interceptors.response.use( const CACHE_LIMIT = 50; +export interface SessionCache { + cached: any[]; + cachedByUrl: Record<string, any>; +} + export class SessionCache { constructor() { this.cached = []; @@ -86,7 +92,7 @@ export class SessionCache { const sessionCache = new SessionCache(); -session.getStatic = async function getStatic(url) { +session.getStatic = async function getStatic(url: string) { if (sessionCache.has(url)) { return sessionCache.lookup(url); } diff --git a/src/api/types/document.ts b/src/api/types/document.ts index 0f03fc923..ec6a85949 100644 --- a/src/api/types/document.ts +++ b/src/api/types/document.ts @@ -4,6 +4,13 @@ import type { Project } from "./project"; export type DocumentAccess = "public" | "organization" | "private"; +export type DocumentStatus = + | "success" + | "readable" + | "pending" + | "error" + | "nofile"; + export interface DocumentRevision { version: number; user: number; @@ -19,7 +26,7 @@ export interface Document { asset_url: string; canonical_url: string; created_at: string; - data: Record<string, unknown>; + data: Record<string, string[]>; description: string; edit_access: boolean; file_hash: string; @@ -39,7 +46,7 @@ export interface Document { revisions?: DocumentRevision[]; slug: string; source: string; - status: "success" | "failure" | "queued" | "in_progress"; + status: DocumentStatus; title: string; updated_at: string; user: User | number; diff --git a/src/api/viewer.js b/src/api/viewer.ts similarity index 100% rename from src/api/viewer.js rename to src/api/viewer.ts diff --git a/src/common/Dropdown2.svelte b/src/common/Dropdown2.svelte index d34e9134a..65fb96489 100644 --- a/src/common/Dropdown2.svelte +++ b/src/common/Dropdown2.svelte @@ -99,6 +99,8 @@ <!-- Element to Trigger Dropdown --> <div class="dropdownContainer" class:open={isOpen} {id}> <div + role="button" + tabindex={0} bind:this={title} class={`title ${titleColor}`} class:open={isOpen} diff --git a/src/common/ProgressiveImage.svelte b/src/common/ProgressiveImage.svelte index c7dc18c34..3f54d6890 100644 --- a/src/common/ProgressiveImage.svelte +++ b/src/common/ProgressiveImage.svelte @@ -1,7 +1,7 @@ <script> import { onMount, onDestroy, createEventDispatcher } from "svelte"; import { IMAGE_WIDTHS } from "../config/config.js"; - import { pageImageUrl } from "@/api/viewer.js"; + import { pageImageUrl } from "@/api/viewer"; import { timeout } from "@/util/timeout.js"; const dispatch = createEventDispatcher(); diff --git a/src/config/config.js b/src/config/config.js index a07d90374..bd3c7807c 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -51,6 +51,9 @@ export const CSRF_HEADER_NAME = "X-CSRFToken"; export const POLL_INTERVAL = 5000; +/** + * @type {Array<[string, number]>} + */ export const IMAGE_WIDTHS_ENTRIES = [ ["xlarge", 2000], ["large", 1000], @@ -59,10 +62,9 @@ export const IMAGE_WIDTHS_ENTRIES = [ ["thumbnail", 60], ]; -export const IMAGE_WIDTHS = IMAGE_WIDTHS_ENTRIES.map(([name, width]) => [ - width, - name, -]).sort((a, b) => a[0] - b[0]); +export const IMAGE_WIDTHS = IMAGE_WIDTHS_ENTRIES.sort( + (a, b) => a[1] - b[1], +).map(([k, v]) => [v, k]); export const IMAGE_WIDTHS_MAP = new Map(IMAGE_WIDTHS_ENTRIES); diff --git a/src/lib/api/addons.ts b/src/lib/api/addons.ts index 61ffddb2c..73ebb9a37 100644 --- a/src/lib/api/addons.ts +++ b/src/lib/api/addons.ts @@ -1,8 +1,10 @@ +import type { Page } from "@/api/types/common"; +import type { AddOnListItem } from "@/addons/types"; + import { error } from "@sveltejs/kit"; + import { BASE_API_URL } from "@/config/config.js"; -import { type AddOnListItem } from "@/addons/types"; import { isErrorCode } from "../utils"; -import type { Page } from "@/api/types/common"; export async function getPinnedAddons( fetch = globalThis.fetch, @@ -15,7 +17,7 @@ export async function getPinnedAddons( if (isErrorCode(resp.status)) { error(resp.status, resp.statusText); } - return resp.json(); + return resp.json() as Promise<Page<AddOnListItem>>; } export async function getAddons( @@ -26,5 +28,5 @@ export async function getAddons( if (isErrorCode(resp.status)) { error(resp.status, resp.statusText); } - return resp.json(); + return resp.json() as Promise<Page<AddOnListItem>>; } diff --git a/src/lib/api/notes.js b/src/lib/api/notes.js deleted file mode 100644 index 897b41d50..000000000 --- a/src/lib/api/notes.js +++ /dev/null @@ -1,107 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { BASE_API_URL } from "@/config/config.js"; -import { DEFAULT_EXPAND } from "@/api/common.js"; -import { canonicalUrl } from "./documents"; - -/** - * Load notes from a single document from the API - * Example: https://api.www.documentcloud.org/api/documents/2622/notes/ - * - * @async - * @export - * @param {number} doc_id - * @param {globalThis.fetch} fetch - * @returns {import('./types').NoteResults} - */ -export async function list(doc_id, fetch) { - const endpoint = new URL(`documents/${doc_id}/notes.json`, BASE_API_URL); - - endpoint.searchParams.set("expand", DEFAULT_EXPAND); - - const resp = await fetch(endpoint, { credentials: "include" }); - - if (!resp.ok) { - error(resp.status, resp.statusText); - } - - return resp.json(); -} - -/** - * Load a single note from a single document from the API - * Example: https://api.www.documentcloud.org/api/documents/2622/notes/549/ - * - * @async - * @export - * @param {number} doc_id - * @param {globalThis.fetch} fetch - * @returns {import('./types').Note} - */ -export async function get(doc_id, note_id, fetch) { - const endpoint = new URL( - `documents/${doc_id}/notes/${note_id}.json`, - BASE_API_URL, - ); - - endpoint.searchParams.set("expand", DEFAULT_EXPAND); - - const resp = await fetch(endpoint, { credentials: "include" }); - - if (!resp.ok) { - error(resp.status, resp.statusText); - } - - return resp.json(); -} - -/** - * Canonical URL for a note, relative to the current server - * This will be correct in all environments, including deploy previews - * https://www.documentcloud.org/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/annotations/557 - * - * @export - * @param {import('./types').Document} document - * @param {import('./types').Note} note - * @returns {URL} - */ -export function canonicalNoteUrl(document, note) { - return new URL(`annotations/${note.id}/`, canonicalUrl(document)); -} - -/** - * Hash URL for a note within the document viewer - * https://www.documentcloud.org/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/#document/p3/a557 - * - * @export - * @param {import('./types').Document} document - * @param {import('./types').Note} note - * @returns {URL} - */ -export function noteUrl(document, note) { - return new URL( - `#document/p${note.page_number + 1}/a${note.id}`, - canonicalUrl(document), - ); -} - -/** - * Width of a note, relative to the document - * - * @export - * @param {import('./types').Note} note - * @returns {number} - */ -export function width(note) { - return note.x2 - note.x1; -} - -/** - * Height of a note, relative to the document - * - * @export - * @param {import('./types').Note} note - * @returns {number} - */ -export function height(note) { - return note.y2 - note.y1; -} diff --git a/src/lib/api/notes.test.js b/src/lib/api/notes.test.js deleted file mode 100644 index 775262e3c..000000000 --- a/src/lib/api/notes.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { test as base, describe, expect } from "vitest"; - -import { APP_URL } from "@/config/config.js"; -import * as notes from "./notes.js"; - -const test = base.extend({ - document: async ({}, use) => { - const doc = await import("./fixtures/documents/document.json"); - - await use(doc); - }, - - note: async ({}, use) => { - const note = await import("./fixtures/notes/note.json"); - - await use(note); - }, -}); - -describe("fetching notes", () => { - test.todo("notes.get"); - test.todo("notes.list"); -}); - -describe("note helper methods", () => { - test("canonicalNoteUrl", ({ document, note }) => { - expect(notes.canonicalNoteUrl(document, note)).toStrictEqual( - new URL( - "/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/annotations/557/", - APP_URL, - ), - ); - }); - - test("noteUrl", ({ document, note }) => { - expect(notes.noteUrl(document, note)).toStrictEqual( - new URL( - "/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/#document/p3/a557", - APP_URL, - ), - ); - }); - - test("width", ({ note }) => { - expect(notes.width(note)).toStrictEqual(note.x2 - note.x1); - }); - - test("height", ({ note }) => { - expect(notes.height(note)).toStrictEqual(note.y2 - note.y1); - }); -}); diff --git a/src/lib/api/notes.test.ts b/src/lib/api/notes.test.ts new file mode 100644 index 000000000..9648b9a5f --- /dev/null +++ b/src/lib/api/notes.test.ts @@ -0,0 +1,43 @@ +import { test, describe, expect } from "vitest"; + +import { APP_URL } from "@/config/config.js"; +import * as notes from "./notes"; +import type { Document, Note } from "./types"; +import document from "./fixtures/documents/document.json"; +import note from "./fixtures/notes/note.json"; + +describe("fetching notes", () => { + test.todo("notes.get"); + test.todo("notes.list"); +}); + +describe("note helper methods", () => { + const d = document as Document; + const n = note as Note; + + test("canonicalNoteUrl", () => { + expect(notes.canonicalNoteUrl(d, n)).toStrictEqual( + new URL( + "/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/annotations/557/", + APP_URL, + ), + ); + }); + + test("noteUrl", () => { + expect(notes.noteUrl(d, n)).toStrictEqual( + new URL( + "/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/#document/p3/a557", + APP_URL, + ), + ); + }); + + test("width", () => { + expect(notes.width(n)).toStrictEqual(n.x2 - n.x1); + }); + + test("height", () => { + expect(notes.height(n)).toStrictEqual(n.y2 - n.y1); + }); +}); diff --git a/src/lib/api/notes.ts b/src/lib/api/notes.ts new file mode 100644 index 000000000..91d6eeac2 --- /dev/null +++ b/src/lib/api/notes.ts @@ -0,0 +1,82 @@ +import { error } from "@sveltejs/kit"; +import { BASE_API_URL } from "@/config/config.js"; +import { DEFAULT_EXPAND } from "@/api/common.js"; +import { canonicalUrl } from "./documents"; +import type { Document, Note, NoteResults } from "./types"; +import { isErrorCode } from "../utils"; + +/** + * Load notes from a single document from the API + * @example https://api.www.documentcloud.org/api/documents/2622/notes/ + */ +export async function list( + doc_id: number, + fetch = globalThis.fetch, +): Promise<NoteResults> { + const endpoint = new URL(`documents/${doc_id}/notes.json`, BASE_API_URL); + + endpoint.searchParams.set("expand", DEFAULT_EXPAND); + + const resp = await fetch(endpoint, { credentials: "include" }); + + if (isErrorCode(resp.status)) { + error(resp.status, resp.statusText); + } + + return resp.json(); +} + +/** + * Load a single note from a single document from the API + * @example https://api.www.documentcloud.org/api/documents/2622/notes/549/ + */ +export async function get( + doc_id: number, + note_id: number, + fetch = globalThis.fetch, +): Promise<Note> { + const endpoint = new URL( + `documents/${doc_id}/notes/${note_id}.json`, + BASE_API_URL, + ); + + endpoint.searchParams.set("expand", DEFAULT_EXPAND); + + const resp = await fetch(endpoint, { credentials: "include" }); + + if (isErrorCode(resp.status)) { + error(resp.status, resp.statusText); + } + + return resp.json(); +} + +/** + * Canonical URL for a note, relative to the current server + * This will be correct in all environments, including deploy previews + * @example https://www.documentcloud.org/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/annotations/557 + */ +export function canonicalNoteUrl(document: Document, note: Note): URL { + return new URL(`annotations/${note.id}/`, canonicalUrl(document)); +} + +/** + * Hash URL for a note within the document viewer + * @example https://www.documentcloud.org/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government/#document/p3/a557 + */ +export function noteUrl(document: Document, note: Note): URL { + return new URL( + `#document/p${note.page_number + 1}/a${note.id}`, + canonicalUrl(document), + ); +} + +/** Width of a note, relative to the document */ +export function width(note: Note): number { + return note.x2 - note.x1; +} + +/** Height of a note, relative to the document */ +export function height(note: Note): number { + return note.y2 - note.y1; +} diff --git a/src/lib/api/projects.js b/src/lib/api/projects.ts similarity index 50% rename from src/lib/api/projects.js rename to src/lib/api/projects.ts index c894d910f..76ad41ce5 100644 --- a/src/lib/api/projects.js +++ b/src/lib/api/projects.ts @@ -1,6 +1,9 @@ // api methods for projects -import { error } from "@sveltejs/kit"; +import type { Project, ProjectResults, ProjectMembershipList } from "./types"; + +import { error, type NumericRange } from "@sveltejs/kit"; import { BASE_API_URL } from "@/config/config.js"; +import { isErrorCode } from "$lib/utils/api"; /** * Get a single project @@ -8,9 +11,26 @@ import { BASE_API_URL } from "@/config/config.js"; * @export * @param {number} id * @param {globalThis.fetch} fetch - * @returns {import('./types').Project} + * @returns {Promise<import('./types').Project>} */ -export async function get(id, fetch) {} +export async function get( + id: number, + fetch = globalThis.fetch, +): Promise<Project> { + const endpoint = new URL(`projects/${id}/`, BASE_API_URL); + + const res = await fetch(endpoint, { credentials: "include" }).catch((e) => { + error(500, { message: e }); + }); + + if (isErrorCode(res.status)) { + error(res.status, { + message: res.statusText, + }); + } + + return res.json(); +} /** * Get a list of projects @@ -18,24 +38,24 @@ export async function get(id, fetch) {} * @export * @param {any} params filter params * @param {globalThis.fetch} fetch - * @returns {Promise<import('./types').ProjectResults>} */ -export async function list(params = {}, fetch) { +export async function list( + params: any = {}, + fetch = globalThis.fetch, +): Promise<ProjectResults> { const endpoint = new URL("projects/", BASE_API_URL); for (const [k, v] of Object.entries(params)) { - endpoint.searchParams.set(k, v); + endpoint.searchParams.set(k, String(v)); } - const res = await fetch(endpoint, { credentials: "include" }).catch((e) => ({ - ok: false, - error: e, - })); + const res = await fetch(endpoint, { credentials: "include" }).catch((e) => { + error(500, { message: e }); + }); - if (!res.ok) { + if (isErrorCode(res.status)) { error(res.status, { message: res.statusText, - error: res.error, }); } @@ -48,26 +68,26 @@ export async function list(params = {}, fetch) { * @export * @param {number} id * @param {globalThis.fetch} fetch - * @returns {import('./types').ProjectMembershipList} */ -export async function documents(id, fetch) { +export async function documents( + id: number | string, + fetch = globalThis.fetch, +): Promise<ProjectMembershipList> { const endpoint = new URL(`projects/${id}/documents/`, BASE_API_URL); const expand = ["user", "organization", "document"]; // might make these configurable later endpoint.searchParams.set("expand", expand.join(",")); endpoint.searchParams.set("ordering", "-created_at"); - endpoint.searchParams.set("per_page", 12); + endpoint.searchParams.set("per_page", "12"); - const res = await fetch(endpoint, { credentials: "include" }).catch((e) => ({ - ok: false, - error: e, - })); + const res = await fetch(endpoint, { credentials: "include" }).catch((e) => { + error(500, { message: e }); + }); - if (!res.ok) { + if (isErrorCode(res.status)) { error(res.status, { message: res.statusText, - error: res.error, }); } diff --git a/src/lib/components/common/Logo.svelte b/src/lib/components/common/Logo.svelte index 1ccabdb0c..fecaab696 100644 --- a/src/lib/components/common/Logo.svelte +++ b/src/lib/components/common/Logo.svelte @@ -2,7 +2,6 @@ viewBox="0 0 182 32" fill="none" xmlns="http://www.w3.org/2000/svg" - title="DocumentCloud" class="icon" > <path diff --git a/src/lib/components/forms/DocumentUpload.svelte b/src/lib/components/forms/DocumentUpload.svelte index 9ecebe790..08297ac32 100644 --- a/src/lib/components/forms/DocumentUpload.svelte +++ b/src/lib/components/forms/DocumentUpload.svelte @@ -472,13 +472,6 @@ background: var(--blue-1); } - .excessiveFileSize { - padding: 0.25rem; - border-radius: 0.25rem; - color: var(--red-dark); - font-size: var(--font-s); - } - .drop-instructions { color: var(--gray-5, #233944); text-align: center; diff --git a/src/lib/utils/api.test.ts b/src/lib/utils/api.test.ts new file mode 100644 index 000000000..581e52d3f --- /dev/null +++ b/src/lib/utils/api.test.ts @@ -0,0 +1,22 @@ +import { test, expect } from "vitest"; +import { isErrorCode, isRedirectCode } from "./api"; + +test("isErrorCode", () => { + expect(isErrorCode(200)).toBe(false); + expect(isErrorCode(204)).toBe(false); + expect(isErrorCode(301)).toBe(false); + expect(isErrorCode(307)).toBe(false); + expect(isErrorCode(400)).toBe(true); + expect(isErrorCode(404)).toBe(true); + expect(isErrorCode(500)).toBe(true); +}); + +test("isRedirectCode", () => { + expect(isRedirectCode(200)).toBe(false); + expect(isRedirectCode(204)).toBe(false); + expect(isRedirectCode(301)).toBe(true); + expect(isRedirectCode(307)).toBe(true); + expect(isRedirectCode(400)).toBe(false); + expect(isRedirectCode(404)).toBe(false); + expect(isRedirectCode(500)).toBe(false); +}); diff --git a/src/lib/utils/isErrorCode.ts b/src/lib/utils/api.ts similarity index 57% rename from src/lib/utils/isErrorCode.ts rename to src/lib/utils/api.ts index 66bcf58c9..d0a93386c 100644 --- a/src/lib/utils/isErrorCode.ts +++ b/src/lib/utils/api.ts @@ -3,3 +3,9 @@ import type { NumericRange } from "@sveltejs/kit"; export function isErrorCode(status: number): status is NumericRange<400, 599> { return status >= 400 && status <= 599; } + +export function isRedirectCode( + status: number, +): status is NumericRange<300, 308> { + return status >= 300 && status <= 308; +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 93e586613..af9443a45 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1 +1 @@ -export { isErrorCode } from "./isErrorCode"; +export { isErrorCode, isRedirectCode } from "./api"; diff --git a/src/pages/app/Document.svelte b/src/pages/app/Document.svelte index 20b5e70f6..88219754f 100644 --- a/src/pages/app/Document.svelte +++ b/src/pages/app/Document.svelte @@ -29,7 +29,7 @@ import closeSimpleSvg from "@/assets/close_inline.svg?raw"; import pencilSvg from "@/assets/pencil.svg?raw"; - import { pageImageUrl } from "@/api/viewer.js"; + import { pageImageUrl } from "@/api/viewer"; import RevisionIcon from "../../common/RevisionIcon.svelte"; export let document; diff --git a/src/pages/embed/note/Note.svelte b/src/pages/embed/note/Note.svelte index 2c403be55..c0058ae2c 100644 --- a/src/pages/embed/note/Note.svelte +++ b/src/pages/embed/note/Note.svelte @@ -8,7 +8,7 @@ import { onMount, tick } from "svelte"; import { getAnnotation } from "@/api/annotation.js"; import { getDocument } from "@/api/document.js"; - import { pageImageUrl } from "@/api/viewer.js"; + import { pageImageUrl } from "@/api/viewer"; import { embedUrl } from "@/api/embed.js"; import { currentUrl, diff --git a/src/pages/embed/page/Page.svelte b/src/pages/embed/page/Page.svelte index 9858aeb54..8bd6b4b85 100644 --- a/src/pages/embed/page/Page.svelte +++ b/src/pages/embed/page/Page.svelte @@ -9,7 +9,7 @@ import { informSize } from "@/embed/iframeSizer.js"; import { getDocument } from "@/api/document.js"; import { getAnnotations } from "@/api/annotation.js"; - import { textUrl, pageImageUrl } from "@/api/viewer.js"; + import { textUrl, pageImageUrl } from "@/api/viewer"; import { embedUrl } from "@/api/embed.js"; import { APP_URL, DC_BASE } from "../../../config/config.js"; diff --git a/src/pages/entities/Entities.svelte b/src/pages/entities/Entities.svelte index d0cb1d3a4..ebc7f39eb 100644 --- a/src/pages/entities/Entities.svelte +++ b/src/pages/entities/Entities.svelte @@ -8,7 +8,7 @@ import { router } from "@/router/router.js"; import { getDocument } from "@/api/document.js"; import { extractEntities } from "@/api/entity.js"; - import { jsonUrl } from "@/api/viewer.js"; + import { jsonUrl } from "@/api/viewer"; import session from "@/api/session.js"; import { entities, getE } from "@/entities/entities.js"; import { updateInCollection } from "@/manager/documents.js"; diff --git a/src/pages/viewer/AllText.svelte b/src/pages/viewer/AllText.svelte index 2bcb9eb5c..4902f5be5 100644 --- a/src/pages/viewer/AllText.svelte +++ b/src/pages/viewer/AllText.svelte @@ -5,7 +5,7 @@ import TextPage from "./TextPage.svelte"; import session from "@/api/session.js"; - import { jsonUrl } from "@/api/viewer.js"; + import { jsonUrl } from "@/api/viewer"; import { doc } from "@/viewer/document.js"; import { layout } from "@/viewer/layout.js"; import { viewer } from "@/viewer/viewer.js"; diff --git a/src/pages/viewer/InternalPage.svelte b/src/pages/viewer/InternalPage.svelte index e81ac8f8f..d3ab62f32 100644 --- a/src/pages/viewer/InternalPage.svelte +++ b/src/pages/viewer/InternalPage.svelte @@ -9,7 +9,7 @@ import ProgressiveImage from "@/common/ProgressiveImage.svelte"; import session from "@/api/session.js"; - import { selectableTextUrl } from "@/api/viewer.js"; + import { selectableTextUrl } from "@/api/viewer"; import { doc, showAnnotation } from "@/viewer/document.js"; import { layout } from "@/viewer/layout.js"; diff --git a/src/pages/viewer/ModifyImage.svelte b/src/pages/viewer/ModifyImage.svelte index c46e066c5..a67daf29b 100644 --- a/src/pages/viewer/ModifyImage.svelte +++ b/src/pages/viewer/ModifyImage.svelte @@ -1,7 +1,7 @@ <script> import Image from "@/common/Image.svelte"; import { viewer } from "@/viewer/viewer.js"; - import { pageImageUrl } from "@/api/viewer.js"; + import { pageImageUrl } from "@/api/viewer"; import { modification } from "@/viewer/modification/modification.js"; import { getDocument } from "@/api/document.js"; diff --git a/src/pages/viewer/SearchResults.svelte b/src/pages/viewer/SearchResults.svelte index b077d5bb5..19872a6d5 100644 --- a/src/pages/viewer/SearchResults.svelte +++ b/src/pages/viewer/SearchResults.svelte @@ -6,7 +6,7 @@ import { layout } from "@/viewer/layout.js"; import { viewer } from "@/viewer/viewer.js"; import { doc, changeMode, restorePosition } from "@/viewer/document.js"; - import { selectableTextUrl } from "@/api/viewer.js"; + import { selectableTextUrl } from "@/api/viewer"; import session from "@/api/session.js"; async function handlePage(page) { diff --git a/src/pages/viewer/Sidebar.svelte b/src/pages/viewer/Sidebar.svelte index 784deeca9..a36aab222 100644 --- a/src/pages/viewer/Sidebar.svelte +++ b/src/pages/viewer/Sidebar.svelte @@ -4,7 +4,7 @@ import AccessIcon from "../../common/AccessIcon.svelte"; import HtmlField from "../../common/HtmlField.svelte"; import session from "../../api/session.js"; - import { jsonUrl } from "../../api/viewer.js"; + import { jsonUrl } from "../../api/viewer"; import { enterRedactMode, diff --git a/src/pages/viewer/Viewer.svelte b/src/pages/viewer/Viewer.svelte index 0e9388d5c..6c8cdf12e 100644 --- a/src/pages/viewer/Viewer.svelte +++ b/src/pages/viewer/Viewer.svelte @@ -39,7 +39,7 @@ } from "@/viewer/layout.js"; import { doc, showAnnotation } from "@/viewer/document.js"; import { viewer } from "@/viewer/viewer.js"; - import { pageImageUrl } from "@/api/viewer.js"; + import { pageImageUrl } from "@/api/viewer"; let shareOption = null; diff --git a/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.svelte b/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.svelte index c289bd4f8..4a4a66855 100644 --- a/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.svelte +++ b/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.svelte @@ -1,13 +1,13 @@ -<script> +<script lang="ts"> import DomPurify from "dompurify"; import { onMount } from "svelte"; import { _ } from "svelte-i18n"; import { informSize } from "@/embed/iframeSizer.js"; import { pageImageUrl } from "@/lib/api/documents"; - import * as notes from "$lib/api/notes.js"; + import * as notes from "@/lib/api/notes"; import { embedUrl } from "$lib/api/embed"; - import { canonicalNoteUrl, noteUrl } from "$lib/api/notes.js"; + import { canonicalNoteUrl, noteUrl } from "@/lib/api/notes"; import { pageSizesFromSpec } from "@/api/pageSize.js"; import { IMAGE_WIDTHS_MAP } from "@/config/config.js"; @@ -32,9 +32,6 @@ onMount(async () => { informSize(elem); - - // debug - window.note = note; }); </script> @@ -51,11 +48,11 @@ <link rel="alternate" type="application/json+oembed" - href={embedUrl(url)} + href={embedUrl(url).toString()} {title} /> - {#if note.description} - <meta property="og:description" content={note.description} /> + {#if note.content} + <meta property="og:description" content={note.content} /> {/if} <meta property="og:image" content={src} /> </svelte:head> @@ -63,7 +60,7 @@ <div class="DC-note" bind:this={elem} style={`background-image: url(${src})`}> <div class="DC-note-header"> <a - href={noteUrl(doc, note)} + href={noteUrl(doc, note).toString()} class="DC-note-embed-resource" target="_blank" title={$_("embedNote.viewTheNote", { values: { title: note.title } })} @@ -89,7 +86,7 @@ 100}%" > <a - href={noteUrl(doc, note)} + href={noteUrl(doc, note).toString()} target="_blank" class="DC-note-image-link" style:left="{(note.x1 * -100) / notes.width(note)}%" @@ -108,7 +105,7 @@ <div class="DC-note-credit"> <a - href={noteUrl(doc, note)} + href={noteUrl(doc, note).toString()} target="_blank" title={$_("embedNote.viewTheNote", { values: { title: note.title } })} > diff --git a/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.js b/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.ts similarity index 57% rename from src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.js rename to src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.ts index a9de0b125..dbe83ca2c 100644 --- a/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.js +++ b/src/routes/(embed)/documents/[id]/annotations/[note_id]/+page.ts @@ -1,12 +1,11 @@ // load a note for embedding import * as documents from "@/lib/api/documents"; -import * as notesApi from "$lib/api/notes.js"; +import * as notesApi from "$lib/api/notes"; -/** @type {import('./$types').PageLoad} */ export async function load({ params, url, fetch }) { const [document, note] = await Promise.all([ - documents.get(params.id, fetch), - notesApi.get(params.id, params.note_id), + documents.get(+params.id, fetch), + notesApi.get(+params.id, parseInt(params.note_id), fetch), ]); return { diff --git a/src/routes/(embed)/documents/[id]/pages/[page]/+page.svelte b/src/routes/(embed)/documents/[id]/pages/[page]/+page.svelte index 74e95e28e..2216e9259 100644 --- a/src/routes/(embed)/documents/[id]/pages/[page]/+page.svelte +++ b/src/routes/(embed)/documents/[id]/pages/[page]/+page.svelte @@ -1,4 +1,5 @@ -<script> +<script lang="ts"> + import { createEventDispatcher } from "svelte"; import { _ } from "svelte-i18n"; import Annotation from "./Annotation.svelte"; @@ -18,6 +19,8 @@ export let data; + const dispatch = createEventDispatcher(); + let elem; let active = null; @@ -112,13 +115,11 @@ {#each shimPlacements as s} <div class="dc-embed-shim" - on:click={() => (active = null)} style="left:{s[0] * 100}%;top:{s[1] * 100}%;right:{(1 - s[2]) * 100}%;bottom:{(1 - s[3]) * 100}%" tabindex="-1" role="dialog" aria-modal="true" - on:keydown={onKeyup} /> {/each} diff --git a/src/routes/(embed)/documents/[id]/pages/[page]/+page.js b/src/routes/(embed)/documents/[id]/pages/[page]/+page.ts similarity index 52% rename from src/routes/(embed)/documents/[id]/pages/[page]/+page.js rename to src/routes/(embed)/documents/[id]/pages/[page]/+page.ts index 2fb53c484..0c2a36eff 100644 --- a/src/routes/(embed)/documents/[id]/pages/[page]/+page.js +++ b/src/routes/(embed)/documents/[id]/pages/[page]/+page.ts @@ -1,20 +1,18 @@ // load data for a single page embed import * as documents from "@/lib/api/documents"; -import * as notesApi from "$lib/api/notes.js"; +import * as notesApi from "$lib/api/notes"; /** @type {import('./$types').PageLoad} */ export async function load({ params, fetch }) { - const { page } = params; + const page = +params.page; let [document, notes] = await Promise.all([ - documents.get(params.id, fetch), - notesApi.list(params.id, fetch), + documents.get(+params.id, fetch), + notesApi.list(+params.id, fetch), ]); - notes = notes.results.filter((note) => note.page_number === page - 1); - return { document, - notes, - page, + notes: notes.results.filter((note) => note.page_number === page - 1), + page: +page, }; } diff --git a/src/routes/(embed)/projects/[project_id]/+page.svelte b/src/routes/(embed)/projects/[project_id]/+page.svelte index 13082f8a9..6a32a3bd1 100644 --- a/src/routes/(embed)/projects/[project_id]/+page.svelte +++ b/src/routes/(embed)/projects/[project_id]/+page.svelte @@ -1,4 +1,5 @@ -<script> +<script lang="ts"> + import type { Document } from "$lib/api/types"; import DocumentListItem from "$lib/components/documents/DocumentListItem.svelte"; import Paginator from "$lib/components/common/Paginator.svelte"; @@ -11,17 +12,17 @@ $: count = data.documents.count; $: next = data.documents.next; $: previous = data.documents.previous; - $: documents = data.documents.results.map((d) => d.document); + $: documents = data.documents.results.map((d) => d.document) as Document[]; $: total_pages = Math.ceil(count / per_page); async function load(url) { - const res = await fetch(url, { credentials: "include" }).catch((e) => ({ - ok: false, - error: e, - })); + const res = await fetch(url, { credentials: "include" }).catch((e) => { + console.error(e); + return { ok: false, json: () => {} }; + }); if (!res.ok) { - console.error(res.error); + return; } data.documents = await res.json(); diff --git a/src/routes/(embed)/projects/[project_id]/+page.js b/src/routes/(embed)/projects/[project_id]/+page.ts similarity index 61% rename from src/routes/(embed)/projects/[project_id]/+page.js rename to src/routes/(embed)/projects/[project_id]/+page.ts index 3b57f6277..8ed419daf 100644 --- a/src/routes/(embed)/projects/[project_id]/+page.js +++ b/src/routes/(embed)/projects/[project_id]/+page.ts @@ -1,12 +1,12 @@ // load data for project embeds -import * as projects from "$lib/api/projects.js"; +import * as projects from "$lib/api/projects"; /** @type {import('./$types').PageLoad} */ export async function load({ params, fetch }) { const [project, documents] = await Promise.all([ - projects.get(params.project_id, fetch), - projects.documents(params.project_id, fetch), + projects.get(+params.project_id, fetch), + projects.documents(+params.project_id, fetch), ]); return { diff --git a/src/routes/stories/note-embed.stories.svelte b/src/routes/(embed)/stories/note-embed.stories.svelte similarity index 57% rename from src/routes/stories/note-embed.stories.svelte rename to src/routes/(embed)/stories/note-embed.stories.svelte index bf5a75f98..018bde7bc 100644 --- a/src/routes/stories/note-embed.stories.svelte +++ b/src/routes/(embed)/stories/note-embed.stories.svelte @@ -1,14 +1,17 @@ -<script context="module"> +<script context="module" lang="ts"> + import type { Document, Note } from "$lib/api/types"; + // legacy css import "@/style/variables.css"; import "@/style/global.css"; import { Story } from "@storybook/addon-svelte-csf"; - import NoteEmbed from "../(embed)/documents/[id]/annotations/[note_id]/+page.svelte"; + import NoteEmbed from "../documents/[id]/annotations/[note_id]/+page.svelte"; import document from "$lib/api/fixtures/documents/document-expanded.json"; import note from "$lib/api/fixtures/notes/note-expanded.json"; import notes from "$lib/api/fixtures/notes/notes-expanded.json"; + import type { SvelteComponent } from "svelte"; export const meta = { title: "Embed / Note", @@ -17,7 +20,15 @@ parameters: { layout: "centered" }, }; - const data = { note, document }; + const bigNote = notes.results[1] as Note; + + const data = { + note: note as Note, + document: document as Document, + embed: true, + me: null, + org: null, + }; </script> <Story name="default"> @@ -25,5 +36,5 @@ </Story> <Story name="bigger note"> - <NoteEmbed data={{ document, note: notes.results[1] }} /> + <NoteEmbed data={{ ...data, note: bigNote }} /> </Story> diff --git a/src/routes/stories/page-embed.stories.svelte b/src/routes/(embed)/stories/page-embed.stories.svelte similarity index 65% rename from src/routes/stories/page-embed.stories.svelte rename to src/routes/(embed)/stories/page-embed.stories.svelte index 334d8e4f3..8d7104c88 100644 --- a/src/routes/stories/page-embed.stories.svelte +++ b/src/routes/(embed)/stories/page-embed.stories.svelte @@ -1,11 +1,11 @@ -<script context="module"> +<script context="module" lang="ts"> + import type { Document, Note } from "$lib/api/types"; // legacy css import "@/style/variables.css"; import "@/style/global.css"; import { Story } from "@storybook/addon-svelte-csf"; - import PageEmbed from "../(embed)/documents/[id]/pages/[page]/+page.svelte"; - + import PageEmbed from "../documents/[id]/pages/[page]/+page.svelte"; import document from "$lib/api/fixtures/documents/document-expanded.json"; import { results } from "$lib/api/fixtures/notes/notes-expanded.json"; @@ -19,7 +19,14 @@ parameters: { layout: "centered" }, }; - const data = { document, page, notes }; + const data = { + document: document as Document, + page, + notes: notes as Note[], + embed: false, + me: null, + org: null, + }; </script> <Story name="default"> diff --git a/src/routes/stories/project-embed.stories.svelte b/src/routes/(embed)/stories/project-embed.stories.svelte similarity index 66% rename from src/routes/stories/project-embed.stories.svelte rename to src/routes/(embed)/stories/project-embed.stories.svelte index 9da2da7b0..8dfc82365 100644 --- a/src/routes/stories/project-embed.stories.svelte +++ b/src/routes/(embed)/stories/project-embed.stories.svelte @@ -1,10 +1,12 @@ -<script context="module"> +<script context="module" lang="ts"> + import type { Project, ProjectMembershipList } from "$lib/api/types"; + // legacy css import "@/style/variables.css"; import "@/style/global.css"; import { Story } from "@storybook/addon-svelte-csf"; - import ProjectEmbed from "../(embed)/projects/[project_id]/+page.svelte"; + import ProjectEmbed from "../projects/[project_id]/+page.svelte"; import documents from "$lib/api/fixtures/projects/project-documents-expanded.json"; import project from "$lib/api/fixtures/projects/project.json"; @@ -17,7 +19,13 @@ parameters: { layout: "centered" }, }; - const data = { project, documents }; + const data = { + project: project as Project, + documents: documents as ProjectMembershipList, + embed: true, + me: null, + org: null, + }; </script> <Story diff --git a/src/routes/(pages)/[...path]/+page.js b/src/routes/(pages)/[...path]/+page.ts similarity index 81% rename from src/routes/(pages)/[...path]/+page.js rename to src/routes/(pages)/[...path]/+page.ts index 19b8c2c83..40b65c64f 100644 --- a/src/routes/(pages)/[...path]/+page.js +++ b/src/routes/(pages)/[...path]/+page.ts @@ -1,10 +1,11 @@ // load data for flatpages -import { error, redirect } from "@sveltejs/kit"; +import { error, redirect, type NumericRange } from "@sveltejs/kit"; import { marked } from "marked"; import { gfmHeadingId } from "marked-gfm-heading-id"; import DOMPurify from "isomorphic-dompurify"; import { BASE_API_URL } from "@/config/config.js"; +import { isErrorCode, isRedirectCode } from "@/lib/utils/api"; marked.use(gfmHeadingId()); @@ -16,12 +17,12 @@ export async function load({ fetch, params }) { const resp = await fetch(endpoint, { credentials: "include" }); - if (resp.status > 400) { + if (isErrorCode(resp.status)) { error(resp.status, resp.statusText); } // we should be following redirects, so this shouldn't happen - if (resp.status > 300) { + if (isRedirectCode(resp.status)) { redirect(resp.status, resp.headers.get("Location")); } diff --git a/src/routes/(pages)/home/+page.js b/src/routes/(pages)/home/+page.ts similarity index 90% rename from src/routes/(pages)/home/+page.js rename to src/routes/(pages)/home/+page.ts index eb5540c9d..8bd97e6d0 100644 --- a/src/routes/(pages)/home/+page.js +++ b/src/routes/(pages)/home/+page.ts @@ -5,6 +5,7 @@ import { gfmHeadingId } from "marked-gfm-heading-id"; import DOMPurify from "isomorphic-dompurify"; import { BASE_API_URL } from "@/config/config.js"; +import { isErrorCode } from "@/lib/utils/api"; marked.use(gfmHeadingId()); @@ -15,7 +16,7 @@ const endpoint = new URL("home/", ROOT); export async function load({ fetch }) { const resp = await fetch(endpoint, { credentials: "include" }); - if (!resp.ok) { + if (isErrorCode(resp.status)) { error(resp.status, resp.statusText); } diff --git a/src/routes/app/+layout.ts b/src/routes/app/+layout.ts index c033906a9..9ff57d834 100644 --- a/src/routes/app/+layout.ts +++ b/src/routes/app/+layout.ts @@ -1,5 +1,5 @@ import { getPinnedAddons } from "@/lib/api/addons"; -import * as projects from "$lib/api/projects.js"; +import * as projects from "$lib/api/projects"; export async function load({ url, fetch }) { const pinnedAddons = getPinnedAddons(fetch); diff --git a/src/routes/app/sidebar/AddOns.svelte b/src/routes/app/sidebar/AddOns.svelte index c0b0b7f79..138ec47d5 100644 --- a/src/routes/app/sidebar/AddOns.svelte +++ b/src/routes/app/sidebar/AddOns.svelte @@ -1,7 +1,9 @@ <script lang="ts"> + import type { Page } from "@/api/types/common"; + import type { AddOnListItem } from "@/addons/types"; + import { Book16, Hourglass24, Pin24, Plug16 } from "svelte-octicons"; - import type { Page } from "@/api/types"; - import type { AddOnListItem } from "$lib/api/types"; + import Action from "$lib/components/common/Action.svelte"; import Empty from "$lib/components/common/Empty.svelte"; import Flex from "$lib/components/common/Flex.svelte"; diff --git a/src/routes/app/upload/+layout.ts b/src/routes/app/upload/+layout.ts index a0f86ab22..3419918ed 100644 --- a/src/routes/app/upload/+layout.ts +++ b/src/routes/app/upload/+layout.ts @@ -2,7 +2,7 @@ * Data loading for upload */ -import * as projectsApi from "$lib/api/projects.js"; +import * as projectsApi from "$lib/api/projects"; export async function load({ fetch, parent }) { const { me } = await parent(); diff --git a/src/routes/app/upload/+page.server.ts b/src/routes/app/upload/+page.server.ts index d6133e224..3526e0e80 100644 --- a/src/routes/app/upload/+page.server.ts +++ b/src/routes/app/upload/+page.server.ts @@ -1,15 +1,6 @@ import type { Actions } from "./$types"; -import type { - Access, - Document, - DocumentUpload, - OCREngine, - Project, -} from "$lib/api/types"; -import { CSRF_COOKIE_NAME, DEFAULT_LANGUAGE } from "@/config/config.js"; -import * as documents from "$lib/api/documents"; -import { unwrap } from "$lib/components/inputs/Select.svelte"; +import { CSRF_COOKIE_NAME } from "@/config/config.js"; import { upload } from "$lib/components/forms/DocumentUpload.svelte"; export function load({ cookies }) { @@ -23,6 +14,6 @@ export const actions = { const csrf_token = cookies.get(CSRF_COOKIE_NAME); const form = await request.formData(); - return upload(form, csrf_token, fetch); + return upload(form, csrf_token, null, fetch); }, } satisfies Actions; diff --git a/src/routes/documents/[id]-[slug]/+layout.svelte b/src/routes/documents/[id]-[slug]/+layout.svelte index cdc784e3f..4f764f782 100644 --- a/src/routes/documents/[id]-[slug]/+layout.svelte +++ b/src/routes/documents/[id]-[slug]/+layout.svelte @@ -15,7 +15,7 @@ import Sections from "./sidebar/Sections.svelte"; import { embedUrl } from "@/api/embed"; - import { pageImageUrl } from "@/api/viewer.js"; + import { pageImageUrl } from "@/api/viewer"; import { canonicalUrl } from "@/lib/api/documents"; export let data; diff --git a/src/test/fixtures/documents.ts b/src/test/fixtures/documents.ts index e76b3eacc..5a6fb48e1 100644 --- a/src/test/fixtures/documents.ts +++ b/src/test/fixtures/documents.ts @@ -1267,3 +1267,79 @@ export const documentsList: Page<Document> = { }, ], }; + +export const documentExpanded: Document = { + id: 2622, + access: "public", + admin_noindex: false, + asset_url: "https://s3.documentcloud.org/", + canonical_url: + "https://www.documentcloud.org/documents/2622-agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government", + created_at: "2010-05-12T15:44:15.278766Z", + data: {}, + description: + "<p>This agreement between Conservatives and Liberal Democrats makes David Cameron the new prime minister and installs Nick Clegg, whose party came in third in last week's election, as his deputy. The document details the long list of compromises between the two parties, who don't share much in terms of ideology but need each other to form a government.</p>\n\n<p>Here, the NewsHour's Simon Marks reads between the lines to explain who won which battles and what each side is giving up.</p>", + edit_access: false, + file_hash: "", + noindex: false, + language: "eng", + organization: { + id: 60, + avatar_url: "", + individual: false, + name: "NewsHour", + slug: "newshour", + uuid: "23f98aa7-92d4-4edb-bf18-ccf362b29bdf", + }, + original_extension: "pdf", + page_count: 7, + page_spec: "595.0x842.0:0-6", + projects: [ + { + id: 200, + created_at: "2020-09-25T13:59:43.519966Z", + description: "", + edit_access: false, + add_remove_access: false, + private: false, + slug: "british-election", + title: "British Election", + updated_at: "2020-09-25T13:59:43.526790Z", + user: 126, + }, + { + id: 6909, + created_at: "2020-10-28T17:41:54.481623Z", + description: "Documents to use for testing", + edit_access: false, + add_remove_access: false, + private: false, + slug: "test-project", + title: "Test project", + updated_at: "2024-02-14T15:31:38.770237Z", + user: 1020, + }, + ], + publish_at: null, + published_url: "", + related_article: + "http://www.pbs.org/newshour/rundown/2010/05/the-conservative-libdem-agreement-reading-between-the-lines.html", + revision_control: false, + slug: "agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government", + source: "", + status: "success", + title: + "Agreement between Conservatives and Liberal Democrats to form a Coalition Government", + updated_at: "2024-02-14T15:32:52.849946Z", + user: { + id: 126, + avatar_url: "", + name: "Chris Amico", + organization: 14187, + organizations: [14187], + admin_organizations: [14187], + username: "ChrisAmico_lSozDZNW", + uuid: "caea06a2-ee1c-43e6-865a-a09078522bf1", + verified_journalist: true, + }, +}; diff --git a/vite.config.js b/vite.config.js index 7dc6fbed8..0188dc3ad 100644 --- a/vite.config.js +++ b/vite.config.js @@ -50,6 +50,7 @@ export default defineConfig({ ], environment: "jsdom", coverage: { + provider: "v8", include: ["src/lib/**", "src/routes/**"], exclude: ["src/**/*.stories.@(js|jsx|ts|tsx|svelte)"], reporter: ["text", "html", "lcov", "clover", "json", "json-summary"],