diff --git a/src/addons/Drawer.svelte b/src/addons/Drawer.svelte index 5d28c54ea..79439412a 100644 --- a/src/addons/Drawer.svelte +++ b/src/addons/Drawer.svelte @@ -73,7 +73,7 @@ button.close { margin: 0; - top: 1em; + top: 1.25em; position: absolute; border-radius: 9999px; border: transparent; diff --git a/src/addons/browser/AddOnListItem.svelte b/src/addons/browser/AddOnListItem.svelte index 7c7afd567..ae5e57be3 100644 --- a/src/addons/browser/AddOnListItem.svelte +++ b/src/addons/browser/AddOnListItem.svelte @@ -4,15 +4,15 @@ import AddOnPin from "../AddOnPin.svelte"; import AddOnPopularity from "../Popularity.svelte"; import type { AddOnListItem } from "../types.js"; + import Credit from "../../common/icons/Credit.svelte"; + import Badge from "../../common/Badge.svelte"; export let addon: AddOnListItem; - $: description = addon.parameters?.description; - $: if (!addon?.author) { - addon.author = { name: addon?.repository?.split("/")[0] }; - } - - $: url = `#add-ons/${addon.repository}`; + $: description = addon?.parameters?.description; + $: author = { name: addon?.repository?.split("/")[0] }; + $: url = `#add-ons/${addon?.repository}`; + $: isPremium = addon?.parameters?.categories?.includes("premium") ?? false; @@ -225,6 +233,11 @@

{$_("addonBrowserDialog.featuredTip")}

+ {:else if $filter === "premium"} + {/if} diff --git a/src/addons/browser/Filters.svelte b/src/addons/browser/Filters.svelte index 7d81fb107..fa74f660d 100644 --- a/src/addons/browser/Filters.svelte +++ b/src/addons/browser/Filters.svelte @@ -6,6 +6,7 @@ ["all", "All"], ["active", "Pinned"], ["featured", "Featured"], + ["premium", "Premium"], ]; export const CATEGORIES = [ @@ -26,6 +27,7 @@ import Pin from "../../common/icons/Pin.svelte"; import Star from "../../common/icons/Star.svelte"; import Infinity from "svelte-octicons/lib/Infinity16.svelte"; + import Credit from "../../common/icons/Credit.svelte";

{$_("addonBrowserDialog.categories")}

diff --git a/src/addons/browser/stories/AddOnListItem.stories.svelte b/src/addons/browser/stories/AddOnListItem.stories.svelte index da17ea7ac..83dd12775 100644 --- a/src/addons/browser/stories/AddOnListItem.stories.svelte +++ b/src/addons/browser/stories/AddOnListItem.stories.svelte @@ -43,3 +43,12 @@ }, }} /> + diff --git a/src/addons/dispatch/Dispatch.svelte b/src/addons/dispatch/Dispatch.svelte index e8b80f73b..685e41e8c 100644 --- a/src/addons/dispatch/Dispatch.svelte +++ b/src/addons/dispatch/Dispatch.svelte @@ -14,11 +14,17 @@ import Selection from "./Selection.svelte"; import ScheduledInset from "./ScheduledInset.svelte"; import { eventValues, schedules } from "../runs/ScheduledEvent.svelte"; - import { baseApiUrl } from "../../api/base.js"; import { getCsrfToken } from "../../api/session.js"; import { pushToast } from "../../common/Toast.svelte"; import { runs } from "../progress/AddonRun.svelte"; + import Premium from "./Premium.svelte"; + + import { + orgsAndUsers, + isPremiumOrg, + getCreditBalance, + } from "../../manager/orgsAndUsers"; export let visible: boolean = false; export let addon: AddOnListItem; @@ -40,6 +46,12 @@ reset(); } + $: isPremiumUser = isPremiumOrg($orgsAndUsers?.me?.organization); + $: creditBalance = getCreditBalance($orgsAndUsers?.me?.organization) ?? 0; + $: isPremiumAddon = + addon?.parameters.categories?.includes("premium") ?? false; + $: disablePremium = isPremiumAddon && (!isPremiumUser || creditBalance === 0); + onMount(() => { if (event) { $values = { @@ -54,7 +66,7 @@ let qs = new URLSearchParams(window.location.search); // only accept values in properties - const { properties } = addon.parameters; + const { properties } = addon?.parameters ?? {}; const values = Object.fromEntries( Array.from(qs).filter(([k, v]) => properties.hasOwnProperty(k)), ); @@ -289,18 +301,28 @@ + +
{#if event} -
diff --git a/src/addons/dispatch/Form.svelte b/src/addons/dispatch/Form.svelte index 270e215fb..2d5b9ccf9 100644 --- a/src/addons/dispatch/Form.svelte +++ b/src/addons/dispatch/Form.svelte @@ -124,7 +124,9 @@ {/if} - + + +
diff --git a/src/addons/dispatch/Header.svelte b/src/addons/dispatch/Header.svelte index 80f682ae5..63bd113ac 100644 --- a/src/addons/dispatch/Header.svelte +++ b/src/addons/dispatch/Header.svelte @@ -9,10 +9,13 @@ import { pushToast } from "../../common/Toast.svelte"; import AddOnPin from "../AddOnPin.svelte"; + import Badge from "../../common/Badge.svelte"; + import Credit from "../../common/icons/Credit.svelte"; export let addon: AddOnListItem; - $: author = addon.author || addon.repository.split("/")[0]; + $: author = addon.repository.split("/")[0]; + $: isPremium = addon?.parameters.categories?.includes("premium") ?? false; async function onShare() { try { @@ -54,18 +57,22 @@ .name { flex: 1 1 100%; display: flex; - align-items: flex-start; - gap: 1rem; + align-items: baseline; + gap: 1em; } .pin { flex: 0 1 auto; + transform: translateY(0.15rem); } .name h2 { margin: 0; + flex: 1 1 auto; } .metadata { + flex: 1 1 auto; display: flex; gap: 1em; + align-items: flex-end; margin: 0; } .metadata dt { @@ -83,10 +90,25 @@ display: inline-block; font-weight: 600; } + .categories { + flex: 1 1 auto; + } + .premium { + flex: 0 1 auto; + justify-self: flex-end; + } .actions { display: flex; flex-direction: row; - gap: 1em; + flex-wrap: wrap; + gap: 0 1em; + } + .actions.jB { + flex: 1 1 auto; + justify-content: space-between; + } + .actions.padRight { + margin-right: 3rem; } .author dd { padding: 0.1em 0; @@ -101,6 +123,7 @@ padding: 0.1em 0.2em; border-radius: var(--radius); transform: translateX(-0.2em); + text-transform: capitalize; } .category:hover { background: rgba(0, 0, 0, 0.1); @@ -108,10 +131,21 @@
- +
+ +
+ + +
+
@@ -125,30 +159,32 @@
- {#if addon.categories} + {#if addon?.parameters?.categories}
{$_("addonDispatchDialog.categories")}
- {#each addon.categories as category} -
- - {category} - -
+ {#each addon.parameters.categories as category} + {#if category !== "premium"} +
+ + {category} + +
+ {/if} {/each} {/if}
+ {#if isPremium} + + {/if} -
- - -
-
- {@html addon.parameters.description} + {@html addon?.parameters?.description}
diff --git a/src/addons/dispatch/Premium.svelte b/src/addons/dispatch/Premium.svelte new file mode 100644 index 000000000..7d1950aae --- /dev/null +++ b/src/addons/dispatch/Premium.svelte @@ -0,0 +1,219 @@ + + + + +{#if isPremium} + {#if isPremiumOrg(user?.organization)} +
+ {$_("addonDispatchDialog.premium")} +
+
+ {#if amount} +

+ {$_("addonDispatchDialog.cost", { + values: { amount: amount, unit: unit, price: price || 1}, + })} +

+ {/if} + +
+
+
+
Your credit balance
+
+
+ +
+
+
+ {:else if isOrgAdmin(user)} + triggerPremiumUpgradeFlow(user?.organization)} + /> + {:else} + + {/if} +{/if} diff --git a/src/addons/dispatch/stories/Dispatch.stories.svelte b/src/addons/dispatch/stories/Dispatch.stories.svelte index a78b02560..a5b8f202b 100644 --- a/src/addons/dispatch/stories/Dispatch.stories.svelte +++ b/src/addons/dispatch/stories/Dispatch.stories.svelte @@ -97,3 +97,5 @@ + + diff --git a/src/addons/dispatch/stories/Premium.stories.svelte b/src/addons/dispatch/stories/Premium.stories.svelte new file mode 100644 index 000000000..2e3c9a06b --- /dev/null +++ b/src/addons/dispatch/stories/Premium.stories.svelte @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + diff --git a/src/addons/fixtures/addon-list.json b/src/addons/fixtures/addon-list.json index 4e0374f9a..88caca6f1 100644 --- a/src/addons/fixtures/addon-list.json +++ b/src/addons/fixtures/addon-list.json @@ -106,6 +106,10 @@ "required": ["access_level", "output_lang", "input_lang"], "documents": ["selected"], "categories": ["ai", "premium", "file"], + "cost": { + "unit": "character", + "amount": 75 + }, "properties": { "dry_run": { "type": "boolean", diff --git a/src/addons/fixtures/addons.json b/src/addons/fixtures/addons.json index 9aded61c5..eae9e4169 100644 --- a/src/addons/fixtures/addons.json +++ b/src/addons/fixtures/addons.json @@ -237,5 +237,391 @@ "active": false, "default": false, "featured": false + }, + { + "id": 455, + "user": 100000, + "organization": 10001, + "access": "public", + "name": "Translate Documents", + "repository": "MuckRock/google-translate-addon", + "parameters": { + "type": "object", + "title": "Translate Documents", + "required": ["access_level", "output_lang", "input_lang"], + "documents": ["selected"], + "cost": { + "amount": 150, + "unit": "characters" + }, + "categories": ["ai", "premium", "file"], + "properties": { + "dry_run": { + "type": "boolean", + "title": "Dry Run", + "description": "Select this to calculate the cost of running this translation, it won't translate the text if selected." + }, + "input_lang": { + "enum": [ + "af", + "sq", + "am", + "ar", + "hy", + "as", + "ay", + "az", + "bm", + "eu", + "be", + "bn", + "bho", + "bs", + "bg", + "ca", + "ceb", + "zh-CN", + "zh-TW", + "co", + "hr", + "cs", + "da", + "dv", + "doi", + "nl", + "en", + "eo", + "et", + "ee", + "fil", + "fi", + "fr", + "fy", + "gl", + "ka", + "de", + "el", + "gn", + "gu", + "ht", + "ha", + "haw", + "he", + "hi", + "hmn", + "hu", + "is", + "ig", + "ilo", + "id", + "ga", + "it", + "ja", + "jv", + "kn", + "kk", + "km", + "rw", + "gom", + "ko", + "kri", + "ku", + "ckb", + "ky", + "lo", + "la", + "lv", + "ln", + "lt", + "lg", + "lb", + "mk", + "mai", + "mg", + "ms", + "ml", + "mt", + "mi", + "mr", + "mni-Mtei", + "lus", + "mn", + "my", + "ne", + false, + "ny", + "or", + "om", + "ps", + "fa", + "pl", + "pt", + "pa", + "qu", + "ro", + "ru", + "sm", + "sa", + "gd", + "nso", + "sr", + "st", + "sn", + "sd", + "si", + "sk", + "sl", + "so", + "es", + "su", + "sw", + "sv", + "tl", + "tg", + "ta", + "tt", + "te", + "th", + "ti", + "ts", + "tr", + "tk", + "ak", + "uk", + "ur", + "ug", + "uz", + "vi", + "cy", + "xh", + "yi", + "yo", + "zu" + ], + "type": "string", + "title": "Input language code", + "default": "en" + }, + "project_id": { + "type": "integer", + "title": "Project ID", + "description": "(Optional) Project ID of project you want to upload the translations to." + }, + "output_lang": { + "enum": [ + "af", + "sq", + "am", + "ar", + "hy", + "as", + "ay", + "az", + "bm", + "eu", + "be", + "bn", + "bho", + "bs", + "bg", + "ca", + "ceb", + "zh-CN", + "zh-TW", + "co", + "hr", + "cs", + "da", + "dv", + "doi", + "nl", + "en", + "eo", + "et", + "ee", + "fil", + "fi", + "fr", + "fy", + "gl", + "ka", + "de", + "el", + "gn", + "gu", + "ht", + "ha", + "haw", + "he", + "hi", + "hmn", + "hu", + "is", + "ig", + "ilo", + "id", + "ga", + "it", + "ja", + "jv", + "kn", + "kk", + "km", + "rw", + "gom", + "ko", + "kri", + "ku", + "ckb", + "ky", + "lo", + "la", + "lv", + "ln", + "lt", + "lg", + "lb", + "mk", + "mai", + "mg", + "ms", + "ml", + "mt", + "mi", + "mr", + "mni-Mtei", + "lus", + "mn", + "my", + "ne", + false, + "ny", + "or", + "om", + "ps", + "fa", + "pl", + "pt", + "pa", + "qu", + "ro", + "ru", + "sm", + "sa", + "gd", + "nso", + "sr", + "st", + "sn", + "sd", + "si", + "sk", + "sl", + "so", + "es", + "su", + "sw", + "sv", + "tl", + "tg", + "ta", + "tt", + "te", + "th", + "ti", + "ts", + "tr", + "tk", + "ak", + "uk", + "ur", + "ug", + "uz", + "vi", + "cy", + "xh", + "yi", + "yo", + "zu" + ], + "type": "string", + "title": "Output language code", + "default": "es" + }, + "access_level": { + "enum": ["public", "private", "organization"], + "type": "string", + "title": "Access level (public, private, organization) of translations", + "default": "public" + } + }, + "description": "

This Add-On allows you to translate documents using the Google Translate service. Supply a two character ISO 639-1 code for the input and output languages and you will receive your translation in a download as well as it will be uploaded to DocumentCloud. See https://cloud.google.com/translate/docs/languages for supported languages.

" + }, + "created_at": "2023-07-19T19:59:18.086713Z", + "updated_at": "2023-07-19T19:59:18.088614Z", + "active": false, + "default": false, + "featured": false + }, + { + "id": 46, + "user": null, + "organization": null, + "access": "public", + "name": "Azure Document Intelligence OCR", + "repository": "MuckRock/documentcloud-azure-document-intelligence-ocr-addon", + "parameters": { + "cost": { + "unit": "page", + "amount": 1 + }, + "type": "object", + "title": "Azure Document Intelligence OCR", + "documents": ["selected"], + "categories": ["extraction", "premium"], + "properties": {}, + "description": "

This Add-On uses Azure’s Document Intelligence API to OCR documents. The document(s) must be public to be processed. This Add-On uses 1 AI Credit per page.

", + "instructions": "" + }, + "created_at": "2023-09-18T17:09:58.097621Z", + "updated_at": "2023-11-15T16:04:34.442286Z", + "active": false, + "default": false, + "featured": false + }, + { + "id": 360, + "user": 100000, + "organization": 10001, + "access": "public", + "name": "Diakopoulos GPT-3 Example: Classifying Comments Based on Subject", + "repository": "MuckRock/gpt3-classification", + "parameters": { + "type": "object", + "title": "Diakopoulos GPT-3 Example: Classifying Comments Based on Subject", + "documents": ["query", "selected"], + "cost": { + "unit": "document", + "amount": 1, + "price": 14 + }, + "categories": ["ai", "premium"], + "properties": { + "value": { + "type": "string", + "title": "Value Label" + }, + "category": { + "type": "string", + "title": "Describe the category that of records you want labeled as \"True\"" + } + }, + "description": "

A modified implementation of the example using GPT-3 to classify documents, described by Nick Diakopoulos. For simplicity’s sake, it only implements the first half of the classification task described, but allows the user to customize the subject matter. Use with care as it may introduce factual errors, hallucinations, and other artifacts in summaries.

\n

Note that this plugin uses 14 credits per document analyzed, rounded up. Give this a full prompt that you would pass into GPT-3 and it will run it against your selected documents, one at a time. The Add-On only looks at the 12,000 characters of a document. The prompt refers to the user input as the “Assignment,” the document’s text as “Document Text”, and GPT-3’s generated response as the “Answer,” which can be helpful when defining your prompt.

\n

We’d love your feedback and ideas — drop a note to info at documentcloud dot org or join us in the News Nerdery Slack in the #proj-documentcloud channel.

" + }, + "created_at": "2023-07-19T19:59:18.054811Z", + "updated_at": "2023-07-19T19:59:18.057428Z", + "active": false, + "default": false, + "featured": false } ] diff --git a/src/addons/fixtures/premium-addon.json b/src/addons/fixtures/premium-addon.json new file mode 100644 index 000000000..449fd6196 --- /dev/null +++ b/src/addons/fixtures/premium-addon.json @@ -0,0 +1,26 @@ +{ + "id": 46, + "user": null, + "organization": null, + "access": "public", + "name": "Azure Document Intelligence OCR", + "repository": "MuckRock/documentcloud-azure-document-intelligence-ocr-addon", + "parameters": { + "cost": { + "unit": "page", + "amount": 1 + }, + "type": "object", + "title": "Azure Document Intelligence OCR", + "documents": ["selected"], + "categories": ["extraction", "premium"], + "properties": {}, + "description": "

This Add-On uses Azure’s Document Intelligence API to OCR documents. The document(s) must be public to be processed. This Add-On uses 1 AI Credit per page.

", + "instructions": "" + }, + "created_at": "2023-09-18T17:09:58.097621Z", + "updated_at": "2023-11-15T16:04:34.442286Z", + "active": false, + "default": false, + "featured": false +} diff --git a/src/addons/runs/HistoryEvent.svelte b/src/addons/runs/HistoryEvent.svelte index 1e1f9bfc5..8697d5a0c 100644 --- a/src/addons/runs/HistoryEvent.svelte +++ b/src/addons/runs/HistoryEvent.svelte @@ -15,6 +15,7 @@ comment: string; created_at: string; updated_at: string; + credits_spent?: number; } @@ -27,6 +28,7 @@ Question24, Sync24, } from "svelte-octicons"; + import Price from "../../premium-credits/Price.svelte"; export let run; @@ -74,11 +76,14 @@ .unknown-status.icon { fill: var(--gray); } + .row { + display: flex; + } .info { flex: 1 1 auto; - display: flex; - align-items: center; - flex-wrap: wrap; + } + .info .row { + align-items: baseline; justify-content: space-between; } .primary-info { @@ -88,7 +93,7 @@ gap: 0.5em; } .name { - margin: 0; + margin: 0 1rem 0 0; font-weight: 600; } .date { @@ -102,6 +107,11 @@ font-size: 0.8em; font-style: italic; } + .price { + margin: 0.5em 0 0; + font-size: 0.8em; + align-self: flex-end; + }
@@ -121,17 +131,28 @@ {/if}
-
-

{run.addon.name}

- {#if run.file_url}{/if} +
+
+

{run.addon.name}

+ {#if run.file_url}{/if} +
+ +
+
+ {#if run.message} +

{run.message}

+ {/if} + {#if run.credits_spent} +

+ +

+ {/if}
- - {#if run.message}

{run.message}

{/if}
diff --git a/src/addons/runs/ScheduledEvent.svelte b/src/addons/runs/ScheduledEvent.svelte index db6cc05bd..773e9c902 100644 --- a/src/addons/runs/ScheduledEvent.svelte +++ b/src/addons/runs/ScheduledEvent.svelte @@ -32,7 +32,7 @@ export let event: Event; $: disabled = event.event === 0; - $: key = event.addon.parameters?.eventOptions?.name; + $: key = event.addon?.parameters?.eventOptions?.name; $: target = event.parameters[key]; function url(event: Event) { diff --git a/src/addons/runs/stories/HistoryEvent.stories.svelte b/src/addons/runs/stories/HistoryEvent.stories.svelte index 596f2224e..89d780887 100644 --- a/src/addons/runs/stories/HistoryEvent.stories.svelte +++ b/src/addons/runs/stories/HistoryEvent.stories.svelte @@ -21,3 +21,7 @@ + diff --git a/src/addons/types.ts b/src/addons/types.ts index 511971f72..0622877e3 100644 --- a/src/addons/types.ts +++ b/src/addons/types.ts @@ -3,18 +3,50 @@ interface Author { avatar?: string; } +type AddOnCategory = "premium" | string; + +interface AddOnProperty { + type: string; + title: string; + description?: string; + default?: string; + format?: string; + enum?: string[]; +} + +interface AddOnParameters { + type: string; + version: number; + title: string; + description: string; + instructions: string; + categories: AddOnCategory[]; + documents: string[]; + required: string[]; + properties: Record; + cost: { + amount: number; + unit: string; + }; + eventOptions: { + name: string; + events: string[]; + }; +} + // API endpoint https://api.www.documentcloud.org/api/addons/ export interface AddOnListItem { id: number; + user: number; + organization: number; + access: "public" | "private"; name: string; repository: string; - parameters: any; - description?: string; - author?: Author; - usage?: number; - categories: string[]; - documents: string[]; + parameters: Partial; + created_at: string; + updated_at: string; active: boolean; featured: boolean; default: boolean; + usage?: number; } diff --git a/src/api/orgAndUser.js b/src/api/orgAndUser.js index d8f80f47b..edfe92318 100644 --- a/src/api/orgAndUser.js +++ b/src/api/orgAndUser.js @@ -14,14 +14,10 @@ export async function getMe(expand = DEFAULT_EXPAND) { } } // Check that the user is logged in via network request - try { - const { data } = await session.get( - queryBuilder(apiUrl(`users/me/`), { expand }), - ); - return data; - } catch (e) { - return null; - } + const { data } = await session.get( + queryBuilder(apiUrl(`users/me/`), { expand }), + ); + return data; } export async function getUser(id, expand = DEFAULT_EXPAND) { diff --git a/src/common/Badge.svelte b/src/common/Badge.svelte new file mode 100644 index 000000000..ed64c81a0 --- /dev/null +++ b/src/common/Badge.svelte @@ -0,0 +1,35 @@ + + + + +
+ {#if $$slots.icon}{/if} + {label} +
diff --git a/src/common/Button.svelte b/src/common/Button.svelte index 2e9edd2f1..a1886376b 100644 --- a/src/common/Button.svelte +++ b/src/common/Button.svelte @@ -8,6 +8,7 @@ export let small = false; export let secondary = false; export let tertiary = false; + export let premium = false; export let nondescript = false; export let action = false; export let caution = false; @@ -15,6 +16,7 @@ export let disabled = false; export let plain = false; export let nomargin = false; + export let fullWidth = false; export let type: "submit" | "reset" | "button" = "submit"; export let label = "Submit"; @@ -71,6 +73,10 @@ background: var(--tertiary, #0c8a01); } + .premium { + background: var(--premium, #24cc99); + } + .danger { background: var(--caution, #f04c42); } @@ -127,48 +133,56 @@ font-weight: normal; margin: 0 5px; } + + .fullWidth { + display: flex; + width: 100%; + justify-content: center; + } - - - {#if href} - - {label} - - {:else} - - {/if} - - + + {#if href} + + {label} + + {:else} + + {/if} + diff --git a/src/common/Dropdown2.svelte b/src/common/Dropdown2.svelte new file mode 100644 index 000000000..cd04e8fd3 --- /dev/null +++ b/src/common/Dropdown2.svelte @@ -0,0 +1,195 @@ + + + + + + + + + +{#if overlay && isOpen} +
+{/if} + + diff --git a/src/common/Menu.svelte b/src/common/Menu.svelte index 52a80f4ec..5e7c4b68d 100644 --- a/src/common/Menu.svelte +++ b/src/common/Menu.svelte @@ -2,7 +2,6 @@ .menu { @include menu; - border-top-left-radius: 0; border: 1px solid #cdcdcd; padding: 7px 0; diff --git a/src/common/MenuInsert.svelte b/src/common/MenuInsert.svelte new file mode 100644 index 000000000..607319a7c --- /dev/null +++ b/src/common/MenuInsert.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/common/MenuItem.svelte b/src/common/MenuItem.svelte index 9300ae5c8..bb3125023 100644 --- a/src/common/MenuItem.svelte +++ b/src/common/MenuItem.svelte @@ -7,6 +7,7 @@ export let special = false; export let href = null; export let target = null; + export let selected = false; let className = ""; export { className as class }; @@ -26,20 +27,35 @@ } .item { - padding: 6px 21px; - font-size: 16px; + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem 0.5rem 0.75rem; + font-size: 1rem; + font-family: "Source Sans Pro", sans-serif; user-select: none; + white-space: nowrap; + box-sizing: border-box; } + + .item .label { + flex: 1 1 auto; + } + .item.danger { color: var(--caution, #f04c42); + fill: var(--caution, #f04c42); } .item.special { color: var(--searchSpecial, #5a00ff); + fill: var(--searchSpecial, #5a00ff); } .item.primary { color: var(--primary, #4294f0); + fill: var(--primary, #4294f0); } .item.indent { @@ -50,24 +66,29 @@ cursor: pointer; } - .item.selectable:hover { - background: var(--primary, #4294f0); - color: white; - } - - .item.selectable:hover :global(.info), - .item.selectable:hover :global(.scope) { - color: white; - } - .item.disabled { color: var(--gray), rgba(0, 0, 0, 0.53); + fill: var(--gray), rgba(0, 0, 0, 0.53); pointer-events: none; } .item :global(.info) { font-size: 13px; color: var(--gray), rgba(0, 0, 0, 0.53); + fill: var(--gray), rgba(0, 0, 0, 0.53); + } + + .item.selectable:hover, + .item.selected { + background: var(--primary, #4294f0); + color: var(--white, white); + fill: var(--white, white); + } + + .item.selectable:hover :global(.info), + .item.selectable:hover :global(.scope) { + color: white; + fill: white; } @@ -77,6 +98,7 @@ {href} class="item {className}" class:selectable + class:selected class:danger class:primary class:disabled @@ -84,5 +106,7 @@ class:special on:click > - Define an item + + Define an item + {#if selected}{/if} diff --git a/src/common/MenuTitle.svelte b/src/common/MenuTitle.svelte new file mode 100644 index 000000000..be9dd7d20 --- /dev/null +++ b/src/common/MenuTitle.svelte @@ -0,0 +1,31 @@ + + + + + + {#if $$slots.icon}{/if} + {label} + + diff --git a/src/common/UploadOptions.svelte b/src/common/UploadOptions.svelte index 1d5e486a0..f5d009ee0 100644 --- a/src/common/UploadOptions.svelte +++ b/src/common/UploadOptions.svelte @@ -104,7 +104,7 @@ {$_("uploadOptions.creditHelpText", { values: { organization: $orgsAndUsers.me.organization.name, - n: $orgsAndUsers.me.organization.monthly_ai_credits, + n: $orgsAndUsers.me.organization.monthly_credits, }, })}

diff --git a/src/common/icons/Credit.svelte b/src/common/icons/Credit.svelte new file mode 100644 index 000000000..c4c396b9e --- /dev/null +++ b/src/common/icons/Credit.svelte @@ -0,0 +1,37 @@ + + + + + + {title} + {#if badge} + + {:else} + + {/if} + diff --git a/src/common/icons/Help.svelte b/src/common/icons/Help.svelte new file mode 100644 index 000000000..d772b323a --- /dev/null +++ b/src/common/icons/Help.svelte @@ -0,0 +1,20 @@ + + + + + + {title} + + diff --git a/src/common/icons/Language.svelte b/src/common/icons/Language.svelte new file mode 100644 index 000000000..9f6e47f14 --- /dev/null +++ b/src/common/icons/Language.svelte @@ -0,0 +1,24 @@ + + + + + + {title} + + diff --git a/src/common/stories/Badge.demo.svelte b/src/common/stories/Badge.demo.svelte new file mode 100644 index 000000000..69f8136cc --- /dev/null +++ b/src/common/stories/Badge.demo.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/common/stories/Badge.stories.svelte b/src/common/stories/Badge.stories.svelte new file mode 100644 index 000000000..19c55e58c --- /dev/null +++ b/src/common/stories/Badge.stories.svelte @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/src/common/stories/Dropdown.demo.svelte b/src/common/stories/Dropdown.demo.svelte new file mode 100644 index 000000000..beeefc42e --- /dev/null +++ b/src/common/stories/Dropdown.demo.svelte @@ -0,0 +1,67 @@ + + + + + + + + {#each options as option} + closeDropdown("dropdown-1")}>{option} + {/each} + + + + {#each subOptions as subOption} + closeDropdown("dropdown-1-1")} + >{subOption} + {/each} + + + + + + + +
+ Placeholder +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus id + metus vel euismod. Phasellus luctus, orci sit amet vestibulum tincidunt, + purus erat interdum ex, vel bibendum tortor justo sit amet lectus. Fusce + consequat, ligula non viverra auctor, velit nulla elementum lectus, nec + luctus felis libero a quam. In hac habitasse platea dictumst. Vivamus + euismod, purus non tincidunt blandit, erat augue dapibus metus, et aliquam + sapien elit ac orci. Integer ut justo nec justo vestibulum consectetur ut + non nisi. Sed vestibulum eget tortor eget tristique. Nulla facilisi. Cras + eget vehicula quam, id scelerisque ipsum. Nunc sagittis elit vitae justo + viverra, at condimentum lectus tristique. Integer nec malesuada purus. + Nullam eu bibendum libero. +

+
+
diff --git a/src/common/stories/Dropdown.stories.svelte b/src/common/stories/Dropdown.stories.svelte new file mode 100644 index 000000000..874384486 --- /dev/null +++ b/src/common/stories/Dropdown.stories.svelte @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/src/langs/json/en.json b/src/langs/json/en.json index 8210a6fb5..aae1b6a34 100644 --- a/src/langs/json/en.json +++ b/src/langs/json/en.json @@ -350,20 +350,51 @@ "addOnsMenu": "Add-Ons" }, "authSection": { - "help": "Help", - "language": "Language", - "faq": "FAQ", - "searchDocs": "Search Documentation", - "apiDocs": "API Documentation", - "addOns": "Add-Ons", - "premium": "DocumentCloud Premium", - "emailUs": "Email Us", - "acctSettings": "Account settings", - "signOut": "Sign out", - "changeOrg": "Change organization", - "personalAcct": "Personal Account", - "signIn": "Sign in", - "uploadEmail": "Upload via email" + "help": { + "title": "Help", + "faq": "FAQ", + "searchDocs": "Search Documentation", + "apiDocs": "API Documentation", + "addOns": "Add-Ons", + "premium": "DocumentCloud Premium", + "emailUs": "Email Us" + }, + "language": { + "title": "Language" + }, + "user": { + "uploadEmail": "Upload via email", + "acctSettings": "Account settings", + "signOut": "Sign out", + "signIn": "Sign in" + }, + "premiumUpgrade": { + "title": "Premium", + "heading": "Go Pro with DocumentCloud’s premium tools", + "orgHeading": "Power your team with DocumentCloud’s premium tools", + "description": "Upgrade to a Professional plan to search document annotations and access Premium Add-Ons like advanced OCR, GPT-driven document analysis, and more.", + "orgDescription": "Upgrade to an Organization plan to search document annotations and access Premium Add-Ons like advanced OCR, GPT-driven document analysis, and more.", + "docs": "Learn more about DocumentCloud Premium", + "addons": "Explore premium add-ons", + "orgs": "Join or start an organization", + "cta": "Upgrade Plan" + }, + "org": { + "changeOrg": "Change organization", + "personalAcct": "Personal Account", + "adminRole": "Admin", + "memberListError": "An error occurred while loading the member list.", + "memberListEmpty": "This organization has no other members." + }, + "credits": { + "monthlyOrg": "Monthly Org Allowance", + "monthlyPro": "Monthly Pro Allowance", + "refreshOn": "Credit allowance will reset on {date}", + "purchased": "Purchased Credits", + "purchasedHelpText": "Purchased credits never expire and will only be used after you run out of monthly credits.", + "purchaseCreditsButton": "Purchase Credits", + "purchaseCreditsAdminOnly": "Only org admins may purchase additional credits." + } }, "documents": { "yourDocuments": "Your Documents", @@ -706,6 +737,8 @@ "addons": "Add-Ons", "backButton": "Browse Add-Ons", "select": "Documents to run this Add-On against:", + "premium": "This Add-On uses Premium Credits:", + "premiumSpendLimit": "Set a spending limit", "queryNoSelected": "This Add-On will try to run against the {n, plural, one {# document} other {# documents}} currently included in your search results. To run it against only selected documents, cancel this and select some, then select the Add-On again.", "noSelected": "You must select some documents to run against. Cancel this and select some, then select the Add-On again.", "runSelected": "This Add-On will try to run against the {n, plural, one {# currently selected document} other {# currently selected documents}}.", @@ -729,7 +762,13 @@ "scheduleSuccess": "Add-on is now scheduled", "runSuccess": "Add-on is now running", "selectionHelp": "From the main list, select individual documents or run a search for the documents you want, for example “+project:mueller-docs-200005”.", - "selectionLearnMore": "Learn more about how Add-Ons work" + "selectionLearnMore": "Learn more about how Add-Ons work", + "cost": "{amount} {unit} per {price, plural, one {credit} other {# credits}}", + "premiumUpgrade": { + "message": "This Premium Add-On uses AI to perform advanced analysis. Upgrade to a {plan} account to utilize this and other powerful Add-Ons.", + "callToAction": "Upgrade Plan", + "memberMessage": "This Premium Add-On uses AI to perform advanced analysis. Contact your organization admin about upgrading your plan." + } }, "addonBrowserDialog": { "title": "Browse Add-Ons", @@ -747,7 +786,8 @@ "viewsource": "View Source", "usage": "Usage", "pinnedTip": "Quickly access your favorite Add-Ons by clicking the “Pin” icon next to its name. They’ll then be available to run from the Add-Ons dropdown menu.", - "featuredTip": "Here’s some of the DocumentCloud team’s favorite Add-Ons, including both new additions as well as classics we think every user should try." + "featuredTip": "Here’s some of the DocumentCloud team’s favorite Add-Ons, including both new additions as well as classics we think every user should try.", + "premiumTip": "Premium add-ons have powerful functionality backed by AI and cloud services. They have an additional cost to run and are only available to Professional and Organizational users." }, "addonRuns": { "scheduled": "Scheduled Add-Ons", @@ -783,11 +823,12 @@ "disable": "Disable upload via email" }, "anonymous": { - "title": "Welcome to DocumentCloud, an open document archive from MuckRock!", - "p1": "This site helps organize, analyze and host millions of records contributed by verified newsrooms, research organizations and other groups that help inform the public through the user of primary source materials. Using the search bar above, you can browse through {n} publicly published documents, with thousands more added ever day.", - "p2": "If you're part of a newsroom, academic organization, or other public-interest organization that vets and publishes materials in the public interest, you can register here and request verification to upload materials, or learn more about DocumentCloud and it's powerful suite of hosting, analysis and publication tools.", - "p3": "Want more fascinating documents, open data and original reporting to your inbox? Subscribe to MuckRock's newsletter:", - "p4": "DocumentCloud is part of a suite of transparency tools from the MuckRock Foundation, a 501c3 registered non-profit. This archive is open to the public and advertisement free thanks to support from readers like you — you can learn more about our work or make a donation.", - "subscribe": "Subscribe" - } + "title": "Welcome to DocumentCloud, an open document archive from MuckRock!", + "p1": "This site helps organize, analyze and host millions of records contributed by verified newsrooms, research organizations and other groups that help inform the public through the user of primary source materials. Using the search bar above, you can browse through {n} publicly published documents, with thousands more added ever day.", + "p2": "If you're part of a newsroom, academic organization, or other public-interest organization that vets and publishes materials in the public interest, you can register here and request verification to upload materials, or learn more about DocumentCloud and it's powerful suite of hosting, analysis and publication tools.", + "p3": "Want more fascinating documents, open data and original reporting to your inbox? Subscribe to MuckRock's newsletter:", + "p4": "DocumentCloud is part of a suite of transparency tools from the MuckRock Foundation, a 501c3 registered non-profit. This archive is open to the public and advertisement free thanks to support from readers like you — you can learn more about our work or make a donation.", + "subscribe": "Subscribe" + }, + "premium": {} } diff --git a/src/langs/langs.json b/src/langs/langs.json index 782bd914d..4395197fc 100644 --- a/src/langs/langs.json +++ b/src/langs/langs.json @@ -1,8 +1,8 @@ [ - ["English", "en"], - ["Español", "es"], - ["Français", "fr"], - ["українська", "uk"], - ["русский", "ru"], - ["Deutsche", "de"] + ["US English", "en", "🇺🇸"], + ["Español", "es", "🇪🇸"], + ["Français", "fr", "🇫🇷"], + ["Deutsche", "de", "🇩🇪"], + ["українська", "uk", "🇺🇦"], + ["русский", "ru", "🇷🇺"] ] diff --git a/src/manager/orgsAndUsers.js b/src/manager/orgsAndUsers.js index 3e965f629..257d0eced 100644 --- a/src/manager/orgsAndUsers.js +++ b/src/manager/orgsAndUsers.js @@ -11,6 +11,7 @@ import { getUsers, getOrganization, } from "../api/orgAndUser.js"; +import { SQUARELET_URL } from "../api/auth.js"; import { projects, initProjects } from "./projects.js"; import { userUrl, allDocumentsUrl } from "../search/search.js"; import { layout } from "./layout.js"; @@ -116,7 +117,11 @@ function initProjectsIfNecessary(route) { } export async function initOrgsAndUsers(callback = null) { - orgsAndUsers.me = await getMe(); + try { + orgsAndUsers.me = await getMe(); + } catch (e) { + orgsAndUsers.me = null; + } if (orgsAndUsers.me !== null) { // Logged in orgsAndUsers.usersById[orgsAndUsers.me.id] = orgsAndUsers.me; @@ -127,11 +132,15 @@ export async function initOrgsAndUsers(callback = null) { const org = orgsAndUsers.selfOrgs[i]; orgsAndUsers.orgsById[org.id] = org; } - - orgsAndUsers.sameOrgUsers = await inMyOrg( - orgsAndUsers.me.organization, - orgsAndUsers.me, - ); + try { + orgsAndUsers.sameOrgUsers = await inMyOrg( + orgsAndUsers.me.organization.id, + orgsAndUsers.me.id, + ); + } catch (err) { + console.error(err); + orgsAndUsers.sameOrgUsers = []; + } // Trigger update orgsAndUsers.usersById = orgsAndUsers.usersById; @@ -173,10 +182,15 @@ export async function changeActive(org) { orgsAndUsers.me.organization = org; orgsAndUsers.me = orgsAndUsers.me; - orgsAndUsers.sameOrgUsers = await inMyOrg( - orgsAndUsers.me.organization, - orgsAndUsers.me, - ); + try { + orgsAndUsers.sameOrgUsers = await inMyOrg( + orgsAndUsers.me.organization.id, + orgsAndUsers.me.id, + ); + } catch (err) { + console.error(err); + orgsAndUsers.sameOrgUsers = []; + } pushToast("Successfully changed active organization"); }); } @@ -185,26 +199,59 @@ export async function usersInOrg(orgId) { return getUsers({ orgIds: [orgId] }); } +function alphabetizeUsers(userA, userB) { + const aName = String(userA.name || userA.username); + const bName = String(userB.name || userB.username); + return aName.localeCompare(bName); +} + // same as above, but exclude me -export async function inMyOrg(organization, me) { - if (!organization.id) return []; - const users = await getUsers({ orgIds: [organization.id] }).catch((e) => { - console.error(e); - return []; - }); +export async function inMyOrg(orgId, myId) { + if (!orgId) return []; + const users = await getUsers({ orgIds: [orgId] }); + // Sort by admin status, then username + const adminUsers = users + .filter((u) => u.admin_organizations.includes(orgId)) + .sort(alphabetizeUsers); + const regularUsers = users + .filter((u) => !adminUsers.includes(u)) + .sort(alphabetizeUsers); + // Remove me from the user list + return [...adminUsers, ...regularUsers].filter((u) => u.id !== myId); +} - users.sort((a, b) => { - // Sort by admin status, then username - const aAdmin = a.admin_organizations.includes(organization.id); - const bAdmin = b.admin_organizations.includes(organization.id); - if (aAdmin == bAdmin) { - return String(a.name || a.username).localeCompare( - String(b.name || b.username), - ); - } else { - return aAdmin < bAdmin; - } - }); +export function isOrgAdmin(user) { + if (!user) return false; + const id = + typeof user.organization === "string" + ? user.organization + : user.organization.id; + return user.admin_organizations.includes(id); +} + +export function isPremiumOrg(org) { + if (!org || !org.plan) return null; + return org.plan !== "Free"; +} + +export function getCreditBalance(org) { + if (!org) return null; + return org.monthly_credits + org.purchased_credits; +} + +export async function triggerPremiumUpgradeFlow(org) { + let url; + if (org.individual) { + // Redirect the user to their Squarelet account settings + url = SQUARELET_URL + `/users/~payment/`; + } else { + // Redirect the user to the Squarelet organization settings + url = SQUARELET_URL + `/organizations/${org.slug}/payment/`; + } + window?.open(url); +} - return users.filter((u) => u.id !== me.id); +// TODO: Handle flow for purchasing premium credits (#342) +export async function triggerCreditPurchaseFlow() { + alert("Purchase Credits!"); } diff --git a/src/pages/app/AccountNavigation/AccountNavigation.svelte b/src/pages/app/AccountNavigation/AccountNavigation.svelte new file mode 100644 index 000000000..5e7f4c9c8 --- /dev/null +++ b/src/pages/app/AccountNavigation/AccountNavigation.svelte @@ -0,0 +1,60 @@ + + + + + diff --git a/src/pages/app/AccountNavigation/HelpMenu.svelte b/src/pages/app/AccountNavigation/HelpMenu.svelte new file mode 100644 index 000000000..2270d0cbf --- /dev/null +++ b/src/pages/app/AccountNavigation/HelpMenu.svelte @@ -0,0 +1,74 @@ + + + + + + + +
+
+ + + + + {$_("authSection.help.faq")} + + + + + + {$_("authSection.help.searchDocs")} + + + + + + {$_("authSection.help.apiDocs")} + + + + + + {$_("authSection.help.addOns")} + + + + + + {$_("authSection.help.premium")} + + + + + + {$_("authSection.help.emailUs")} + + + +
diff --git a/src/pages/app/AccountNavigation/LanguageMenu.svelte b/src/pages/app/AccountNavigation/LanguageMenu.svelte new file mode 100644 index 000000000..ccb1bb3f3 --- /dev/null +++ b/src/pages/app/AccountNavigation/LanguageMenu.svelte @@ -0,0 +1,53 @@ + + + + +{#if langs.length > 1} + + + +
+
+ + {#each langs as [name, code, flag]} + { + updateLanguage(code); + closeDropdown("language"); + }} + selected={code === $locale} + > + {name} + {flag} + + {/each} + +
+{/if} diff --git a/src/pages/app/AccountNavigation/OrgMemberList.svelte b/src/pages/app/AccountNavigation/OrgMemberList.svelte new file mode 100644 index 000000000..ac1f27323 --- /dev/null +++ b/src/pages/app/AccountNavigation/OrgMemberList.svelte @@ -0,0 +1,142 @@ + + + + +{#await promise} + +{:then users} + {#if users.length > 0} +

{handlePlural(users.length, "organization member")}

+
    + {#each users as user} +
  • + +
    + {#if user.avatar_url} + + {:else} + + {/if} + {user.name} + {#if user.admin_organizations.includes(orgId)} + {$_("authSection.org.adminRole")} + {/if} +
    + +
  • + {/each} +
+ {:else} +
+
+

{$_("authSection.org.memberListEmpty")}

+
+ {/if} +{:catch} + +
+
+

{$_("authSection.org.memberListError")}

+ +
+{/await} diff --git a/src/pages/app/AccountNavigation/OrgMenu.svelte b/src/pages/app/AccountNavigation/OrgMenu.svelte new file mode 100644 index 000000000..796880aaa --- /dev/null +++ b/src/pages/app/AccountNavigation/OrgMenu.svelte @@ -0,0 +1,175 @@ + + + + +{#await getOrgPromise} + + + + + +

Getting org data…

+
+
+{:then activeOrg} + {#if activeOrg.individual === true} + + {#await listOrgsPromise then orgOptions} + {#if orgOptions.length > 1} + + {/if} + {/await} + + {:else} + + + + {#if activeOrg.avatar_url} + + {:else} +
+ {/if} +
+
+ +
+ {#if isPremiumOrg(activeOrg)} + + + + + {:else if isOrgAdmin(user)} + +
+

+ {$_("authSection.premiumUpgrade.orgHeading")} +

+

+ {$_("authSection.premiumUpgrade.orgDescription")} +

+
+
+ {/if} + + {#await listOrgsPromise then orgOptions} + {#if orgOptions.length > 1} + + {/if} + {/await} +
+
+
+ {/if} +{/await} diff --git a/src/pages/app/AccountNavigation/OrgPicker.svelte b/src/pages/app/AccountNavigation/OrgPicker.svelte new file mode 100644 index 000000000..f5bf8843c --- /dev/null +++ b/src/pages/app/AccountNavigation/OrgPicker.svelte @@ -0,0 +1,76 @@ + + + + +
+

Switch organization

+
+ + + + {#if activeOrg.avatar_url} + + {:else} +
+ {/if} +
+
+ + {#if loading} + + {:else} + {#each orgOptions as org} + { + closeDropdown("orgSelect"); + handleChange?.(org); + }} + selected={org.id === activeOrg.id} + > + {org.name} + + {/each} + {/if} + +
+
+
diff --git a/src/pages/app/AccountNavigation/PremiumMenu.svelte b/src/pages/app/AccountNavigation/PremiumMenu.svelte new file mode 100644 index 000000000..e756bab5a --- /dev/null +++ b/src/pages/app/AccountNavigation/PremiumMenu.svelte @@ -0,0 +1,139 @@ + + + + + + + +
+
+
+ + {#if isPremium} + + + + + {:else} + +

{$_("authSection.premiumUpgrade.heading")}

+

+ {$_("authSection.premiumUpgrade.description")} +

+
+
diff --git a/src/pages/app/AccountNavigation/UserMenu.svelte b/src/pages/app/AccountNavigation/UserMenu.svelte new file mode 100644 index 000000000..d737d3a3c --- /dev/null +++ b/src/pages/app/AccountNavigation/UserMenu.svelte @@ -0,0 +1,70 @@ + + + + +{#if user} + + + + {#if user.avatar_url} + + {:else} +
+ {/if} +
+
+ + + + {$_("authSection.user.acctSettings")} + + + + {$_("authSection.user.uploadEmail")} + + + + {$_("authSection.user.signOut")} + + +
+{:else} + {$_("authSection.user.signIn")} +{/if} diff --git a/src/pages/app/AccountNavigation/fixtures/getMe.json b/src/pages/app/AccountNavigation/fixtures/getMe.json new file mode 100644 index 000000000..aa7fbccca --- /dev/null +++ b/src/pages/app/AccountNavigation/fixtures/getMe.json @@ -0,0 +1,22 @@ +{ + "id": 4, + "avatar_url": "https://cdn.muckrock.com/media/account_images/allan-headshot-2016.jpg", + "feature_level": 2, + "name": "Allan Lasser", + "organization": { + "id": 1, + "avatar_url": "https://squarelet-staging.s3.amazonaws.com/media/org_avatars/logo_uEHCMva.png", + "individual": false, + "name": "MuckRock", + "slug": "muckrock", + "monthly_credits": 5000, + "purchased_credits": 0, + "credit_reset_date": "2023-11-28", + "monthly_credit_allowance": 5000, + "plan": "Organization" + }, + "organizations": [1, 4], + "admin_organizations": [40742], + "username": "lasser.allan", + "verified_journalist": true +} diff --git a/src/pages/app/AccountNavigation/fixtures/orgList.json b/src/pages/app/AccountNavigation/fixtures/orgList.json new file mode 100644 index 000000000..9f95472b2 --- /dev/null +++ b/src/pages/app/AccountNavigation/fixtures/orgList.json @@ -0,0 +1,30 @@ +{ + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "avatar_url": "https://squarelet-staging.s3.amazonaws.com/media/org_avatars/logo_uEHCMva.png", + "individual": false, + "name": "MuckRock", + "slug": "muckrock", + "monthly_credits": 5000, + "purchased_credits": 0, + "credit_reset_date": "2023-11-28", + "monthly_credit_allowance": 5000, + "plan": "Organization" + }, + { + "id": 4, + "avatar_url": "https://cdn.muckrock.com/media/account_images/allan-headshot-2016.jpg", + "individual": true, + "name": "lasser.allan", + "slug": "lasserallan", + "monthly_credits": 2500, + "purchased_credits": 3000, + "credit_reset_date": "2023-11-28", + "monthly_credit_allowance": 2500, + "plan": "Professional" + } + ] +} diff --git a/src/pages/app/AccountNavigation/fixtures/orgMembers.json b/src/pages/app/AccountNavigation/fixtures/orgMembers.json new file mode 100644 index 000000000..736c6b202 --- /dev/null +++ b/src/pages/app/AccountNavigation/fixtures/orgMembers.json @@ -0,0 +1,125 @@ +{ + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "avatar_url": "https://cdn.muckrock.com/media/avatars/Michael_Headshot.jpeg", + "name": "Michael Morisy", + "organization": 1, + "organizations": [1, 10], + "admin_organizations": [1, 10], + "username": "morisy", + "verified_journalist": true + }, + { + "id": 2, + "avatar_url": "https://cdn.muckrock.com/media/avatars/20140211-0O1A7147-2.jpg", + "name": "Chris Amico", + "organization": 1, + "organizations": [1, 11], + "admin_organizations": [11], + "username": "chrisamico", + "verified_journalist": true + }, + { + "id": 3, + "avatar_url": "https://cdn.muckrock.com/media/account_images/22_MuckRock_2018_PRELIMANRY_EDITS_DSC_5387_2018_Derek_Kouyoumjian_preview_SfuGGnJ.jpg", + "name": "Mitchell Kotler", + "organization": 1, + "organizations": [1, 12], + "admin_organizations": [1, 12], + "username": "mitch", + "verified_journalist": true + }, + { + "id": 4, + "avatar_url": "https://cdn.muckrock.com/media/account_images/allan-headshot-2016.jpg", + "name": "Allan Lasser", + "organization": 1, + "organizations": [1, 13], + "admin_organizations": [13], + "username": "lasser.allan", + "verified_journalist": true + }, + { + "id": 5, + "avatar_url": "https://cdn.muckrock.com/media/avatars/noah_oct2021_vsco_cropped_smaller.gif", + "name": "Amanda Hickman", + "organization": 1, + "organizations": [1, 14], + "admin_organizations": [1, 14], + "username": "amandabee", + "verified_journalist": true + }, + { + "id": 6, + "avatar_url": "https://cdn.muckrock.com/media/avatars/derek-crop-square_Rx7mFk8.jpg", + "name": "Derek Kravitz", + "organization": 1, + "organizations": [1, 15], + "admin_organizations": [1, 15], + "username": "DerekRKravitz", + "verified_journalist": true + }, + { + "id": 7, + "avatar_url": null, + "name": "Miranda Carruth", + "organization": 1, + "organizations": [1, 16], + "admin_organizations": [16], + "username": "mirandac" + }, + { + "id": 8, + "avatar_url": "https://cdn.muckrock.com/media/avatars/kelly-square.jpg", + "name": "Kelly Kauffman", + "organization": 1, + "organizations": [1, 17], + "admin_organizations": [17], + "username": "kellykauffman", + "verified_journalist": true + }, + { + "id": 9, + "avatar_url": "https://cdn.muckrock.com/media/avatars/Albert_Serna_Jr.jpg", + "name": "Albert Serna Jr.", + "organization": 1, + "organizations": [1, 18], + "admin_organizations": [18], + "username": "AlbertSGJ", + "verified_journalist": true + }, + { + "id": 10, + "avatar_url": "https://cdn.muckrock.com/media/avatars/under.jpg", + "name": "JPat Brown", + "organization": 1, + "organizations": [1, 19], + "admin_organizations": [19], + "username": "JPatBrown", + "verified_journalist": true + }, + { + "id": 11, + "avatar_url": null, + "name": "Scott Klein", + "organization": 1, + "organizations": [1, 20], + "admin_organizations": [20], + "username": "ScottKlein", + "verified_journalist": true + }, + { + "id": 12, + "avatar_url": "https://cdn.muckrock.com/media/account_images/1_Nz2NFvHxTjqw5asaiPKf5w_sssJnjW.jpg", + "name": "Aron Pilhofer", + "organization": 1, + "organizations": [1, 21], + "admin_organizations": [22], + "username": "Pilhofer", + "verified_journalist": true + } + ] +} diff --git a/src/pages/app/AccountNavigation/stories/AccountNavigation.stories.svelte b/src/pages/app/AccountNavigation/stories/AccountNavigation.stories.svelte new file mode 100644 index 000000000..952ed6fe3 --- /dev/null +++ b/src/pages/app/AccountNavigation/stories/AccountNavigation.stories.svelte @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/app/AccountNavigation/stories/HelpMenu.stories.svelte b/src/pages/app/AccountNavigation/stories/HelpMenu.stories.svelte new file mode 100644 index 000000000..a5c365886 --- /dev/null +++ b/src/pages/app/AccountNavigation/stories/HelpMenu.stories.svelte @@ -0,0 +1,17 @@ + + + + + + + diff --git a/src/pages/app/AccountNavigation/stories/LanguageMenu.stories.svelte b/src/pages/app/AccountNavigation/stories/LanguageMenu.stories.svelte new file mode 100644 index 000000000..f6c45bf28 --- /dev/null +++ b/src/pages/app/AccountNavigation/stories/LanguageMenu.stories.svelte @@ -0,0 +1,17 @@ + + + + + + + diff --git a/src/pages/app/AccountNavigation/stories/OrgMemberList.stories.svelte b/src/pages/app/AccountNavigation/stories/OrgMemberList.stories.svelte new file mode 100644 index 000000000..faa081a90 --- /dev/null +++ b/src/pages/app/AccountNavigation/stories/OrgMemberList.stories.svelte @@ -0,0 +1,62 @@ + + + + + + + + + + + + diff --git a/src/pages/app/AccountNavigation/stories/OrgMenu.stories.svelte b/src/pages/app/AccountNavigation/stories/OrgMenu.stories.svelte new file mode 100644 index 000000000..4815e2270 --- /dev/null +++ b/src/pages/app/AccountNavigation/stories/OrgMenu.stories.svelte @@ -0,0 +1,172 @@ + + + + + + + + + + + + + diff --git a/src/pages/app/AccountNavigation/stories/PremiumMenu.stories.svelte b/src/pages/app/AccountNavigation/stories/PremiumMenu.stories.svelte new file mode 100644 index 000000000..66c9144fb --- /dev/null +++ b/src/pages/app/AccountNavigation/stories/PremiumMenu.stories.svelte @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/src/pages/app/AccountNavigation/stories/UserMenu.stories.svelte b/src/pages/app/AccountNavigation/stories/UserMenu.stories.svelte new file mode 100644 index 000000000..ea21b857e --- /dev/null +++ b/src/pages/app/AccountNavigation/stories/UserMenu.stories.svelte @@ -0,0 +1,95 @@ + + + + + + + + + + diff --git a/src/pages/app/AccountNavigation/types.ts b/src/pages/app/AccountNavigation/types.ts new file mode 100644 index 000000000..99ea5ffe7 --- /dev/null +++ b/src/pages/app/AccountNavigation/types.ts @@ -0,0 +1,37 @@ +export type Maybe = T | undefined | null; + +interface PremiumOrgFields { + purchased_credits: number; + monthly_credits: number; + monthly_credit_allowance: number; + credit_reset_date: string; +} + +export interface Org extends Partial { + id: string; + name: string; + slug: string; + avatar_url: string; + individual: boolean; + plan: "Free" | "Professional" | "Organization"; +} + +export interface GroupOrg extends Org { + individual: false; + plan: "Free" | "Organization"; +} + +export interface IndividualOrg extends Org { + individual: true; + plan: "Free" | "Professional"; +} + +export interface User { + id: string; + name: Maybe; + avatar_url: Maybe; + username: string; + organization: string | Org; + organizations: string[]; + admin_organizations: string[]; +} diff --git a/src/pages/app/Documents.svelte b/src/pages/app/Documents.svelte index 80604eb31..a690d04f5 100644 --- a/src/pages/app/Documents.svelte +++ b/src/pages/app/Documents.svelte @@ -7,7 +7,7 @@ import AddonStatus from "../../addons/progress/AddonStatus.svelte"; import ActionBar from "./ActionBar.svelte"; import Anonymous from "./Anonymous.svelte"; - import AuthSection from "@/pages/app/AuthSection.svelte"; + import AccountNavigation from "./AccountNavigation/AccountNavigation.svelte"; import Button from "@/common/Button.svelte"; import Draggable from "@/common/Draggable.svelte"; import Document from "./Document.svelte"; @@ -254,7 +254,7 @@ {#if $orgsAndUsers.loggedIn} {/if} - + {/if} {#if embed && $layout.projectEmbedTitle != null}
{$layout.projectEmbedTitle}
diff --git a/src/pages/app/sidebar/OrgUsers.svelte b/src/pages/app/sidebar/OrgUsers.svelte deleted file mode 100644 index d313e1aa1..000000000 --- a/src/pages/app/sidebar/OrgUsers.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - - - -
- {#if $orgsAndUsers.me !== null && !$orgsAndUsers.me.organization.individual} -
- -

- {$_("organizations.sameOrgUsers")}: {$orgsAndUsers.me.organization.name} -

-
- -
    - {#each $orgsAndUsers.sameOrgUsers as user} -
  • - - {user.name} - {#if user.admin_organizations.includes($orgsAndUsers.me.organization.id)} - (Admin) - {/if} - -
  • - {/each} -
-
- {/if} -
diff --git a/src/pages/app/sidebar/Sidebar.svelte b/src/pages/app/sidebar/Sidebar.svelte index 41190400d..981382a04 100644 --- a/src/pages/app/sidebar/Sidebar.svelte +++ b/src/pages/app/sidebar/Sidebar.svelte @@ -7,7 +7,6 @@ import ProjectFilters from "./ProjectFilters.svelte"; import Projects from "./Projects.svelte"; - import OrgUsers from "./OrgUsers.svelte"; import AddonSidebar from "../../../addons/sidebar/Sidebar.svelte"; @@ -73,7 +72,6 @@ {#if $orgsAndUsers.me !== null} - {/if} diff --git a/src/premium-credits/CreditMeter.svelte b/src/premium-credits/CreditMeter.svelte new file mode 100644 index 000000000..96e9958af --- /dev/null +++ b/src/premium-credits/CreditMeter.svelte @@ -0,0 +1,91 @@ + + + + + + + diff --git a/src/premium-credits/Price.svelte b/src/premium-credits/Price.svelte new file mode 100644 index 000000000..c95105488 --- /dev/null +++ b/src/premium-credits/Price.svelte @@ -0,0 +1,29 @@ + + + + + + + {value?.toLocaleString()} + diff --git a/src/premium-credits/UpgradePrompt.svelte b/src/premium-credits/UpgradePrompt.svelte new file mode 100644 index 000000000..70a88557b --- /dev/null +++ b/src/premium-credits/UpgradePrompt.svelte @@ -0,0 +1,51 @@ + + + + +
+
+ +

{message}

+
+ {#if callToAction} +
+
+ {/if} +
diff --git a/src/premium-credits/stories/CreditMeter.stories.svelte b/src/premium-credits/stories/CreditMeter.stories.svelte new file mode 100644 index 000000000..b74241b48 --- /dev/null +++ b/src/premium-credits/stories/CreditMeter.stories.svelte @@ -0,0 +1,47 @@ + + + + + + + + + + + + + diff --git a/src/premium-credits/stories/Price.stories.svelte b/src/premium-credits/stories/Price.stories.svelte new file mode 100644 index 000000000..8f1ac0ec1 --- /dev/null +++ b/src/premium-credits/stories/Price.stories.svelte @@ -0,0 +1,22 @@ + + + + + + + diff --git a/src/premium-credits/stories/UpgradePrompt.stories.svelte b/src/premium-credits/stories/UpgradePrompt.stories.svelte new file mode 100644 index 000000000..96e2b0f3b --- /dev/null +++ b/src/premium-credits/stories/UpgradePrompt.stories.svelte @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/src/router/router.js b/src/router/router.js index 594ec3d4b..d37206dbe 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -60,7 +60,7 @@ export class Router extends Svue { } lookup(name) { - return this.routes[name].path; + return this.routes?.[name]?.path; } resolve(path) { diff --git a/src/style/variables.css b/src/style/variables.css index 6c8744c17..1f6ef7048 100644 --- a/src/style/variables.css +++ b/src/style/variables.css @@ -3,6 +3,7 @@ --primary: #4294f0; --primary-faded: rgba(66, 148, 240, 0.13); --sidebar: #edeeef; + --white: #ffffff; --black: #000000; --gray: rgba(0, 0, 0, 0.53); --darkgray: rgba(0, 0, 0, 0.8); @@ -23,6 +24,7 @@ --actionPane: #fffdea; --highlight-orange: #ff785c; --inputBg: #ffffff; + --premium: #24CC99; /* Viewer */ --viewerPaneColor: #f3f3f3; diff --git a/tests/anonymous/manager/app.spec.js b/tests/anonymous/manager/app.spec.js index 41703742b..5b338d954 100644 --- a/tests/anonymous/manager/app.spec.js +++ b/tests/anonymous/manager/app.spec.js @@ -29,16 +29,16 @@ test.describe("manager tests", () => { await page.goto("/app"); // help - await page.getByText("Help ▼").click(); + await page.getByText("Help", { exact: true }).dispatchEvent("click"); await expect(page.getByRole("button", { name: "FAQ" })).toBeVisible(); // close the menu - await page.locator(".shim").click(); + await page.locator(".overlay").click(); // language - await page.getByText("Language ▼").click(); - await expect(page.getByRole("button", { name: "English ✓" })).toBeVisible(); + await page.getByText("Language", { exact: true }).dispatchEvent("click"); + await expect(page.getByRole("button", { name: "Español" })).toBeVisible(); - await page.locator(".shim").click(); + await page.locator(".overlay").click(); }); }); diff --git a/tests/anonymous/pages/home.spec.js b/tests/anonymous/pages/home.spec.js index 699c9aecc..6bc3be552 100644 --- a/tests/anonymous/pages/home.spec.js +++ b/tests/anonymous/pages/home.spec.js @@ -11,7 +11,7 @@ test("basic homepage test", async ({ page }) => { await page.getByRole("banner").getByRole("link").first().click(); // and back - await page.getByRole("link", { name: "Home" }).click(); + await page.goBack(); await expect(page).toHaveTitle("Home | DocumentCloud"); });