From f6fe51235da551acb951fd37bf5009f1885e1a11 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 24 Jul 2024 13:50:53 +0700 Subject: [PATCH] Display related projects in project create page If the org dropdown has a selected org, and there is a valid language code (2 or 3 characters), then a search will be made for any projects belonging to the selected org that have that language code in their list of currently active vernacular writing systems. --- frontend/src/lib/util/time.ts | 27 ++++++++++++++++++ .../project/create/+page.svelte | 28 ++++++++++++++++++- .../(authenticated)/project/create/+page.ts | 20 ++++++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/util/time.ts b/frontend/src/lib/util/time.ts index 43c5272ba..31689717d 100644 --- a/frontend/src/lib/util/time.ts +++ b/frontend/src/lib/util/time.ts @@ -77,3 +77,30 @@ export function deriveAsync( }, debounceTime); }, initialValue); } + +/** + * @param fn A function that maps the store value to an async result, filtering out undefined values + * @returns A store that contains the result of the async function + */ +export function deriveAsyncIfDefined( + store: Readable, + fn: (value: T) => Promise, + initialValue?: D, + debounce: number | boolean = false): Readable { + + const debounceTime = pickDebounceTime(debounce); + let timeout: ReturnType | undefined; + + return derived(store, (value, set) => { + if (value) { + clearTimeout(timeout); + timeout = setTimeout(() => { + const myTimeout = timeout; + void fn(value).then((result) => { + if (myTimeout !== timeout) return; // discard outdated results + set(result); + }); + }, debounceTime); + } + }, initialValue); +} diff --git a/frontend/src/routes/(authenticated)/project/create/+page.svelte b/frontend/src/routes/(authenticated)/project/create/+page.svelte index 6914099c2..ffa3180b2 100644 --- a/frontend/src/routes/(authenticated)/project/create/+page.svelte +++ b/frontend/src/routes/(authenticated)/project/create/+page.svelte @@ -8,7 +8,7 @@ import { _createProject, _projectCodeAvailable } from './+page'; import AdminContent from '$lib/layout/AdminContent.svelte'; import { useNotifications } from '$lib/notify'; - import { Duration, deriveAsync } from '$lib/util/time'; + import { Duration, deriveAsync, deriveAsyncIfDefined } from '$lib/util/time'; import { getSearchParamValues } from '$lib/util/query-params'; import { onMount } from 'svelte'; import MemberBadge from '$lib/components/Badges/MemberBadge.svelte'; @@ -18,6 +18,7 @@ import { ProjectConfidentialityCombobox } from '$lib/components/Projects'; import DevContent from '$lib/layout/DevContent.svelte'; import { isDev } from '$lib/layout/DevContent.svelte'; + import { _getProjectsByLangCodeAndOrg } from './+page'; export let data; $: user = data.user; @@ -85,6 +86,19 @@ $: $asyncCodeError = $codeIsAvailable ? undefined : $t('project.create.code_exists'); const codeErrors = derived([errors, asyncCodeError], () => [...new Set(concatAll($errors.code, $asyncCodeError))]); + const langCodeStore = derived(form, ($form) => $form.languageCode); + const orgIdStore = derived(form, ($form) => $form.orgId); + const langCodeAndOrgIdStore = derived([langCodeStore, orgIdStore], ([lang, orgId], set) => { + if (lang && orgId && (lang.length == 2 || lang.length == 3)) { + set({ langCode: lang, orgId: orgId }); + } + }); + + const relatedProjectsStoreStore = deriveAsyncIfDefined(langCodeAndOrgIdStore, _getProjectsByLangCodeAndOrg); + const relatedProjects = derived(relatedProjectsStoreStore, (nestedStore, set) => { + if (nestedStore) return nestedStore.subscribe(set); // Return the unsubscribe fn so we don't leak memory + }, []); + const typeCodeMap: Partial> = { [ProjectType.FlEx]: 'flex', [ProjectType.WeSay]: 'dictionary', @@ -212,6 +226,18 @@ bind:value={$form.languageCode} error={$errors.languageCode} /> + + {#if $relatedProjects?.length} +
+ Possibly related projects: +
    + {#each $relatedProjects as proj} +
  • {proj.name} ({proj.code})
  • + {/each} +
+
+ {/if} + diff --git a/frontend/src/routes/(authenticated)/project/create/+page.ts b/frontend/src/routes/(authenticated)/project/create/+page.ts index 53a7e6b87..29e525a21 100644 --- a/frontend/src/routes/(authenticated)/project/create/+page.ts +++ b/frontend/src/routes/(authenticated)/project/create/+page.ts @@ -1,9 +1,10 @@ -import type { $OpResult, CreateProjectInput, CreateProjectMutation } from '$lib/gql/types'; +import type { $OpResult, CreateProjectInput, CreateProjectMutation, ProjectsByLangCodeAndOrgQuery } from '$lib/gql/types'; import { getClient, graphql } from '$lib/gql'; import type { PageLoadEvent } from './$types'; import { getSearchParam } from '$lib/util/query-params'; import { isGuid } from '$lib/util/guid'; +import type { Readable } from 'svelte/store'; export async function load(event: PageLoadEvent) { const userIsAdmin = (await event.parent()).user.isAdmin; @@ -88,3 +89,20 @@ export async function _projectCodeAvailable(code: string): Promise { if (!result.ok) throw new Error('Failed to check project code availability'); return await result.json() as boolean; } + +export async function _getProjectsByLangCodeAndOrg(input: { orgId: string, langCode: string }): Promise> { + const client = getClient(); + //language=GraphQL + const results = await client.awaitedQueryStore(fetch, + graphql(` + query ProjectsByLangCodeAndOrg($input: ProjectsByLangCodeAndOrgInput!) { + projectsByLangCodeAndOrg(input: $input) { + id + code + name + } + } + `), { input } + ); + return results.projectsByLangCodeAndOrg; +}