From 135954fdce7b4e252b3bf36c8fb76b5953353090 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 29 Oct 2024 09:53:05 +0100 Subject: [PATCH 01/30] Allow project managers to always remove their projects from orgs (#1150) * Allow project managers to always remove their projects from orgs * Move permission login to permission service --- backend/LexBoxApi/GraphQL/OrgMutations.cs | 3 +-- backend/LexBoxApi/Services/PermissionService.cs | 9 +++++++++ backend/LexCore/ServiceInterfaces/IPermissionService.cs | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/LexBoxApi/GraphQL/OrgMutations.cs b/backend/LexBoxApi/GraphQL/OrgMutations.cs index df62a3995..6ffb4bd5d 100644 --- a/backend/LexBoxApi/GraphQL/OrgMutations.cs +++ b/backend/LexBoxApi/GraphQL/OrgMutations.cs @@ -144,12 +144,11 @@ public async Task> RemoveProjectFromOrg( { var org = await dbContext.Orgs.Include(o => o.Members).SingleOrDefaultAsync(o => o.Id == orgId); NotFoundException.ThrowIfNull(org); - permissionService.AssertCanAddProjectToOrg(org); var project = await dbContext.Projects.Where(p => p.Id == projectId) .Include(p => p.Organizations) .SingleOrDefaultAsync(); NotFoundException.ThrowIfNull(project); - await permissionService.AssertCanManageProject(projectId); + await permissionService.AssertCanRemoveProjectFromOrg(org, projectId); var foundOrg = project.Organizations.FirstOrDefault(o => o.Id == orgId); if (foundOrg is not null) { diff --git a/backend/LexBoxApi/Services/PermissionService.cs b/backend/LexBoxApi/Services/PermissionService.cs index fcaec39e5..6656d8eb2 100644 --- a/backend/LexBoxApi/Services/PermissionService.cs +++ b/backend/LexBoxApi/Services/PermissionService.cs @@ -234,4 +234,13 @@ public void AssertCanAddProjectToOrg(Organization org) if (org.Members.Any(m => m.UserId == User.Id)) return; throw new UnauthorizedAccessException(); } + + public async ValueTask AssertCanRemoveProjectFromOrg(Organization org, Guid projectId) + { + // Org managers can kick projects out and project managers can pull projects out + if (!CanEditOrg(org.Id) && !await CanManageProject(projectId)) + { + throw new UnauthorizedAccessException(); + } + } } diff --git a/backend/LexCore/ServiceInterfaces/IPermissionService.cs b/backend/LexCore/ServiceInterfaces/IPermissionService.cs index dec18d5bd..13e488fc1 100644 --- a/backend/LexCore/ServiceInterfaces/IPermissionService.cs +++ b/backend/LexCore/ServiceInterfaces/IPermissionService.cs @@ -43,4 +43,5 @@ public interface IPermissionService void AssertCanEditOrg(Organization org); void AssertCanEditOrg(Guid orgId); void AssertCanAddProjectToOrg(Organization org); + ValueTask AssertCanRemoveProjectFromOrg(Organization org, Guid projectId); } From a8ac83ee44226e973d297f34f3a113264b222ea7 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 24 Oct 2024 15:55:51 +0200 Subject: [PATCH 02/30] Catch TryAgain Socket Exceptions when waiting for postgres in dev --- backend/LexData/DbStartupService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/LexData/DbStartupService.cs b/backend/LexData/DbStartupService.cs index 9f6108362..c75abf841 100644 --- a/backend/LexData/DbStartupService.cs +++ b/backend/LexData/DbStartupService.cs @@ -96,6 +96,10 @@ private async Task TryMigrate(DbContext dbContext, CancellationToken cance { return false; } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TryAgain) + { + return false; + } } public Task StopAsync(CancellationToken cancellationToken) From 18ad52234ce14700b3d77517e7a10fa211053725 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 25 Oct 2024 11:40:46 +0200 Subject: [PATCH 03/30] Make about link absolute so it works on every page --- frontend/src/lib/layout/Footer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/layout/Footer.svelte b/frontend/src/lib/layout/Footer.svelte index 488994b19..a7782eabf 100644 --- a/frontend/src/lib/layout/Footer.svelte +++ b/frontend/src/lib/layout/Footer.svelte @@ -24,7 +24,7 @@
- {$t('about.title')} + {$t('about.title')} {$t('footer.terms_of_use')} {$t('footer.contact')} {$t('footer.privacy_policy')} From 4e7519bbab245c0e998698cdf3fb8f69e91409da Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 24 Oct 2024 16:39:49 +0200 Subject: [PATCH 04/30] Fix Part of speech not rendered in dictionary preview on page load --- frontend/tests/viewerPage.test.ts | 12 ++++++------ frontend/viewer/src/lib/DictionaryEntry.svelte | 12 +++--------- .../entry-editor/object-editors/SenseEditor.svelte | 6 +++--- frontend/viewer/src/lib/parts-of-speech.ts | 14 ++++++++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/tests/viewerPage.test.ts b/frontend/tests/viewerPage.test.ts index ad65cdf59..765736635 100644 --- a/frontend/tests/viewerPage.test.ts +++ b/frontend/tests/viewerPage.test.ts @@ -1,10 +1,10 @@ import * as testEnv from './envVars'; -import { UserDashboardPage } from './pages/userDashboardPage'; -import { ViewerPage } from './pages/viewerPage'; -import { expect } from '@playwright/test'; -import { loginAs } from './utils/authHelpers'; -import { test } from './fixtures'; +import {UserDashboardPage} from './pages/userDashboardPage'; +import {ViewerPage} from './pages/viewerPage'; +import {expect} from '@playwright/test'; +import {loginAs} from './utils/authHelpers'; +import {test} from './fixtures'; test('navigate to viewer', async ({ page }) => { // Step 1: Login @@ -58,7 +58,7 @@ test('entry details', async ({ page }) => { // -- Dictionary Preview const expectPreview = expect(viewerPage.entryDictionaryPreview); await expectPreview.toContainText('nthembe'); - await expectPreview.toContainText(' N. '); + await expectPreview.toContainText(' Nome '); await expectPreview.toContainText(' Eng '); await expectPreview.toContainText('animal skin alone, after it is taken off the body'); await expectPreview.toContainText(' Por '); diff --git a/frontend/viewer/src/lib/DictionaryEntry.svelte b/frontend/viewer/src/lib/DictionaryEntry.svelte index f40028620..e2de23d24 100644 --- a/frontend/viewer/src/lib/DictionaryEntry.svelte +++ b/frontend/viewer/src/lib/DictionaryEntry.svelte @@ -45,13 +45,7 @@ .map(ws => ({ws: ws.wsId, value: headword(entry, ws.wsId)})) .filter(({value}) => !!value); - const partsOfSpeech = usePartsOfSpeech(); - function partOfSpeechLabel(id: string | undefined): string | undefined { - if (!id) return undefined; - const partOfSpeech = $partsOfSpeech.find(pos => pos.id === id); - if (!partOfSpeech) return undefined; - return pickBestAlternative(partOfSpeech.name, 'analysis', $allWritingSystems) - } + const partsOfSpeech = usePartsOfSpeech(allWritingSystems);
@@ -68,9 +62,9 @@
{i + 1} · {/if} - {@const partOfSpeech = partOfSpeechLabel(sense.partOfSpeechId)} + {@const partOfSpeech = $partsOfSpeech.find(pos => pos.id === sense.partOfSpeechId)?.label} {#if partOfSpeech} - {partOfSpeech}. + {partOfSpeech} {/if} {#each $allWritingSystems.analysis as ws} diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditor.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditor.svelte index 2b3e94593..6d186a90c 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditor.svelte @@ -12,9 +12,9 @@ export let sense: ISense; export let readonly: boolean = false; - const partsOfSpeech = usePartsOfSpeech(); - const semanticDomains = useSemanticDomains(); const writingSystems = useWritingSystems(); + const partsOfSpeech = usePartsOfSpeech(writingSystems); + const semanticDomains = useSemanticDomains(); function setSemanticDomains(ids: string[]): void { sense.semanticDomains = ids.map(id => $semanticDomains.find(sd => sd.id === id)).filter((sd): sd is SemanticDomain => !!sd); } @@ -34,7 +34,7 @@ wsType="analysis" /> ({value: pos.id, label: pickBestAlternative(pos.name, 'analysis', $writingSystems)}))} + options={$partsOfSpeech.map(pos => ({value: pos.id, label: pos.label}))} {readonly} id="partOfSpeechId" wsType="first-analysis" /> diff --git a/frontend/viewer/src/lib/parts-of-speech.ts b/frontend/viewer/src/lib/parts-of-speech.ts index 69a20fc02..db779fe3b 100644 --- a/frontend/viewer/src/lib/parts-of-speech.ts +++ b/frontend/viewer/src/lib/parts-of-speech.ts @@ -1,10 +1,13 @@ import {derived, type Readable, type Writable, writable} from 'svelte/store'; -import type {PartOfSpeech} from './mini-lcm'; +import type {PartOfSpeech, WritingSystems} from './mini-lcm'; import {useLexboxApi} from './services/service-provider'; +import {pickBestAlternative} from './utils'; + +type LabeledPartOfSpeech = PartOfSpeech & {label: string}; let partsOfSpeechStore: Writable | null = null; -export function usePartsOfSpeech(): Readable { +export function usePartsOfSpeech(writingSystemsStore: Readable): Readable { if (partsOfSpeechStore === null) { partsOfSpeechStore = writable([], (set) => { useLexboxApi().GetPartsOfSpeech().then(partsOfSpeech => { @@ -15,7 +18,10 @@ export function usePartsOfSpeech(): Readable { }); }); } - return derived(partsOfSpeechStore, (partsOfSpeech) => { - return partsOfSpeech ?? []; + return derived([partsOfSpeechStore, writingSystemsStore], ([partsOfSpeech, writingSystems]) => { + return (partsOfSpeech ?? []).map(partOfSpeech => ({ + ...partOfSpeech, + label: pickBestAlternative(partOfSpeech.name, 'analysis', writingSystems), + })); }); } From 20de1743dd6b6bc0a7dea431312585bc9cb71c83 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 23 Oct 2024 12:26:28 +0200 Subject: [PATCH 05/30] Add organizations breadcrumb to org page --- frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte index 79e13d03e..b289de4aa 100644 --- a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte +++ b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte @@ -1,5 +1,5 @@ +{$t('org.table.title')} + {#if isMember} From 81a77f296b35d3e2ef79f517e1bc4041f49bfe52 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 23 Oct 2024 13:45:05 +0200 Subject: [PATCH 06/30] Convert organizations list page to HeaderPage --- frontend/src/lib/icons/Icon.svelte | 6 ++- frontend/src/lib/layout/HeaderPage.svelte | 2 +- .../(authenticated)/org/list/+page.svelte | 49 +++++++++---------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/frontend/src/lib/icons/Icon.svelte b/frontend/src/lib/icons/Icon.svelte index 435f41abc..99fa52775 100644 --- a/frontend/src/lib/icons/Icon.svelte +++ b/frontend/src/lib/icons/Icon.svelte @@ -10,8 +10,12 @@ export let size: IconSize = 'text-lg'; export let color: `text-${string}` | undefined = undefined; export let pale = false; + // For pixel perfect text alignment, because the svgs often contain vertical white-space + export let y: string | undefined = undefined; + + $: transform = y ? `translateY(${y})` : ''; {#if icon} - + {/if} diff --git a/frontend/src/lib/layout/HeaderPage.svelte b/frontend/src/lib/layout/HeaderPage.svelte index 7b2b2801d..cf5321be9 100644 --- a/frontend/src/lib/layout/HeaderPage.svelte +++ b/frontend/src/lib/layout/HeaderPage.svelte @@ -13,7 +13,7 @@
-

+

{#if $$slots.title} {:else} diff --git a/frontend/src/routes/(authenticated)/org/list/+page.svelte b/frontend/src/routes/(authenticated)/org/list/+page.svelte index 7c9fc5c73..3234098bb 100644 --- a/frontend/src/routes/(authenticated)/org/list/+page.svelte +++ b/frontend/src/routes/(authenticated)/org/list/+page.svelte @@ -3,7 +3,7 @@ import type { OrgListPageQuery } from '$lib/gql/types'; import t, { date, number } from '$lib/i18n'; import { Icon } from '$lib/icons'; - import { Page } from '$lib/layout'; + import { HeaderPage } from '$lib/layout'; import AdminContent from '$lib/layout/AdminContent.svelte'; import { getSearchParams, queryParam } from '$lib/util/query-params'; import type { PageData } from './$types'; @@ -70,32 +70,27 @@ TODO: * Paging --> - - -
- - - -

- {$t('org.table.title')} - -

-
-
- -
+ + + + + {$t('org.create.title')} + + + + + + {$t('org.table.title')} + + + + -
@@ -133,4 +128,4 @@ TODO:
- + From a9efef89a5a11a7c243ffcbfc4bfac80c7717969 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 23 Oct 2024 13:51:16 +0200 Subject: [PATCH 07/30] Make tab lists scrollable on narrow screens --- frontend/src/routes/(authenticated)/admin/AdminTabs.svelte | 2 +- frontend/src/routes/(authenticated)/org/[org_id]/OrgTabs.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/(authenticated)/admin/AdminTabs.svelte b/frontend/src/routes/(authenticated)/admin/AdminTabs.svelte index ffac799ab..d0026cb21 100644 --- a/frontend/src/routes/(authenticated)/admin/AdminTabs.svelte +++ b/frontend/src/routes/(authenticated)/admin/AdminTabs.svelte @@ -20,7 +20,7 @@ export let activeTab: AdminTabId = 'projects'; -