diff --git a/src/addons/AddOnPin.svelte b/src/addons/AddOnPin.svelte index a6114cd96..aed8124e0 100644 --- a/src/addons/AddOnPin.svelte +++ b/src/addons/AddOnPin.svelte @@ -52,4 +52,9 @@ } - + diff --git a/src/addons/browser/AddOnList.svelte b/src/addons/browser/AddOnList.svelte index da65f3d1c..602773eea 100644 --- a/src/addons/browser/AddOnList.svelte +++ b/src/addons/browser/AddOnList.svelte @@ -8,10 +8,10 @@ import ListItem from "./AddOnListItem.svelte"; import type { AddOnListItem } from "../types"; - export let items: AddOnListItem[]; - export let loading: boolean; - export let error: string | undefined; - export let reload: () => void | undefined; + export let items: AddOnListItem[] = []; + export let loading: boolean = false; + export let error: string | undefined = undefined; + export let reload: () => void | undefined = undefined; $: empty = !(items && items.length > 0); diff --git a/src/api/types/addons.ts b/src/api/types/addons.ts new file mode 100644 index 000000000..f3c8e51b5 --- /dev/null +++ b/src/api/types/addons.ts @@ -0,0 +1,11 @@ +import type { PageParams } from "./common"; + +export interface AddOnParams extends PageParams { + query?: string; + active?: boolean; + default?: boolean; + featured?: boolean; + premium?: boolean; + category?: string; + repository?: string; +} diff --git a/src/api/types/common.ts b/src/api/types/common.ts index 1462a184f..5212c561c 100644 --- a/src/api/types/common.ts +++ b/src/api/types/common.ts @@ -9,3 +9,8 @@ export interface Page { results: T[]; escaped?: boolean; } + +export interface PageParams { + cursor?: string; + per_page?: number; +} diff --git a/src/common/icons/Pin.svelte b/src/common/icons/Pin.svelte index 28e3f06ec..969d2e7c3 100644 --- a/src/common/icons/Pin.svelte +++ b/src/common/icons/Pin.svelte @@ -16,5 +16,6 @@ svg { display: block; transform: rotate(-45deg); + fill: var(--fill, var(--orange, #ec7b6b)); } diff --git a/src/common/icons/Premium.svelte b/src/common/icons/Premium.svelte index dcb094465..00b50b386 100644 --- a/src/common/icons/Premium.svelte +++ b/src/common/icons/Premium.svelte @@ -2,15 +2,9 @@ const REM = 16; export let size = 1; export let title = "Premium"; - export let color = "#24CC99"; - + {title} svg { display: block; + fill: var(--fill, var(--green, #27c6a2)); } diff --git a/src/lib/api/addons.test.ts b/src/lib/api/addons.test.ts new file mode 100644 index 000000000..7a5cb95a8 --- /dev/null +++ b/src/lib/api/addons.test.ts @@ -0,0 +1,95 @@ +import { vi, test, describe, it, expect, beforeEach, afterEach } from "vitest"; +import * as addons from "./addons"; +import { BASE_API_URL } from "@/config/config"; +import { addonsList } from "@/test/fixtures/addons"; +import { emptyList } from "@/test/fixtures/common"; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("getAddons", () => { + let mockFetch; + beforeEach(() => { + mockFetch = vi + .fn() + .mockResolvedValue({ ok: true, json: async () => addonsList }); + }); + it("calls the addons API endpoint", async () => { + await addons.getAddons({}, mockFetch); + const expectedEndpoint = new URL(`addons`, BASE_API_URL); + expect(mockFetch).toHaveBeenCalledWith( + expectedEndpoint, + expect.any(Object), + ); + }); + it("passes parameters through as query arguments", async () => { + await addons.getAddons( + { active: true, featured: true, query: "foobar" }, + mockFetch, + ); + const expectedEndpoint = new URL(`addons`, BASE_API_URL); + expectedEndpoint.searchParams.set("active", "true"); + expectedEndpoint.searchParams.set("featured", "true"); + expectedEndpoint.searchParams.set("query", "foobar"); + expect(mockFetch).toHaveBeenCalledWith( + expectedEndpoint, + expect.any(Object), + ); + }); + it("returns the full list", async () => { + const response = await addons.getAddons({}, mockFetch); + expect(response).toBe(addonsList); + }); + it("calls SvelteKit's error fn given a response error", async () => { + mockFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: "Server Error!", + }); + await expect(addons.getAddons({}, mockFetch)).rejects.toThrowError(); + }); +}); + +test("getPinnedAddons", async () => { + const mockFetch = vi + .fn() + .mockResolvedValue({ ok: true, json: async () => {} }); + await addons.getPinnedAddons(mockFetch); + expect(mockFetch).toHaveBeenCalledWith( + new URL(`addons?active=true`, BASE_API_URL), + expect.any(Object), + ); +}); + +describe("getAddon", async () => { + let mockFetch; + beforeEach(() => { + mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => addonsList, + }); + }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("calls the addons list endpoint with a respository query argument", async () => { + await addons.getAddon("MuckRock", "addon-repo", mockFetch); + expect(mockFetch).toHaveBeenCalledWith( + new URL(`addons?repository=MuckRock%2Faddon-repo`, BASE_API_URL), + expect.any(Object), + ); + }); + it("returns the first result in the addon list", async () => { + const response = await addons.getAddon("MuckRock", "addon-repo", mockFetch); + expect(response).toEqual(addonsList.results[0]); + }); + it("returns null if the returned list is empty", async () => { + mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => emptyList, + }); + const response = await addons.getAddon("MuckRock", "addon-repo", mockFetch); + expect(response).toEqual(null); + }); +}); diff --git a/src/lib/api/addons.ts b/src/lib/api/addons.ts index 73ebb9a37..a46e65894 100644 --- a/src/lib/api/addons.ts +++ b/src/lib/api/addons.ts @@ -1,32 +1,51 @@ import type { Page } from "@/api/types/common"; +import type { AddOnParams } from "@/api/types/addons"; import type { AddOnListItem } from "@/addons/types"; - +import { BASE_API_URL } from "@/config/config"; +import { isErrorCode } from "../utils/api"; import { error } from "@sveltejs/kit"; -import { BASE_API_URL } from "@/config/config.js"; -import { isErrorCode } from "../utils"; +export const CATEGORIES = [ + ["ai", "AI"], + ["statistical", "Analyze"], + ["bulk", "Bulk"], + ["export", "Export"], + ["extraction", "Extract"], + ["file", "File"], + ["monitor", "Monitor"], +]; -export async function getPinnedAddons( +export async function getAddons( + params: AddOnParams = {}, fetch = globalThis.fetch, ): Promise> { - const endpoint = new URL( - "/api/addons/?active=true&per_page=100", - BASE_API_URL, - ); + const endpoint = new URL("addons", BASE_API_URL); + Object.entries(params).forEach(([key, value]) => { + endpoint.searchParams.set(key, String(value)); + }); const resp = await fetch(endpoint, { credentials: "include" }); if (isErrorCode(resp.status)) { error(resp.status, resp.statusText); } - return resp.json() as Promise>; + return resp.json(); } -export async function getAddons( +export async function getPinnedAddons( fetch = globalThis.fetch, ): Promise> { - const endpoint = new URL("/api/addons/?per_page=100", BASE_API_URL); - const resp = await fetch(endpoint, { credentials: "include" }); - if (isErrorCode(resp.status)) { - error(resp.status, resp.statusText); + return getAddons({ active: true }, fetch); +} + +export async function getAddon( + owner: string, + repo: string, + fetch = globalThis.fetch, +): Promise { + const repository = [owner, repo].join("/"); + const addons = await getAddons({ repository }, fetch); + // there should only be one result, if the addon exists + if (addons.results.length < 1) { + return null; } - return resp.json() as Promise>; + return addons.results[0]; } diff --git a/src/lib/components/ContentLayout.svelte b/src/lib/components/ContentLayout.svelte index 3341a8a1b..35c9beda9 100644 --- a/src/lib/components/ContentLayout.svelte +++ b/src/lib/components/ContentLayout.svelte @@ -30,6 +30,7 @@ width: 100%; position: sticky; top: 0; + z-index: 2; padding: 0.625rem; } main { @@ -37,13 +38,14 @@ width: 100%; display: flex; flex-direction: column; - justify-content: center; + justify-content: flex-start; } footer { flex: 0 0 0; width: 100%; position: sticky; bottom: 0; + z-index: 2; padding: 0.625rem; } diff --git a/src/lib/components/addons/AddOnListItem.svelte b/src/lib/components/addons/AddOnListItem.svelte new file mode 100644 index 000000000..ba011a294 --- /dev/null +++ b/src/lib/components/addons/AddOnListItem.svelte @@ -0,0 +1,125 @@ + + + +
+
+
+ +
+
+

{addon.name}

+
+ +
+ {#if description} +
{@html description}
+ {/if} +
+ + + diff --git a/src/lib/components/addons/AddOnsNavigation.svelte b/src/lib/components/addons/AddOnsNavigation.svelte new file mode 100644 index 000000000..a2f7285e6 --- /dev/null +++ b/src/lib/components/addons/AddOnsNavigation.svelte @@ -0,0 +1,66 @@ + + + +
+

{$_("addonBrowserDialog.title")}

+

{$_("addonBrowserDialog.subtitle")}

+
+ + + + All + + + + Pinned + + + + Featured + + + + Premium + + + + Collections + {#each CATEGORIES as [key, label]} + + + {label} + + {/each} + +
diff --git a/src/lib/components/addons/stories/AddOnListItem.stories.svelte b/src/lib/components/addons/stories/AddOnListItem.stories.svelte new file mode 100644 index 000000000..b2530b97c --- /dev/null +++ b/src/lib/components/addons/stories/AddOnListItem.stories.svelte @@ -0,0 +1,66 @@ + + + + + + +Edit Document Data* feature to add a key value pair with the category you want, with about five examples of documents that match a category and five that don't. For example, you might add a key of *Email* with five documents set to *True* and five set to *False*. Since *Email* is the value you're training on, set it as the *Value to Train* below. SideKick is a binary classifier — it will try to rank all documents on a spectrum from -1 to 1 based on how likely it believe the document is to be that value.", + }, + }, + }} +/> +Heading

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

", + }, + }, + }} +/> + diff --git a/src/lib/components/addons/stories/AddOnsNavigation.stories.svelte b/src/lib/components/addons/stories/AddOnsNavigation.stories.svelte new file mode 100644 index 000000000..fc3a9a5f8 --- /dev/null +++ b/src/lib/components/addons/stories/AddOnsNavigation.stories.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/common/Empty.svelte b/src/lib/components/common/Empty.svelte index 82dbae30f..88b42311d 100644 --- a/src/lib/components/common/Empty.svelte +++ b/src/lib/components/common/Empty.svelte @@ -13,6 +13,7 @@ .container { display: flex; padding: 1.5rem; + flex: 1 0 0; flex-direction: column; align-items: center; justify-content: center; diff --git a/src/lib/components/common/Tip.svelte b/src/lib/components/common/Tip.svelte new file mode 100644 index 000000000..9f8566db0 --- /dev/null +++ b/src/lib/components/common/Tip.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/src/lib/components/common/stories/Tip.stories.svelte b/src/lib/components/common/stories/Tip.stories.svelte new file mode 100644 index 000000000..e5b6af04a --- /dev/null +++ b/src/lib/components/common/stories/Tip.stories.svelte @@ -0,0 +1,48 @@ + + + + All dogs go to heaven + + + + + + Pinned items will appear here. + + + + + + + This feature is for premium users. + + + + + + + Learn about all the interesting facts + + diff --git a/src/lib/components/Search.svelte b/src/lib/components/inputs/Search.svelte similarity index 90% rename from src/lib/components/Search.svelte rename to src/lib/components/inputs/Search.svelte index a6e59aa50..886392cef 100644 --- a/src/lib/components/Search.svelte +++ b/src/lib/components/inputs/Search.svelte @@ -1,6 +1,7 @@ DocumentCloud - - - - - - - - - - - - - - - - - - + diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index 8ea406a44..66cb3ba50 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -1,6 +1,6 @@ - - - - - {#await searchResults} - Loading… - {:then results} - - {/await} + + + + + + + - - + + + + + + {#await searchResults} + Loading… + {:then results} + + {/await} - - {#if $visible && $total} - Showing {$visible.size.toLocaleString()} of {$total.toLocaleString()} results - {/if} - - - + + + + + {#if $visible && $total} + Showing {$visible.size.toLocaleString()} of {$total.toLocaleString()} + results + {/if} + + + + + + + + + + + + + diff --git a/src/routes/app/add-ons/+page.ts b/src/routes/app/add-ons/+page.ts index e69de29bb..788335f94 100644 --- a/src/routes/app/add-ons/+page.ts +++ b/src/routes/app/add-ons/+page.ts @@ -0,0 +1,9 @@ +import { getAddons } from "@/lib/api/addons.js"; + +export async function load({ url, fetch }) { + const params = Object.fromEntries(url.searchParams.entries()); + const addons = getAddons(params, fetch); + return { + addons, + }; +} diff --git a/src/routes/app/add-ons/[owner]/[repo]/+layout.ts b/src/routes/app/add-ons/[owner]/[repo]/+layout.ts new file mode 100644 index 000000000..d7732afd2 --- /dev/null +++ b/src/routes/app/add-ons/[owner]/[repo]/+layout.ts @@ -0,0 +1,18 @@ +import { breadcrumbTrail } from "$lib/utils/navigation"; +import { getAddon } from "@/lib/api/addons.js"; +import { error } from "@sveltejs/kit"; + +export async function load({ url, params, fetch, parent }) { + const { owner, repo } = params; + const addon = await getAddon(owner, repo, fetch); + if (!addon) { + return error(404, "Add-On Not Found"); + } + const breadcrumbs = await breadcrumbTrail(parent, [ + { href: url.pathname, title: addon.name }, + ]); + return { + addon, + breadcrumbs, + }; +} diff --git a/src/routes/app/add-ons/[owner]/[repo]/+page.svelte b/src/routes/app/add-ons/[owner]/[repo]/+page.svelte index e69de29bb..f7cb5f44b 100644 --- a/src/routes/app/add-ons/[owner]/[repo]/+page.svelte +++ b/src/routes/app/add-ons/[owner]/[repo]/+page.svelte @@ -0,0 +1,11 @@ + + + + +

{data.addon.name}

+
+
diff --git a/src/routes/app/sidebar/AddOns.svelte b/src/routes/app/sidebar/AddOns.svelte index 778d6fabd..72b0f54e7 100644 --- a/src/routes/app/sidebar/AddOns.svelte +++ b/src/routes/app/sidebar/AddOns.svelte @@ -9,6 +9,7 @@ import SidebarItem from "$lib/components/sidebar/SidebarItem.svelte"; import SidebarGroup from "$lib/components/sidebar/SidebarGroup.svelte"; import Pin from "@/common/Pin.svelte"; + import { getPinnedAddons } from "@/lib/api/addons"; export let pinnedAddOns: Promise>;