From 9e8eb2dcf5d80121a7208050318bb527838d81f6 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Wed, 07 Sep 2022 23:21:25 +0100 Subject: [PATCH] Add frontend documentation --- chartered-frontend/src/components/Icon.svelte | 23 +++++++++++++++++++++++ chartered-frontend/src/components/NavItem.svelte | 5 +++++ chartered-frontend/src/components/RelativeTime.svelte | 4 ++++ chartered-frontend/src/routes/(authed)/+layout.svelte | 3 +++ chartered-frontend/src/routes/(unauthed)/+layout.svelte | 3 +++ chartered-frontend/src/routes/(authed)/organisations/+page.svelte | 1 + chartered-frontend/src/routes/(authed)/search/+page.svelte | 2 ++ chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte | 19 +++++++++++++++++++ chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte | 32 ++++++++++++++++++++++++++++++++ chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte | 1 + chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte | 19 ++++++++++++++----- chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte | 2 ++ chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte | 2 ++ chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte | 9 +++++++++ chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte | 15 +++++++++++++-- chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/MemberTab.svelte | 9 +++++++++ chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/VersionTab.svelte | 15 +++++++++++++-- 18 files changed, 203 insertions(+), 13 deletions(-) diff --git a/chartered-frontend/src/components/Icon.svelte b/chartered-frontend/src/components/Icon.svelte index 96e3425..2bb31ee 100644 --- a/chartered-frontend/src/components/Icon.svelte +++ a/chartered-frontend/src/components/Icon.svelte @@ -1,12 +1,35 @@ diff --git a/chartered-frontend/src/routes/(authed)/search/+page.svelte b/chartered-frontend/src/routes/(authed)/search/+page.svelte index 7133603..3bf7d66 100644 --- a/chartered-frontend/src/routes/(authed)/search/+page.svelte +++ a/chartered-frontend/src/routes/(authed)/search/+page.svelte @@ -1,9 +1,11 @@ diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte index 7928ca9..3bfc3ae 100644 --- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte +++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte @@ -9,9 +9,17 @@ import AddMember from './AddMember.svelte'; import type { CrateMembers, CrateMember } from '../../../../types/crate'; + // Load the requested organisation from the URL let organisationPromise: Promise; $: organisationPromise = request(`/web/v1/organisations/${$page.params.organisation}`); + /** + * Whenever a member is updated/added/deleted to this organisation, we'll want to reload to ensure we + * show the user exactly what the server currently sees. + * + * @param event a struct containing the updated member's UUID, so we can empty the newMember value if that member + * has just been added to we don't show them twice. + */ function reload(event: { detail: string }) { organisationPromise = request(`/web/v1/organisations/${$page.params.organisation}`); @@ -20,11 +28,18 @@ } } + /** + * Contains all the possible tabs, used for maintaining state on the current tab. + */ enum Tab { CRATES, MEMBERS, } + /** + * Mapping of `Tab`s to their human-readable form alongside a friendly icon to show to the + * user. + */ const allTabs = [ { id: Tab.CRATES, @@ -38,7 +53,11 @@ }, ]; + // binding to the current tab the user has selected let currentTab = Tab.CRATES; + + // contains the member the user is currently considering adding to the org & has not yet persisted to + // the server. let newMember: CrateMember | null = null; diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte index ad6e964..0d49972 100644 --- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte +++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte @@ -6,20 +6,46 @@ import { createEventDispatcher } from 'svelte'; import type { UserSearch, UserSearchUser } from '../../../../types/user'; + // Create the dispatcher to send an event whenever a new member is selected + // by the user. const dispatch = createEventDispatcher(); + /** + * A list of UUIDs to hide from the search, so we can skip showing users that + * are already members. + */ export let hideUuids: string[] = []; + /** + * Contains whether the search results are currently loading so a spinner + * can be shown. + */ let loading = false; + /** + * Binding to the search terms the user has entered. + */ let search = ''; + + /** + * A list of search results from the backend + */ let searchResults: UserSearchUser[] = []; + + // update `searchResults` whenever `search` is updated $: performSearch(search); + // debounce the user's input for 250ms, so we don't just spam the backend with search + // requests even though the user isn't finished yet. const onInput = debounce((event) => { search = event.target.value; }, 250); + /** + * Call the backend and fetch user results for the user's given search terms. + * + * @param search terms to search for + */ async function performSearch(search: string) { if (search === '') { return; @@ -29,7 +55,6 @@ try { let result = await request(`/web/v1/users/search?q=${search}`); - searchResults = result.users || []; } catch (e: unknown) { console.log(e); @@ -38,6 +63,11 @@ } } + /** + * Send an event back to the parent component whenever a user is selected. + * + * @param member member to send to the parent component + */ function dispatchNewMember(member: UserSearchUser) { dispatch('new', member); searchResults = []; diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte index c2af427..6273faa 100644 --- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte +++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte @@ -10,32 +10,76 @@ const dispatch = createEventDispatcher(); - let clazz = ''; - + /** + * The name of the organisation that this is a `member` of. + */ export let organisation: string; + + /** + * The name of the crate that this is a `member` of, or `null` if we're showing organisation + * results. + */ export let crate: string | null = null; + + /** + * The member to show + */ export let member: CrateMember; + + /** + * A list of new permissions for the user, this is normally set by the user in the UI via bindings and default + * to the user's current permissions. Whenever this differs from the user's current permissions then the save + * icon is shown. This is exposed to the consumer for new ("prospective") members that don't currently exist + * on the backend, where the consumer wants to give a default `VISIBLE` permission but also show the save icon. + */ export let newPermissions = member.permissions; + + /** + * A list of possible permissions this user can be given. + */ export let possiblePermissions: string[]; + + /** + * A list of CSS classes to add to the outer div. + */ + let clazz = ''; export { clazz as class }; + /** + * Whether the member is currently being persisted to the backend and a spinner is showing. + */ let saving = false; + + /** + * Any errors that happened upon the last invocation of `save` to give feedback to the user. + */ let error: string | null = null; + /** + * Persist updates to this member to the backend. + */ async function save() { saving = true; error = null; try { + // determine the HTTP verb to send for this membership change. let method; if (!newPermissions.includes('VISIBLE')) { + // if the user is removing the VISIBLE permission from this member then it's a DELETE + // operation otherwise their membership would be useless. method = 'DELETE'; } else if (member.permissions.length === 0) { + // if the member did not have initial permissions on this crate/org then they're a new + // member to it, welcome aboard! method = 'PUT'; } else { + // anything else is simply just an update to an existing member method = 'PATCH'; } + // this component is called from both organisation views and crate views, so we need to figure + // out which one we need to persist the changes to... let url; if (crate) { url = `web/v1/crates/${organisation}/${crate}`; @@ -43,6 +87,7 @@ url = `web/v1/organisations/${organisation}`; } + // send the membership update to the backend let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/${url}/members`, { method, headers: { @@ -61,6 +106,9 @@ throw new Error(json.error); } + // fast-update the permissions locally to hide the save button, then prompt the parent + // component to update their membership list so the user gets the most up-to-date view + // of permissions that the server sees. member.permissions = newPermissions; dispatch('updated', member.uuid); } catch (e) { diff --git a/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte b/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte index 5a6e8d7..1754939 100644 --- a/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte +++ a/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte @@ -1,9 +1,10 @@ diff --git a/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte b/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte index 961347a..d866082 100644 --- a/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte +++ a/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte @@ -6,16 +6,23 @@ import Icon from '../../../../components/Icon.svelte'; import DeleteSessionModal from './DeleteSessionModal.svelte'; - let sessionPromise = loadSessions(); - + /** + * If not null, then the user is currently attempting to delete a session and a modal is being + * shown for them to confirm. + */ let deleting: Session | null = null; - function loadSessions(): Promise { - return request('/web/v1/sessions'); - } + /** + * Grab the list of current sessions from the user from the backend. + */ + let sessionPromise: Promise = request('/web/v1/sessions'); + /** + * Reload all the user's current sessions whenever a session is deleted so the user gets + * an up-to-date view of what the backend sees. + */ function reloadSessions() { - sessionPromise = loadSessions(); + sessionPromise = request('/web/v1/sessions'); } diff --git a/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte b/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte index 889c1ee..d0e39ea 100644 --- a/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte +++ a/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte @@ -5,6 +5,8 @@ import Icon from '../../../../components/Icon.svelte'; import type { User } from '../../../../types/user'; + // grab the requested user from the backend and determine a `displayName` for them to show on their + // profile. let userPromise: Promise; $: userPromise = request(`/web/v1/users/info/${$page.params.uuid}`).then( (user: User & { displayName?: string }) => { diff --git a/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte b/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte index 9758c10..c639297 100644 --- a/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte +++ a/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte @@ -1,9 +1,11 @@ diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte index 5341842..216565a 100644 --- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte +++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte @@ -9,12 +9,19 @@ import VersionTab from './VersionTab.svelte'; import MemberTab from './MemberTab.svelte'; + /** + * Contains all the possible tabs, used for maintaining state on the current tab. + */ enum Tab { README, VERSIONS, MEMBERS, } + /** + * Mapping of `Tab`s to their human-readable form alongside a friendly icon to show to the + * user. + */ const allTabs = [ { id: Tab.README, @@ -33,9 +40,11 @@ }, ]; + // lookup the crate currently requested by the user based on the URL let cratePromise: Promise; $: cratePromise = request(`/web/v1/crates/${$page.params.organisation}/${$page.params.crate}`); + // binding to the current tab the user has selected let currentTab = Tab.README; diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte index 0a84c5e..3569961 100644 --- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte +++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte @@ -1,11 +1,22 @@