-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #505 from MuckRock/sveltekit-manager-addons
[SvelteKit] Add-On Pages
- Loading branch information
Showing
29 changed files
with
761 additions
and
283 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,5 +16,6 @@ | |
svg { | ||
display: block; | ||
transform: rotate(-45deg); | ||
fill: var(--fill, var(--orange, #ec7b6b)); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Page<AddOnListItem>> { | ||
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<Page<AddOnListItem>>; | ||
return resp.json(); | ||
} | ||
|
||
export async function getAddons( | ||
export async function getPinnedAddons( | ||
fetch = globalThis.fetch, | ||
): Promise<Page<AddOnListItem>> { | ||
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<AddOnListItem | null> { | ||
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<Page<AddOnListItem>>; | ||
return addons.results[0]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
<script lang="ts"> | ||
import type { AddOnListItem } from "@/addons/types"; | ||
import { _ } from "svelte-i18n"; | ||
import AddOnPin from "@/addons/AddOnPin.svelte"; | ||
import AddOnPopularity from "@/addons/Popularity.svelte"; | ||
import PremiumBadge from "@/premium-credits/PremiumBadge.svelte"; | ||
export let addon: AddOnListItem; | ||
$: url = `/app/add-ons/${addon?.repository}`; | ||
$: description = addon?.parameters?.description; | ||
$: author = { name: addon?.repository?.split("/")[0] }; | ||
$: isPremium = addon?.parameters?.categories?.includes("premium") ?? false; | ||
</script> | ||
|
||
<a class="addon-link" href={url}> | ||
<div class="container" id={addon.repository}> | ||
<div class="row"> | ||
<div class="center-self"> | ||
<AddOnPin {addon} /> | ||
</div> | ||
<div class="stretch"> | ||
<h3 class="addon-name">{addon.name}</h3> | ||
</div> | ||
<div class="metadata"> | ||
{#if author?.name} | ||
<p class="author"> | ||
<a | ||
href="http://github.com/{addon.repository}" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
title={$_("addonBrowserDialog.viewsource")}>{author.name}</a | ||
> | ||
</p> | ||
{/if} | ||
{#if addon.usage} | ||
<AddOnPopularity useCount={addon.usage} /> | ||
{/if} | ||
{#if isPremium} | ||
<span class="badge"><PremiumBadge /></span> | ||
{/if} | ||
</div> | ||
</div> | ||
{#if description} | ||
<div class="description">{@html description}</div> | ||
{/if} | ||
</div> | ||
</a> | ||
|
||
<style> | ||
a { | ||
color: inherit; | ||
text-decoration: none; | ||
} | ||
.container { | ||
display: block; | ||
min-width: 12rem; | ||
padding: 0.5rem 0.5rem 0.75rem; | ||
text-align: left; | ||
} | ||
.addon-link:hover .container { | ||
background-color: var(--blue-1); | ||
} | ||
.row { | ||
display: flex; | ||
align-items: flex-end; | ||
gap: 0.5rem; | ||
margin: 0.5rem; | ||
} | ||
.badge { | ||
margin-bottom: -0.25em; | ||
font-size: 0.8em; | ||
} | ||
.metadata { | ||
display: flex; | ||
align-items: flex-end; | ||
gap: 1rem; | ||
color: var(--gray-4); | ||
} | ||
.description { | ||
margin: 0 0.5em; | ||
opacity: 0.6; | ||
font-size: 0.875em; | ||
line-height: 1.4; | ||
color: var(--gray-4); | ||
overflow: hidden; | ||
-webkit-line-clamp: 4; | ||
display: -webkit-box; | ||
-webkit-box-orient: vertical; | ||
& > * { | ||
margin-top: 0; | ||
font-size: 0.875rem; | ||
} | ||
} | ||
.addon-name { | ||
margin: 0; | ||
font-weight: 600; | ||
font-size: var(--font-l); | ||
} | ||
.center-self { | ||
align-self: center; | ||
} | ||
.stretch { | ||
flex: 1 1 auto; | ||
} | ||
.author a:hover { | ||
opacity: 0.7; | ||
} | ||
p { | ||
margin: 0; | ||
} | ||
</style> |
Oops, something went wrong.