From 55572fe3b0b292c20dfc057afca619f7d98944ca Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 23 Jul 2024 14:46:37 +0700 Subject: [PATCH 01/31] Add GraphQL query for projects in org by lang code --- backend/LexBoxApi/GraphQL/LexQueries.cs | 28 +++++++++++++++++++++++++ frontend/schema.graphql | 6 ++++++ 2 files changed, 34 insertions(+) diff --git a/backend/LexBoxApi/GraphQL/LexQueries.cs b/backend/LexBoxApi/GraphQL/LexQueries.cs index c9fe6b54d..6288cb87b 100644 --- a/backend/LexBoxApi/GraphQL/LexQueries.cs +++ b/backend/LexBoxApi/GraphQL/LexQueries.cs @@ -54,6 +54,34 @@ public IQueryable DraftProjects(LexBoxDbContext context) return context.DraftProjects; } + public record ProjectsByLangCodeAndOrgInput(Guid OrgId, string LangCode); + [UseProjection] + [UseSorting] + public IQueryable ProjectsByLangCodeAndOrg(LoggedInContext loggedInContext, LexBoxDbContext context, IPermissionService permissionService, ProjectsByLangCodeAndOrgInput input) + { + var userId = loggedInContext.User.Id; + var authorized = loggedInContext.User.IsAdmin || permissionService.IsOrgMember(input.OrgId); + if (!authorized) throw new UnauthorizedAccessException(); + var query = context.Projects.Where(p => + p.Organizations.Any(o => o.Id == input.OrgId) && + p.FlexProjectMetadata != null && + p.FlexProjectMetadata.WritingSystems != null && + p.FlexProjectMetadata.WritingSystems.VernacularWss.Any(ws => + ws.IsActive && ( + ws.Tag == input.LangCode || + ws.Tag == $"qaa-x-{input.LangCode}" || + ws.Tag.StartsWith($"{input.LangCode}-") + ) + ) + ); + // Org admins can see all projects, everyone else can only see non-confidential + if (!permissionService.CanEditOrg(input.OrgId)) + { + query = query.Where(p => p.IsConfidential == false); + } + return query; + } + [UseSingleOrDefault] [UseProjection] public async Task> ProjectById(LexBoxDbContext context, IPermissionService permissionService, Guid projectId) diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 6e3accb8b..b299b70b8 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -409,6 +409,7 @@ type Query { projects(withDeleted: Boolean! = false where: ProjectFilterInput orderBy: [ProjectSortInput!]): [Project!]! @authorize(policy: "AdminRequiredPolicy") myDraftProjects(orderBy: [DraftProjectSortInput!]): [DraftProject!]! draftProjects(where: DraftProjectFilterInput orderBy: [DraftProjectSortInput!]): [DraftProject!]! @authorize(policy: "AdminRequiredPolicy") + projectsByLangCodeAndOrg(input: ProjectsByLangCodeAndOrgInput! orderBy: [ProjectSortInput!]): [Project!]! projectById(projectId: UUID!): Project projectByCode(code: String!): Project draftProjectByCode(code: String!): DraftProject @authorize(policy: "AdminRequiredPolicy") @@ -951,6 +952,11 @@ input ProjectWritingSystemsFilterInput { analysisWss: ListFilterInputTypeOfFLExWsIdFilterInput } +input ProjectsByLangCodeAndOrgInput { + orgId: UUID! + langCode: String! +} + input RemoveProjectFromOrgInput { orgId: UUID! projectId: UUID! From 76cd9d847282a3e079fe30a1b76bb5c1cc8e0ae1 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 24 Jul 2024 13:50:53 +0700 Subject: [PATCH 02/31] 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 ac68f3920..77e244baa 100644 --- a/frontend/src/lib/util/time.ts +++ b/frontend/src/lib/util/time.ts @@ -78,3 +78,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 52121c449..cf01535b6 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; +} From 0943e118ddd4d80c9498df730530f8db8eee543a Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 24 Jul 2024 16:13:00 +0700 Subject: [PATCH 03/31] Add constants for lang tag lookup --- .../LexBoxApi/Services/LangTagConstants.cs | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 backend/LexBoxApi/Services/LangTagConstants.cs diff --git a/backend/LexBoxApi/Services/LangTagConstants.cs b/backend/LexBoxApi/Services/LangTagConstants.cs new file mode 100644 index 000000000..320830611 --- /dev/null +++ b/backend/LexBoxApi/Services/LangTagConstants.cs @@ -0,0 +1,376 @@ +namespace LexBoxApi.Services; + +public static class LangTagConstants +{ + public static readonly Dictionary TwoToThree = new() + { + // Extracted from langtags.json + { "aa", "aar" }, + { "ab", "abk" }, + { "ae", "ave" }, + { "af", "afr" }, + { "ak", "aka" }, + { "am", "amh" }, + { "an", "arg" }, + { "ar", "ara" }, + { "as", "asm" }, + { "av", "ava" }, + { "ay", "aym" }, + { "az", "aze" }, + { "ba", "bak" }, + { "be", "bel" }, + { "bg", "bul" }, + { "bi", "bis" }, + { "bm", "bam" }, + { "bn", "ben" }, + { "bo", "bod" }, + { "br", "bre" }, + { "bs", "bos" }, + { "ca", "cat" }, + { "ce", "che" }, + { "ch", "cha" }, + { "co", "cos" }, + { "cr", "cre" }, + { "cs", "ces" }, + { "cu", "chu" }, + { "cv", "chv" }, + { "cy", "cym" }, + { "da", "dan" }, + { "de", "deu" }, + { "dv", "div" }, + { "dz", "dzo" }, + { "ee", "ewe" }, + { "el", "ell" }, + { "en", "eng" }, + { "eo", "epo" }, + { "es", "spa" }, + { "et", "est" }, + { "eu", "eus" }, + { "fa", "fas" }, + { "ff", "ful" }, + { "fi", "fin" }, + { "fj", "fij" }, + { "fo", "fao" }, + { "fr", "fra" }, + { "fy", "fry" }, + { "ga", "gle" }, + { "gd", "gla" }, + { "gl", "glg" }, + { "gn", "grn" }, + { "gu", "guj" }, + { "gv", "glv" }, + { "ha", "hau" }, + { "he", "heb" }, + { "hi", "hin" }, + { "ho", "hmo" }, + { "hr", "hrv" }, + { "ht", "hat" }, + { "hu", "hun" }, + { "hy", "hye" }, + { "hz", "her" }, + { "ia", "ina" }, + { "id", "ind" }, + { "ie", "ile" }, + { "ig", "ibo" }, + { "ii", "iii" }, + { "ik", "ipk" }, + { "io", "ido" }, + { "is", "isl" }, + { "it", "ita" }, + { "iu", "iku" }, + { "ja", "jpn" }, + { "jv", "jav" }, + { "ka", "kat" }, + { "kg", "kon" }, + { "ki", "kik" }, + { "kj", "kua" }, + { "kk", "kaz" }, + { "kl", "kal" }, + { "km", "khm" }, + { "kn", "kan" }, + { "ko", "kor" }, + { "kr", "kau" }, + { "ks", "kas" }, + { "ku", "kur" }, + { "kv", "kom" }, + { "kw", "cor" }, + { "ky", "kir" }, + { "la", "lat" }, + { "lb", "ltz" }, + { "lg", "lug" }, + { "li", "lim" }, + { "ln", "lin" }, + { "lo", "lao" }, + { "lt", "lit" }, + { "lu", "lub" }, + { "lv", "lav" }, + { "mg", "mlg" }, + { "mh", "mah" }, + { "mi", "mri" }, + { "mk", "mkd" }, + { "ml", "mal" }, + { "mn", "mon" }, + { "mr", "mar" }, + { "ms", "msa" }, + { "mt", "mlt" }, + { "my", "mya" }, + { "na", "nau" }, + { "nb", "nob" }, + { "nd", "nde" }, + { "ne", "nep" }, + { "ng", "ndo" }, + { "nl", "nld" }, + { "nn", "nno" }, + { "no", "nor" }, + { "nr", "nbl" }, + { "nv", "nav" }, + { "ny", "nya" }, + { "oc", "oci" }, + { "oj", "oji" }, + { "om", "orm" }, + { "or", "ori" }, + { "os", "oss" }, + { "pa", "pan" }, + { "pl", "pol" }, + { "ps", "pus" }, + { "pt", "por" }, + { "qu", "que" }, + { "rm", "roh" }, + { "rn", "run" }, + { "ro", "ron" }, + { "ru", "rus" }, + { "rw", "kin" }, + { "sa", "san" }, + { "sc", "srd" }, + { "sd", "snd" }, + { "se", "sme" }, + { "sg", "sag" }, + { "sh", "hbs" }, + { "si", "sin" }, + { "sk", "slk" }, + { "sl", "slv" }, + { "sm", "smo" }, + { "sn", "sna" }, + { "so", "som" }, + { "sq", "sqi" }, + { "sr", "srp" }, + { "ss", "ssw" }, + { "st", "sot" }, + { "su", "sun" }, + { "sv", "swe" }, + { "sw", "swa" }, + { "ta", "tam" }, + { "te", "tel" }, + { "tg", "tgk" }, + { "th", "tha" }, + { "ti", "tir" }, + { "tk", "tuk" }, + { "tl", "tgl" }, + { "tn", "tsn" }, + { "to", "ton" }, + { "tr", "tur" }, + { "ts", "tso" }, + { "tt", "tat" }, + { "ty", "tah" }, + { "ug", "uig" }, + { "uk", "ukr" }, + { "ur", "urd" }, + { "uz", "uzb" }, + { "ve", "ven" }, + { "vi", "vie" }, + { "vo", "vol" }, + { "wa", "wln" }, + { "wo", "wol" }, + { "xh", "xho" }, + { "yi", "yid" }, + { "yo", "yor" }, + { "za", "zha" }, + { "zu", "zul" }, + }; + + public static readonly Dictionary ThreeToTwo = new() + { + // Extracted from langtags.json + { "aar", "aa" }, + { "abk", "ab" }, + { "ave", "ae" }, + { "afr", "af" }, + { "aka", "ak" }, + { "amh", "am" }, + { "arg", "an" }, + { "ara", "ar" }, + { "asm", "as" }, + { "ava", "av" }, + { "aym", "ay" }, + { "aze", "az" }, + { "bak", "ba" }, + { "bel", "be" }, + { "bul", "bg" }, + { "bis", "bi" }, + { "bam", "bm" }, + { "ben", "bn" }, + { "bod", "bo" }, + { "bre", "br" }, + { "bos", "bs" }, + { "cat", "ca" }, + { "che", "ce" }, + { "cha", "ch" }, + { "cos", "co" }, + { "cre", "cr" }, + { "ces", "cs" }, + { "chu", "cu" }, + { "chv", "cv" }, + { "cym", "cy" }, + { "dan", "da" }, + { "deu", "de" }, + { "div", "dv" }, + { "dzo", "dz" }, + { "ewe", "ee" }, + { "ell", "el" }, + { "eng", "en" }, + { "epo", "eo" }, + { "spa", "es" }, + { "est", "et" }, + { "eus", "eu" }, + { "fas", "fa" }, + { "ful", "ff" }, + { "fin", "fi" }, + { "fij", "fj" }, + { "fao", "fo" }, + { "fra", "fr" }, + { "fry", "fy" }, + { "gle", "ga" }, + { "gla", "gd" }, + { "glg", "gl" }, + { "grn", "gn" }, + { "guj", "gu" }, + { "glv", "gv" }, + { "hau", "ha" }, + { "heb", "he" }, + { "hin", "hi" }, + { "hmo", "ho" }, + { "hrv", "hr" }, + { "hat", "ht" }, + { "hun", "hu" }, + { "hye", "hy" }, + { "her", "hz" }, + { "ina", "ia" }, + { "ind", "id" }, + { "ile", "ie" }, + { "ibo", "ig" }, + { "iii", "ii" }, + { "ipk", "ik" }, + { "ido", "io" }, + { "isl", "is" }, + { "ita", "it" }, + { "iku", "iu" }, + { "jpn", "ja" }, + { "jav", "jv" }, + { "kat", "ka" }, + { "kon", "kg" }, + { "kik", "ki" }, + { "kua", "kj" }, + { "kaz", "kk" }, + { "kal", "kl" }, + { "khm", "km" }, + { "kan", "kn" }, + { "kor", "ko" }, + { "kau", "kr" }, + { "kas", "ks" }, + { "kur", "ku" }, + { "kom", "kv" }, + { "cor", "kw" }, + { "kir", "ky" }, + { "lat", "la" }, + { "ltz", "lb" }, + { "lug", "lg" }, + { "lim", "li" }, + { "lin", "ln" }, + { "lao", "lo" }, + { "lit", "lt" }, + { "lub", "lu" }, + { "lav", "lv" }, + { "mlg", "mg" }, + { "mah", "mh" }, + { "mri", "mi" }, + { "mkd", "mk" }, + { "mal", "ml" }, + { "mon", "mn" }, + { "mar", "mr" }, + { "msa", "ms" }, + { "mlt", "mt" }, + { "mya", "my" }, + { "nau", "na" }, + { "nob", "nb" }, + { "nde", "nd" }, + { "nep", "ne" }, + { "ndo", "ng" }, + { "nld", "nl" }, + { "nno", "nn" }, + { "nor", "no" }, + { "nbl", "nr" }, + { "nav", "nv" }, + { "nya", "ny" }, + { "oci", "oc" }, + { "oji", "oj" }, + { "orm", "om" }, + { "ori", "or" }, + { "oss", "os" }, + { "pan", "pa" }, + { "pol", "pl" }, + { "pus", "ps" }, + { "por", "pt" }, + { "que", "qu" }, + { "roh", "rm" }, + { "run", "rn" }, + { "ron", "ro" }, + { "rus", "ru" }, + { "kin", "rw" }, + { "san", "sa" }, + { "srd", "sc" }, + { "snd", "sd" }, + { "sme", "se" }, + { "sag", "sg" }, + { "hbs", "sh" }, + { "sin", "si" }, + { "slk", "sk" }, + { "slv", "sl" }, + { "smo", "sm" }, + { "sna", "sn" }, + { "som", "so" }, + { "sqi", "sq" }, + { "srp", "sr" }, + { "ssw", "ss" }, + { "sot", "st" }, + { "sun", "su" }, + { "swe", "sv" }, + { "swa", "sw" }, + { "tam", "ta" }, + { "tel", "te" }, + { "tgk", "tg" }, + { "tha", "th" }, + { "tir", "ti" }, + { "tuk", "tk" }, + { "tgl", "tl" }, + { "tsn", "tn" }, + { "ton", "to" }, + { "tur", "tr" }, + { "tso", "ts" }, + { "tat", "tt" }, + { "tah", "ty" }, + { "uig", "ug" }, + { "ukr", "uk" }, + { "urd", "ur" }, + { "uzb", "uz" }, + { "ven", "ve" }, + { "vie", "vi" }, + { "vol", "vo" }, + { "wln", "wa" }, + { "wol", "wo" }, + { "xho", "xh" }, + { "yid", "yi" }, + { "yor", "yo" }, + { "zha", "za" }, + { "zul", "zu" }, + }; +} From 47a1ded9a9153712dca287cec7735246f571920f Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 24 Jul 2024 16:19:23 +0700 Subject: [PATCH 04/31] Use lookup table to convert 3-char codes to 2-char If someone types the language tag "eng", we want to search for "en" because that's how FLEx will have stored it as a writing system. --- backend/LexBoxApi/GraphQL/LexQueries.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/LexBoxApi/GraphQL/LexQueries.cs b/backend/LexBoxApi/GraphQL/LexQueries.cs index 6288cb87b..112fba092 100644 --- a/backend/LexBoxApi/GraphQL/LexQueries.cs +++ b/backend/LexBoxApi/GraphQL/LexQueries.cs @@ -61,6 +61,8 @@ public IQueryable ProjectsByLangCodeAndOrg(LoggedInContext loggedInCont { var userId = loggedInContext.User.Id; var authorized = loggedInContext.User.IsAdmin || permissionService.IsOrgMember(input.OrgId); + // Convert 3-letter code to 2-letter code if relevant, otherwise leave as-is + var langCode = Services.LangTagConstants.ThreeToTwo.GetValueOrDefault(input.LangCode, input.LangCode); if (!authorized) throw new UnauthorizedAccessException(); var query = context.Projects.Where(p => p.Organizations.Any(o => o.Id == input.OrgId) && @@ -68,9 +70,9 @@ public IQueryable ProjectsByLangCodeAndOrg(LoggedInContext loggedInCont p.FlexProjectMetadata.WritingSystems != null && p.FlexProjectMetadata.WritingSystems.VernacularWss.Any(ws => ws.IsActive && ( - ws.Tag == input.LangCode || - ws.Tag == $"qaa-x-{input.LangCode}" || - ws.Tag.StartsWith($"{input.LangCode}-") + ws.Tag == langCode || + ws.Tag == $"qaa-x-{langCode}" || + ws.Tag.StartsWith($"{langCode}-") ) ) ); From b015ce98697ab250c3275b9705e4831be5f2a6dc Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 24 Jul 2024 16:46:44 +0700 Subject: [PATCH 05/31] Update two-to-three lang tags constants The langtags.json file I used initially was a couple years out of date, and there was one new 2-letter code assignment since then. Updating the LangTagConstants.cs file accordingly. --- backend/LexBoxApi/Services/LangTagConstants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/LexBoxApi/Services/LangTagConstants.cs b/backend/LexBoxApi/Services/LangTagConstants.cs index 320830611..ccc1085bd 100644 --- a/backend/LexBoxApi/Services/LangTagConstants.cs +++ b/backend/LexBoxApi/Services/LangTagConstants.cs @@ -131,6 +131,7 @@ public static class LangTagConstants { "or", "ori" }, { "os", "oss" }, { "pa", "pan" }, + { "pi", "pli" }, { "pl", "pol" }, { "ps", "pus" }, { "pt", "por" }, @@ -317,6 +318,7 @@ public static class LangTagConstants { "ori", "or" }, { "oss", "os" }, { "pan", "pa" }, + { "pli", "pi" }, { "pol", "pl" }, { "pus", "ps" }, { "por", "pt" }, From 0f65d0c7b4645f1b3b7a69a027d8147ebfc28f75 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 26 Jul 2024 15:07:14 +0700 Subject: [PATCH 06/31] Move "related projects" section to above description Could put it below the description instead, but we might want to save the user some typing in case they decide not to create the project after all, so we'll put the related projects list above the description so they'll see it before starting to type. --- .../project/create/+page.svelte | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/routes/(authenticated)/project/create/+page.svelte b/frontend/src/routes/(authenticated)/project/create/+page.svelte index cf01535b6..01f7ecd38 100644 --- a/frontend/src/routes/(authenticated)/project/create/+page.svelte +++ b/frontend/src/routes/(authenticated)/project/create/+page.svelte @@ -227,6 +227,16 @@ error={$errors.languageCode} /> + + + + + {#if $relatedProjects?.length}
Possibly related projects: @@ -238,16 +248,6 @@
{/if} - - - - -