From bbf97c7861a12fa72a7b6afee1b014173fd7c94e Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Fri, 13 Dec 2024 15:25:53 +0100 Subject: [PATCH 1/5] Introduce LoadableData type for organizations --- apps/desktop/src/lib/ai/butlerClient.ts | 2 +- apps/desktop/src/lib/ai/service.test.ts | 2 +- apps/desktop/src/lib/ai/service.ts | 2 +- .../lib/backend/projectCloudSync.svelte.ts | 2 +- apps/desktop/src/lib/backend/projects.ts | 2 +- .../src/lib/components/ShareIssueModal.svelte | 2 +- apps/desktop/src/lib/stores/user.ts | 2 +- apps/desktop/src/routes/+layout.svelte | 2 +- apps/desktop/src/routes/+layout.ts | 2 +- .../src/routes/[projectId]/+layout.svelte | 2 +- .../settings/organizations/+page.svelte | 23 +-- apps/web/src/lib/user/userService.ts | 2 +- apps/web/src/routes/+layout.svelte | 2 +- .../web/src/routes/organizations/+page.svelte | 2 +- apps/web/src/routes/projects/+page.svelte | 2 +- .../branches/[branchId]/stack/+page.svelte | 2 +- .../[branchId]/stack/[changeId]/+page.svelte | 2 +- .../[repositoryId]/+layout.svelte | 2 +- .../src/lib/cloud/repositories/service.ts | 2 +- .../shared/src/lib/cloud/stacks/service.ts | 2 +- packages/shared/src/lib/feeds/service.ts | 2 +- .../shared/src/lib/network/Loading.svelte | 24 +++ .../src/lib/{ => network}/httpClient.ts | 3 +- packages/shared/src/lib/network/loadable.ts | 4 + packages/shared/src/lib/network/types.ts | 8 + .../organizations/OrganizationModal.svelte | 138 ++++++++++-------- .../lib/organizations/organizationService.ts | 57 ++++++-- .../lib/organizations/organizationsSlice.ts | 7 +- .../src/lib/organizations/projectService.ts | 2 +- .../shared/src/lib/organizations/types.ts | 4 + packages/shared/src/lib/users/userService.ts | 2 +- 31 files changed, 200 insertions(+), 112 deletions(-) create mode 100644 packages/shared/src/lib/network/Loading.svelte rename packages/shared/src/lib/{ => network}/httpClient.ts (95%) create mode 100644 packages/shared/src/lib/network/loadable.ts create mode 100644 packages/shared/src/lib/network/types.ts diff --git a/apps/desktop/src/lib/ai/butlerClient.ts b/apps/desktop/src/lib/ai/butlerClient.ts index 8dbe79560d..95b79af5ad 100644 --- a/apps/desktop/src/lib/ai/butlerClient.ts +++ b/apps/desktop/src/lib/ai/butlerClient.ts @@ -7,7 +7,7 @@ import { import { ModelKind, type AIClient, type AIEvalOptions, type Prompt } from '$lib/ai/types'; import { andThenAsync, ok, wrapAsync, type Result } from '$lib/result'; import { stringStreamGenerator } from '$lib/utils/promise'; -import type { HttpClient } from '@gitbutler/shared/httpClient'; +import type { HttpClient } from '@gitbutler/shared/network/httpClient'; function splitPromptMessagesIfNecessary( modelKind: ModelKind, diff --git a/apps/desktop/src/lib/ai/service.test.ts b/apps/desktop/src/lib/ai/service.test.ts index 870b66f6e7..a37f494337 100644 --- a/apps/desktop/src/lib/ai/service.test.ts +++ b/apps/desktop/src/lib/ai/service.test.ts @@ -24,7 +24,7 @@ import { import { buildFailureFromAny, ok, unwrap, type Result } from '$lib/result'; import { TokenMemoryService } from '$lib/stores/tokenMemoryService'; import { Hunk } from '$lib/vbranches/types'; -import { HttpClient } from '@gitbutler/shared/httpClient'; +import { HttpClient } from '@gitbutler/shared/network/httpClient'; import { plainToInstance } from 'class-transformer'; import { get } from 'svelte/store'; import { expect, test, describe, vi } from 'vitest'; diff --git a/apps/desktop/src/lib/ai/service.ts b/apps/desktop/src/lib/ai/service.ts index fd5c46c776..d32c092381 100644 --- a/apps/desktop/src/lib/ai/service.ts +++ b/apps/desktop/src/lib/ai/service.ts @@ -21,7 +21,7 @@ import { get } from 'svelte/store'; import type { GitConfigService } from '$lib/backend/gitConfigService'; import type { SecretsService } from '$lib/secrets/secretsService'; import type { TokenMemoryService } from '$lib/stores/tokenMemoryService'; -import type { HttpClient } from '@gitbutler/shared/httpClient'; +import type { HttpClient } from '@gitbutler/shared/network/httpClient'; const maxDiffLengthLimitForAPI = 5000; const prDescriptionTokenLimit = 4096; diff --git a/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts b/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts index 439a9493b7..4513aecc92 100644 --- a/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts +++ b/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts @@ -2,7 +2,7 @@ import { registerInterest } from '@gitbutler/shared/interest/registerInterestFun import { projectsSelectors } from '@gitbutler/shared/organizations/projectsSlice'; import { readableToReactive } from '@gitbutler/shared/reactiveUtils.svelte'; import type { ProjectService, ProjectsService } from '$lib/backend/projects'; -import type { HttpClient } from '@gitbutler/shared/httpClient'; +import type { HttpClient } from '@gitbutler/shared/network/httpClient'; import type { ProjectService as CloudProjectService } from '@gitbutler/shared/organizations/projectService'; import type { AppProjectsState } from '@gitbutler/shared/redux/store.svelte'; diff --git a/apps/desktop/src/lib/backend/projects.ts b/apps/desktop/src/lib/backend/projects.ts index 29669657d9..b11ffbb5b6 100644 --- a/apps/desktop/src/lib/backend/projects.ts +++ b/apps/desktop/src/lib/backend/projects.ts @@ -6,7 +6,7 @@ import { persisted } from '@gitbutler/shared/persisted'; import { open } from '@tauri-apps/plugin-dialog'; import { plainToInstance } from 'class-transformer'; import { derived, get, writable, type Readable } from 'svelte/store'; -import type { HttpClient } from '@gitbutler/shared/httpClient'; +import type { HttpClient } from '@gitbutler/shared/network/httpClient'; import { goto } from '$app/navigation'; export type KeyType = 'gitCredentialsHelper' | 'local' | 'systemExecutable'; diff --git a/apps/desktop/src/lib/components/ShareIssueModal.svelte b/apps/desktop/src/lib/components/ShareIssueModal.svelte index ec9659ff2e..4b3ecf2384 100644 --- a/apps/desktop/src/lib/components/ShareIssueModal.svelte +++ b/apps/desktop/src/lib/components/ShareIssueModal.svelte @@ -4,7 +4,7 @@ import { User } from '$lib/stores/user'; import * as toasts from '$lib/utils/toasts'; import { getContext, getContextStore } from '@gitbutler/shared/context'; - import { HttpClient } from '@gitbutler/shared/httpClient'; + import { HttpClient } from '@gitbutler/shared/network/httpClient'; import Button from '@gitbutler/ui/Button.svelte'; import Checkbox from '@gitbutler/ui/Checkbox.svelte'; import Modal from '@gitbutler/ui/Modal.svelte'; diff --git a/apps/desktop/src/lib/stores/user.ts b/apps/desktop/src/lib/stores/user.ts index 6eaa6b9cad..83fb8f6527 100644 --- a/apps/desktop/src/lib/stores/user.ts +++ b/apps/desktop/src/lib/stores/user.ts @@ -4,7 +4,7 @@ import { showError } from '$lib/notifications/toasts'; import { copyToClipboard } from '$lib/utils/clipboard'; import { sleep } from '$lib/utils/sleep'; import { openExternalUrl } from '$lib/utils/url'; -import { type HttpClient } from '@gitbutler/shared/httpClient'; +import { type HttpClient } from '@gitbutler/shared/network/httpClient'; import { plainToInstance } from 'class-transformer'; import { derived, writable } from 'svelte/store'; import type { PostHogWrapper } from '$lib/analytics/posthog'; diff --git a/apps/desktop/src/routes/+layout.svelte b/apps/desktop/src/routes/+layout.svelte index 6c2548ffc7..db39bfca8a 100644 --- a/apps/desktop/src/routes/+layout.svelte +++ b/apps/desktop/src/routes/+layout.svelte @@ -39,7 +39,7 @@ import * as events from '$lib/utils/events'; import { unsubscribe } from '$lib/utils/unsubscribe'; import { FeedService } from '@gitbutler/shared/feeds/service'; - import { HttpClient } from '@gitbutler/shared/httpClient'; + import { HttpClient } from '@gitbutler/shared/network/httpClient'; import { OrganizationService } from '@gitbutler/shared/organizations/organizationService'; import { ProjectService as CloudProjectService } from '@gitbutler/shared/organizations/projectService'; import { AppDispatch, AppState } from '@gitbutler/shared/redux/store.svelte'; diff --git a/apps/desktop/src/routes/+layout.ts b/apps/desktop/src/routes/+layout.ts index 8162b628a3..409da88912 100644 --- a/apps/desktop/src/routes/+layout.ts +++ b/apps/desktop/src/routes/+layout.ts @@ -15,7 +15,7 @@ import { RemotesService } from '$lib/remotes/service'; import { RustSecretService } from '$lib/secrets/secretsService'; import { TokenMemoryService } from '$lib/stores/tokenMemoryService'; import { UserService } from '$lib/stores/user'; -import { HttpClient } from '@gitbutler/shared/httpClient'; +import { HttpClient } from '@gitbutler/shared/network/httpClient'; import { LineManagerFactory } from '@gitbutler/ui/commitLines/lineManager'; import { LineManagerFactory as StackingLineManagerFactory } from '@gitbutler/ui/commitLines/lineManager'; import lscache from 'lscache'; diff --git a/apps/desktop/src/routes/[projectId]/+layout.svelte b/apps/desktop/src/routes/[projectId]/+layout.svelte index 0cc3c3c77a..39d234b264 100644 --- a/apps/desktop/src/routes/[projectId]/+layout.svelte +++ b/apps/desktop/src/routes/[projectId]/+layout.svelte @@ -37,7 +37,7 @@ import { VirtualBranchService } from '$lib/vbranches/virtualBranch'; import { CloudBranchesService } from '@gitbutler/shared/cloud/stacks/service'; import { getContext } from '@gitbutler/shared/context'; - import { HttpClient } from '@gitbutler/shared/httpClient'; + import { HttpClient } from '@gitbutler/shared/network/httpClient'; import { ProjectService as CloudProjectService } from '@gitbutler/shared/organizations/projectService'; import { AppState } from '@gitbutler/shared/redux/store.svelte'; import { DesktopRoutesService, getRoutesService } from '@gitbutler/shared/sharedRoutes'; diff --git a/apps/desktop/src/routes/settings/organizations/+page.svelte b/apps/desktop/src/routes/settings/organizations/+page.svelte index 332ecf0de4..4ce7cbb43e 100644 --- a/apps/desktop/src/routes/settings/organizations/+page.svelte +++ b/apps/desktop/src/routes/settings/organizations/+page.svelte @@ -1,8 +1,9 @@ + +{#if loadable?.type === 'found'} + {@render children(loadable.value)} +{:else if loadable?.type === 'loading'} +

Loading...

+{:else if loadable?.type === 'not-found'} +

Not found

+{:else if loadable?.type === 'error'} +

{loadable.error.message}

+{:else} +

Organization uninitialized

+{/if} diff --git a/packages/shared/src/lib/httpClient.ts b/packages/shared/src/lib/network/httpClient.ts similarity index 95% rename from packages/shared/src/lib/httpClient.ts rename to packages/shared/src/lib/network/httpClient.ts index 60ea55af6d..60803e11a2 100644 --- a/packages/shared/src/lib/httpClient.ts +++ b/packages/shared/src/lib/network/httpClient.ts @@ -1,3 +1,4 @@ +import { ApiError } from '$lib/network/types'; import { derived, get, type Readable } from 'svelte/store'; export const DEFAULT_HEADERS = { @@ -100,7 +101,7 @@ async function parseResponseJSON(response: Response) { if (response.status === 204 || response.status === 205) { return null; } else if (response.status >= 400) { - throw new Error(`HTTP Error ${response.statusText}: ${await response.text()}`); + throw new ApiError(`HTTP Error ${response.statusText}: ${await response.text()}`, response); } else { return await response.json(); } diff --git a/packages/shared/src/lib/network/loadable.ts b/packages/shared/src/lib/network/loadable.ts new file mode 100644 index 0000000000..66f93f6e87 --- /dev/null +++ b/packages/shared/src/lib/network/loadable.ts @@ -0,0 +1,4 @@ +export type LoadableData = + | { type: 'loading' | 'not-found'; id: Id; error?: undefined } + | { type: 'found'; id: Id; value: T; error?: undefined } + | { type: 'error'; id: Id; error: Error }; diff --git a/packages/shared/src/lib/network/types.ts b/packages/shared/src/lib/network/types.ts new file mode 100644 index 0000000000..d853489eb9 --- /dev/null +++ b/packages/shared/src/lib/network/types.ts @@ -0,0 +1,8 @@ +export class ApiError extends Error { + constructor( + message: string, + readonly response: Response + ) { + super(message); + } +} diff --git a/packages/shared/src/lib/organizations/OrganizationModal.svelte b/packages/shared/src/lib/organizations/OrganizationModal.svelte index 8a8f953a08..11042a7fc1 100644 --- a/packages/shared/src/lib/organizations/OrganizationModal.svelte +++ b/packages/shared/src/lib/organizations/OrganizationModal.svelte @@ -1,6 +1,7 @@ - - - -
Users:
- {#if organization?.inviteCode} -
-

Invite code:

- -
- {/if} - -
- {#each users as user, index} - - - - -

{user.user?.name}

-
- {/each} -
- -
Projects:
-
- {#each projects as { project, projectInterest }, index} - - - -

{project?.name}

-
- {/each} -
+ + + {#snippet children(organization)} + + +
Users:
+ {#if organization.inviteCode} +
+

Invite code:

+ +
+ {/if} + +
+ {#each users as { user, interest }, index} + + + + +

{user?.name}

+
+ {/each} +
+ +
Projects:
+
+ {#each projects as { project, interest }, index} + + + +

{project?.name}

+
+ {/each} +
+ {/snippet} +
diff --git a/packages/shared/src/lib/organizations/organizationService.ts b/packages/shared/src/lib/organizations/organizationService.ts index 877f8bde3d..3c93be7add 100644 --- a/packages/shared/src/lib/organizations/organizationService.ts +++ b/packages/shared/src/lib/organizations/organizationService.ts @@ -1,15 +1,21 @@ import { InterestStore, type Interest } from '$lib/interest/intrestStore'; -import { upsertOrganization, upsertOrganizations } from '$lib/organizations/organizationsSlice'; +import { type HttpClient } from '$lib/network/httpClient'; +import { ApiError } from '$lib/network/types'; +import { + addOrganization, + upsertOrganization, + upsertOrganizations +} from '$lib/organizations/organizationsSlice'; import { upsertProjects } from '$lib/organizations/projectsSlice'; import { apiToOrganization, apiToProject, type ApiOrganization, type ApiOrganizationWithDetails, + type LoadableOrganization, type Organization } from '$lib/organizations/types'; import { POLLING_REGULAR, POLLING_SLOW } from '$lib/polling'; -import type { HttpClient } from '$lib/httpClient'; import type { AppDispatch } from '$lib/redux/store.svelte'; export class OrganizationService { @@ -25,7 +31,14 @@ export class OrganizationService { return this.organizationListingInterests .findOrCreateSubscribable(undefined, async () => { const apiOrganizations = await this.httpClient.get('organization'); - const organizations = apiOrganizations.map(apiToOrganization); + const organizations = apiOrganizations.map( + (apiOrganizations) => + ({ + type: 'found', + id: apiOrganizations.slug, + value: apiToOrganization(apiOrganizations) + }) as LoadableOrganization + ); this.appDispatch.dispatch(upsertOrganizations(organizations)); }) @@ -35,14 +48,26 @@ export class OrganizationService { getOrganizationWithDetailsInterest(slug: string): Interest { return this.orgnaizationInterests .findOrCreateSubscribable({ slug }, async () => { - const apiOrganization = await this.httpClient.get( - `organization/${slug}` - ); - const organization = apiToOrganization(apiOrganization); - const projects = apiOrganization.projects.map(apiToProject); + this.appDispatch.dispatch(addOrganization({ type: 'loading', id: slug })); + + try { + const apiOrganization = await this.httpClient.get( + `organization/${slug}` + ); + const organization = apiToOrganization(apiOrganization); + const projects = apiOrganization.projects.map(apiToProject); - this.appDispatch.dispatch(upsertOrganization(organization)); - this.appDispatch.dispatch(upsertProjects(projects)); + this.appDispatch.dispatch( + upsertOrganization({ type: 'found', id: slug, value: organization }) + ); + this.appDispatch.dispatch(upsertProjects(projects)); + } catch (error: unknown) { + if (error instanceof ApiError && error.response.status === 404) { + this.appDispatch.dispatch(upsertOrganization({ type: 'not-found', id: slug })); + } else if (error instanceof Error) { + this.appDispatch.dispatch(upsertOrganization({ type: 'error', id: slug, error })); + } + } }) .createInterest(); } @@ -59,10 +84,10 @@ export class OrganizationService { description } }); - const orgnaization = apiToOrganization(apiOrganization); - this.appDispatch.dispatch(upsertOrganization(orgnaization)); + const organization = apiToOrganization(apiOrganization); + this.appDispatch.dispatch(upsertOrganization({ type: 'found', id: slug, value: organization })); - return orgnaization; + return organization; } async joinOrganization(slug: string, joinCode: string) { @@ -73,9 +98,9 @@ export class OrganizationService { } ); - const orgnaization = apiToOrganization(apiOrganization); - this.appDispatch.dispatch(upsertOrganization(orgnaization)); + const organization = apiToOrganization(apiOrganization); + this.appDispatch.dispatch(upsertOrganization({ type: 'found', id: slug, value: organization })); - return orgnaization; + return organization; } } diff --git a/packages/shared/src/lib/organizations/organizationsSlice.ts b/packages/shared/src/lib/organizations/organizationsSlice.ts index 87883f2235..3b09212363 100644 --- a/packages/shared/src/lib/organizations/organizationsSlice.ts +++ b/packages/shared/src/lib/organizations/organizationsSlice.ts @@ -1,9 +1,8 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import type { Organization } from '$lib/organizations/types'; +import type { LoadableOrganization } from '$lib/organizations/types'; -const organizationsAdapter = createEntityAdapter({ - selectId: (organization: Organization) => organization.slug, - sortComparer: (a: Organization, b: Organization) => a.slug.localeCompare(b.slug) +const organizationsAdapter = createEntityAdapter({ + selectId: (organization: LoadableOrganization) => organization.id }); const organizationsSlice = createSlice({ diff --git a/packages/shared/src/lib/organizations/projectService.ts b/packages/shared/src/lib/organizations/projectService.ts index d6913ee296..109c031501 100644 --- a/packages/shared/src/lib/organizations/projectService.ts +++ b/packages/shared/src/lib/organizations/projectService.ts @@ -2,7 +2,7 @@ import { InterestStore, type Interest } from '$lib/interest/intrestStore'; import { upsertProject } from '$lib/organizations/projectsSlice'; import { type ApiProject, apiToProject } from '$lib/organizations/types'; import { POLLING_REGULAR } from '$lib/polling'; -import type { HttpClient } from '$lib/httpClient'; +import type { HttpClient } from '$lib/network/httpClient'; import type { AppDispatch } from '$lib/redux/store.svelte'; export class ProjectService { diff --git a/packages/shared/src/lib/organizations/types.ts b/packages/shared/src/lib/organizations/types.ts index a69dd679d4..1470760e56 100644 --- a/packages/shared/src/lib/organizations/types.ts +++ b/packages/shared/src/lib/organizations/types.ts @@ -1,3 +1,5 @@ +import type { LoadableData } from '$lib/network/loadable'; + export type ApiProject = { slug: string; owner: string; @@ -78,6 +80,8 @@ export type Organization = { projectRepositoryIds?: string[]; }; +export type LoadableOrganization = LoadableData; + export function apiToOrganization( apiOrganization: ApiOrganization | ApiOrganizationWithDetails ): Organization { diff --git a/packages/shared/src/lib/users/userService.ts b/packages/shared/src/lib/users/userService.ts index 7b4ebcdcad..32f61d436f 100644 --- a/packages/shared/src/lib/users/userService.ts +++ b/packages/shared/src/lib/users/userService.ts @@ -2,7 +2,7 @@ import { InterestStore, type Interest } from '$lib/interest/intrestStore'; import { POLLING_SLOW } from '$lib/polling'; import { apiToUser, type ApiUser } from '$lib/users/types'; import { upsertUser } from '$lib/users/usersSlice'; -import type { HttpClient } from '$lib/httpClient'; +import type { HttpClient } from '$lib/network/httpClient'; import type { AppDispatch } from '$lib/redux/store.svelte'; export class UserService { From 630ef418ea2a8d1e9f902fb8ed39f311f6d54a8d Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Fri, 13 Dec 2024 17:30:26 +0100 Subject: [PATCH 2/5] Convert project over to loadable data --- .../lib/backend/projectCloudSync.svelte.ts | 7 +- apps/desktop/src/lib/feeds/CreatePost.svelte | 8 +-- .../CloudProjectSettings.svelte | 69 +++++++++++-------- .../ProjectConnectModal.svelte | 67 ++++++++++-------- .../src/routes/[projectId]/feed/+page.svelte | 17 +++-- .../web/src/routes/organizations/+page.svelte | 41 ++++++----- packages/shared/src/lib/network/loadable.ts | 66 ++++++++++++++++-- .../organizations/OrganizationModal.svelte | 6 +- .../lib/organizations/organizationService.ts | 38 +++++----- .../lib/organizations/organizationsSlice.ts | 5 +- .../src/lib/organizations/projectService.ts | 26 +++++-- .../organizations/projectsPreview.svelte.ts | 39 ++++++++--- .../src/lib/organizations/projectsSlice.ts | 12 ++-- .../shared/src/lib/organizations/types.ts | 4 +- 14 files changed, 267 insertions(+), 138 deletions(-) diff --git a/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts b/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts index 4513aecc92..7d5663571b 100644 --- a/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts +++ b/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts @@ -25,14 +25,17 @@ export function projectCloudSync( registerInterest(cloudProjectInterest); }); - const cloudProject = $derived( + const loadableCloudProject = $derived( project.current?.api ? projectsSelectors.selectById(appState.projects, project.current.api.repository_id) : undefined ); $effect(() => { - if (!project.current?.api || !cloudProject) return; + if (!project.current?.api || !loadableCloudProject || loadableCloudProject.type !== 'found') + return; + + const cloudProject = loadableCloudProject.value; const persistedProjectUpdatedAt = new Date(project.current.api.updated_at).getTime(); const cloudProjectUpdatedAt = new Date(cloudProject.updatedAt).getTime(); if (persistedProjectUpdatedAt >= cloudProjectUpdatedAt) return; diff --git a/apps/desktop/src/lib/feeds/CreatePost.svelte b/apps/desktop/src/lib/feeds/CreatePost.svelte index dd16578fde..d9abb15e92 100644 --- a/apps/desktop/src/lib/feeds/CreatePost.svelte +++ b/apps/desktop/src/lib/feeds/CreatePost.svelte @@ -73,13 +73,13 @@ // Post creation let newPostContent = $derived(persisted('', `postContent--${feedIdentity}--${replyTo}`)); function createPost() { - if (!feedIdentity?.current) return; - if (!parentProject?.current) return; + if (feedIdentity?.current.type !== 'found') return; + if (parentProject?.current?.type !== 'found') return; feedService.createPost( $newPostContent, - parentProject.current.repositoryId, - feedIdentity.current, + parentProject.current.value.repositoryId, + feedIdentity.current.value, replyTo, picture ); diff --git a/apps/desktop/src/lib/settings/userPreferences/CloudProjectSettings.svelte b/apps/desktop/src/lib/settings/userPreferences/CloudProjectSettings.svelte index c36d0f684c..c7094b547a 100644 --- a/apps/desktop/src/lib/settings/userPreferences/CloudProjectSettings.svelte +++ b/apps/desktop/src/lib/settings/userPreferences/CloudProjectSettings.svelte @@ -7,6 +7,7 @@ import * as toasts from '$lib/utils/toasts'; import { getContext, getContextStore } from '@gitbutler/shared/context'; import RegisterInterest from '@gitbutler/shared/interest/RegisterInterest.svelte'; + import Loading from '@gitbutler/shared/network/Loading.svelte'; import { OrganizationService } from '@gitbutler/shared/organizations/organizationService'; import { organizationsSelectors } from '@gitbutler/shared/organizations/organizationsSlice'; import { ProjectService as CloudProjectService } from '@gitbutler/shared/organizations/projectService'; @@ -138,36 +139,44 @@ {/snippet} - {#if !cloudProject?.parentProjectRepositoryId} -
- {#snippet title()} - Link your project with an organization - {/snippet} - - - -
- {#each usersOrganizations as organization, index} - - {#snippet children()} -
{organization.name || organization.slug}
- {/snippet} - {#snippet actions()} - - {/snippet} -
- {/each} -
-
- {/if} + + {#snippet children(cloudProject)} +
+ {#snippet title()} + Link your project with an organization + {/snippet} + + + +
+ {#each usersOrganizations as loadableOrganization, index} + + {#snippet children()} + + {#snippet children(organization)} +
+ {organization.name || organization.slug} +
+ {/snippet} +
+ {/snippet} + {#snippet actions()} + + {/snippet} +
+ {/each} +
+
+ {/snippet} +
{:else if !$project?.api?.repository_id}
diff --git a/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte b/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte index ad3b64c091..c0ccf2ee21 100644 --- a/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte +++ b/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte @@ -1,6 +1,7 @@ - + - {#if organization} - {#each organizationProjects as { project, projectInterest }, index} - - - - {#if project} -
{project.name}
- - - {:else} -

Loading...

- {/if} -
- {/each} - {/if} + {#each organizationProjects as { project: organizationProject, interest }, index} + + + + + {#snippet children(organizationProject)} +
{organizationProject.name}
+ + + {/snippet} +
+
+ {/each}
diff --git a/apps/desktop/src/routes/[projectId]/feed/+page.svelte b/apps/desktop/src/routes/[projectId]/feed/+page.svelte index 9a35090ef7..bc3e7f1b62 100644 --- a/apps/desktop/src/routes/[projectId]/feed/+page.svelte +++ b/apps/desktop/src/routes/[projectId]/feed/+page.svelte @@ -22,18 +22,25 @@ : undefined ); - const feed = $derived(getFeed(appState, feedService, feedIdentity?.current)); + const feed = $derived.by(() => { + if (feedIdentity?.current.type !== 'found') return; + return getFeed(appState, feedService, feedIdentity?.current.value); + }); // Infinite scrolling - const lastPost = $derived(getFeedLastPost(appState, feedService, feed.current)); + const lastPost = $derived(getFeedLastPost(appState, feedService, feed?.current)); let lastElement = $state(); $effect(() => { if (!lastElement) return; const observer = new IntersectionObserver((entries) => { - if (entries[0]?.isIntersecting && lastPost.current?.createdAt && feedIdentity?.current) { - feedService.getFeedPage(feedIdentity.current, lastPost.current.createdAt); + if ( + entries[0]?.isIntersecting && + lastPost.current?.createdAt && + feedIdentity?.current.type === 'found' + ) { + feedService.getFeedPage(feedIdentity.current.value, lastPost.current.createdAt); } }); @@ -51,7 +58,7 @@
- {#if feed.current} + {#if feed?.current} {#each feed.current.postIds as postId, index (postId)}
{#if index < feed.current.postIds.length - 1 && lastPost.current && feedIdentity?.current} diff --git a/apps/web/src/routes/organizations/+page.svelte b/apps/web/src/routes/organizations/+page.svelte index 3853805c61..8fa7e4ce0f 100644 --- a/apps/web/src/routes/organizations/+page.svelte +++ b/apps/web/src/routes/organizations/+page.svelte @@ -1,7 +1,8 @@ -{#if loadable?.type === 'found'} +{#if !loadable} +

Uninitialized...

+{:else if loadable.type === 'found'} {@render children(loadable.value)} -{:else if loadable?.type === 'loading'} +{:else if loadable.type === 'loading'}

Loading...

-{:else if loadable?.type === 'not-found'} +{:else if loadable.type === 'not-found'}

Not found

-{:else if loadable?.type === 'error'} +{:else if loadable.type === 'error'}

{loadable.error.message}

{:else} -

Organization uninitialized

+

Unknown state

{/if} diff --git a/packages/shared/src/lib/network/loadable.ts b/packages/shared/src/lib/network/loadable.ts index 682f6c2735..51e1fa1fbd 100644 --- a/packages/shared/src/lib/network/loadable.ts +++ b/packages/shared/src/lib/network/loadable.ts @@ -1,12 +1,12 @@ -import { ApiError } from '$lib/network/types'; +import { ApiError, type Loadable, type LoadableData } from '$lib/network/types'; import type { EntityId, EntityAdapter, EntityState } from '@reduxjs/toolkit'; -export type Loadable = - | { type: 'loading' | 'not-found' } - | { type: 'found'; value: T } - | { type: 'error'; error: Error }; - -export type LoadableData = Loadable & { id: Id }; +export function isFound(loadable?: Loadable): loadable is { + type: 'found'; + value: T; +} { + return loadable?.type === 'found'; +} export function errorToLoadable(error: unknown, id: Id): LoadableData { if (error instanceof Error) { diff --git a/packages/shared/src/lib/network/types.ts b/packages/shared/src/lib/network/types.ts index d853489eb9..ed49c8801f 100644 --- a/packages/shared/src/lib/network/types.ts +++ b/packages/shared/src/lib/network/types.ts @@ -6,3 +6,10 @@ export class ApiError extends Error { super(message); } } + +export type Loadable = + | { type: 'loading' | 'not-found' } + | { type: 'found'; value: T } + | { type: 'error'; error: Error }; + +export type LoadableData = Loadable & { id: Id }; diff --git a/packages/shared/src/lib/organizations/OrganizationModal.svelte b/packages/shared/src/lib/organizations/OrganizationModal.svelte index 4d3c285a7f..3a167f7538 100644 --- a/packages/shared/src/lib/organizations/OrganizationModal.svelte +++ b/packages/shared/src/lib/organizations/OrganizationModal.svelte @@ -1,13 +1,13 @@ - + {#snippet children(organization)} - - -
Users:
{#if organization.inviteCode}

Invite code:

@@ -81,43 +48,49 @@
{/if} -
- {#each users as { user, interest }, index} - - - - -

{user?.name}

-
- {/each} -
- -
Projects:
-
- {#each projects as { project, interest }, index} - - - - - {#snippet children(project)} -

{project.name}

- {/snippet} -
-
- {/each} -
+ {#if organization.memberLogins} +
Users:
+ +
+ {#each organization.memberLogins as login, index} + {@const user = getUserByLogin(appState, userService, login)} + + + +

{user.current?.name}

+
+ {/each} +
+ {/if} + + {#if organization.projectRepositoryIds} +
Projects:
+
+ {#each organization.projectRepositoryIds as repositoryId, index} + {@const project = getProjectByRepositoryId(appState, projectService, repositoryId)} + + + + {#snippet children(project)} +

{project.name}

+ {/snippet} +
+
+ {/each} +
+ {/if} {/snippet}
diff --git a/packages/shared/src/lib/organizations/organizationsPreview.svelte.ts b/packages/shared/src/lib/organizations/organizationsPreview.svelte.ts new file mode 100644 index 0000000000..6f16cb7627 --- /dev/null +++ b/packages/shared/src/lib/organizations/organizationsPreview.svelte.ts @@ -0,0 +1,21 @@ +import { registerInterest } from '$lib/interest/registerInterestFunction.svelte'; +import { organizationsSelectors } from '$lib/organizations/organizationsSlice'; +import type { OrganizationService } from '$lib/organizations/organizationService'; +import type { LoadableOrganization } from '$lib/organizations/types'; +import type { AppOrganizationsState } from '$lib/redux/store.svelte'; +import type { Reactive } from '$lib/storeUtils'; + +export function getOrganizationBySlug( + appState: AppOrganizationsState, + organizationService: OrganizationService, + slug: string +): Reactive { + registerInterest(organizationService.getOrganizationWithDetailsInterest(slug)); + const current = $derived(organizationsSelectors.selectById(appState.organizations, slug)); + + return { + get current() { + return current; + } + }; +} diff --git a/packages/shared/src/lib/organizations/projectsPreview.svelte.ts b/packages/shared/src/lib/organizations/projectsPreview.svelte.ts index 25e5731086..3cbed33e66 100644 --- a/packages/shared/src/lib/organizations/projectsPreview.svelte.ts +++ b/packages/shared/src/lib/organizations/projectsPreview.svelte.ts @@ -1,41 +1,47 @@ import { registerInterest } from '$lib/interest/registerInterestFunction.svelte'; +import { isFound } from '$lib/network/loadable'; import { projectsSelectors } from '$lib/organizations/projectsSlice'; -import type { Loadable } from '$lib/network/loadable'; +import type { Loadable } from '$lib/network/types'; import type { ProjectService } from '$lib/organizations/projectService'; import type { LoadableProject } from '$lib/organizations/types'; import type { AppOrganizationsState, AppProjectsState } from '$lib/redux/store.svelte'; import type { Reactive } from '$lib/storeUtils'; -export function getParentForRepositoryId( - appState: AppProjectsState & AppOrganizationsState, +export function getProjectByRepositoryId( + appState: AppProjectsState, projectService: ProjectService, projectRepositoryId: string ): Reactive { registerInterest(projectService.getProjectInterest(projectRepositoryId)); + const current = $derived(projectsSelectors.selectById(appState.projects, projectRepositoryId)); - const current = $derived.by(() => { - const loadableProject = projectsSelectors.selectById(appState.projects, projectRepositoryId); + return { + get current() { + return current; + } + }; +} - if ( - !loadableProject || - loadableProject.type !== 'found' || - !loadableProject.value.parentProjectRepositoryId - ) - return; +export function getParentForRepositoryId( + appState: AppProjectsState & AppOrganizationsState, + projectService: ProjectService, + projectRepositoryId: string +): Reactive { + const current = $derived.by(() => { + const project = getProjectByRepositoryId(appState, projectService, projectRepositoryId); - registerInterest( - projectService.getProjectInterest(loadableProject.value.parentProjectRepositoryId) - ); + if (!isFound(project.current) || !project.current.value.parentProjectRepositoryId) return; - return projectsSelectors.selectById( - appState.projects, - loadableProject.value.parentProjectRepositoryId + return getProjectByRepositoryId( + appState, + projectService, + project.current.value.parentProjectRepositoryId ); }); return { get current() { - return current; + return current?.current; } }; } @@ -50,8 +56,7 @@ export function getFeedIdentityForRepositoryId( ); const current = $derived.by>(() => { - if (!parentProject.current) return { type: 'loading' }; - if (parentProject.current.type !== 'found') return parentProject.current; + if (!isFound(parentProject.current)) return parentProject.current || { type: 'loading' }; return { type: 'found', diff --git a/packages/shared/src/lib/organizations/types.ts b/packages/shared/src/lib/organizations/types.ts index a0eea2ccab..acf8342b5e 100644 --- a/packages/shared/src/lib/organizations/types.ts +++ b/packages/shared/src/lib/organizations/types.ts @@ -1,4 +1,4 @@ -import type { LoadableData } from '$lib/network/loadable'; +import type { LoadableData } from '$lib/network/types'; export type ApiProject = { slug: string; diff --git a/packages/shared/src/lib/persisted.ts b/packages/shared/src/lib/persisted.ts index ca3afa096e..42b816c135 100644 --- a/packages/shared/src/lib/persisted.ts +++ b/packages/shared/src/lib/persisted.ts @@ -20,6 +20,7 @@ export function setStorageItem(key: string, value: unknown): void { export function persisted(initial: T, key: string): Persisted { function setAndPersist(value: T, set: (value: T) => void) { + console.log(key, value); setStorageItem(key, value); set(value); } diff --git a/packages/shared/src/lib/reactiveUtils.svelte.ts b/packages/shared/src/lib/reactiveUtils.svelte.ts index 773855430a..6a0529099c 100644 --- a/packages/shared/src/lib/reactiveUtils.svelte.ts +++ b/packages/shared/src/lib/reactiveUtils.svelte.ts @@ -1,5 +1,5 @@ -import type { Reactive } from '$lib/storeUtils'; -import type { Readable } from 'svelte/store'; +import type { Reactive, WritableReactive } from '$lib/storeUtils'; +import type { Readable, Writable } from 'svelte/store'; export function readableToReactive(readable: Readable): Reactive { let current = $state(); @@ -18,3 +18,27 @@ export function readableToReactive(readable: Readable): Reactive(writable: Writable): WritableReactive { + let current = $state(); + + $effect(() => { + const unsubscribe = writable.subscribe((value) => { + current = value; + }); + + return unsubscribe; + }); + + return { + get current() { + return current; + }, + + set current(value: T | undefined) { + if (value !== undefined) { + writable.set(value); + } + } + }; +} diff --git a/packages/shared/src/lib/storeUtils.ts b/packages/shared/src/lib/storeUtils.ts index 073e7e7bbf..9500554dd8 100644 --- a/packages/shared/src/lib/storeUtils.ts +++ b/packages/shared/src/lib/storeUtils.ts @@ -56,7 +56,8 @@ export function writableDerived( return derivedStore; } -export type Reactive = { current: T }; +export type Reactive = { readonly current: T }; +export type WritableReactive = { current: T }; export async function guardReadableTrue(target: Readable): Promise { return await new Promise((resolve) => { From ed264586bee5914153214b709b8348d0378e2735 Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Thu, 9 Jan 2025 12:47:57 +0100 Subject: [PATCH 4/5] Loadable users --- apps/desktop/src/lib/feeds/Post.svelte | 17 ++++++++----- .../[projectId]/feed/[postId]/+page.svelte | 17 ++++++++----- .../src/lib/feeds/feedsPreview.svelte.ts | 4 ++-- packages/shared/src/lib/feeds/service.ts | 24 ++++++++++++------- .../organizations/OrganizationModal.svelte | 16 ++++++++----- packages/shared/src/lib/users/types.ts | 4 ++++ packages/shared/src/lib/users/userService.ts | 15 ++++++++---- .../src/lib/users/usersPreview.svelte.ts | 21 ++++++++++++++++ packages/shared/src/lib/users/usersSlice.ts | 12 +++++----- 9 files changed, 92 insertions(+), 38 deletions(-) create mode 100644 packages/shared/src/lib/users/usersPreview.svelte.ts diff --git a/apps/desktop/src/lib/feeds/Post.svelte b/apps/desktop/src/lib/feeds/Post.svelte index 6e06c6205c..52b10cf6ae 100644 --- a/apps/desktop/src/lib/feeds/Post.svelte +++ b/apps/desktop/src/lib/feeds/Post.svelte @@ -6,6 +6,7 @@ import { postsSelectors } from '@gitbutler/shared/feeds/postsSlice'; import { FeedService } from '@gitbutler/shared/feeds/service'; import { registerInterestInView } from '@gitbutler/shared/interest/registerInterestFunction.svelte'; + import Loading from '@gitbutler/shared/network/Loading.svelte'; import { AppState } from '@gitbutler/shared/redux/store.svelte'; import { UserService } from '@gitbutler/shared/users/userService'; import Button from '@gitbutler/ui/Button.svelte'; @@ -42,12 +43,16 @@
- -

{author.current?.name}

+ + {#snippet children(author)} + +

{author.name}

+ {/snippet} +
diff --git a/apps/desktop/src/routes/[projectId]/feed/[postId]/+page.svelte b/apps/desktop/src/routes/[projectId]/feed/[postId]/+page.svelte index 38d1af2d26..2a311e0009 100644 --- a/apps/desktop/src/routes/[projectId]/feed/[postId]/+page.svelte +++ b/apps/desktop/src/routes/[projectId]/feed/[postId]/+page.svelte @@ -8,6 +8,7 @@ import { postsSelectors } from '@gitbutler/shared/feeds/postsSlice'; import { FeedService } from '@gitbutler/shared/feeds/service'; import { registerInterest } from '@gitbutler/shared/interest/registerInterestFunction.svelte'; + import Loading from '@gitbutler/shared/network/Loading.svelte'; import { AppState } from '@gitbutler/shared/redux/store.svelte'; import { UserService } from '@gitbutler/shared/users/userService'; import Button from '@gitbutler/ui/Button.svelte'; @@ -48,12 +49,16 @@
- -

{author?.current?.name}

+ + {#snippet children(author)} + +

{author.name}

+ {/snippet} +
diff --git a/packages/shared/src/lib/feeds/feedsPreview.svelte.ts b/packages/shared/src/lib/feeds/feedsPreview.svelte.ts index 93629a9fb9..312e0cbc17 100644 --- a/packages/shared/src/lib/feeds/feedsPreview.svelte.ts +++ b/packages/shared/src/lib/feeds/feedsPreview.svelte.ts @@ -9,7 +9,7 @@ import type { FeedService } from '$lib/feeds/service'; import type { Feed, Post } from '$lib/feeds/types'; import type { AppFeedsState, AppPostsState, AppUsersState } from '$lib/redux/store.svelte'; import type { Reactive } from '$lib/storeUtils'; -import type { User } from '$lib/users/types'; +import type { LoadableUser } from '$lib/users/types'; import type { UserService } from '$lib/users/userService'; export function getFeed( @@ -66,7 +66,7 @@ export function getPostAuthor( renderInView?: { element?: HTMLElement; } -): Reactive { +): Reactive { const current = $derived.by(() => { const postInterest = feedService.getPostWithRepliesInterest(postId); if (renderInView) { diff --git a/packages/shared/src/lib/feeds/service.ts b/packages/shared/src/lib/feeds/service.ts index 7e8fbd8c9c..5b3f039390 100644 --- a/packages/shared/src/lib/feeds/service.ts +++ b/packages/shared/src/lib/feeds/service.ts @@ -4,7 +4,7 @@ import { apiToPost, type ApiPost, type ApiPostWithReplies, type Post } from '$li import { InterestStore } from '$lib/interest/intrestStore'; import { POLLING_FAST, POLLING_REGULAR } from '$lib/polling'; import { guardReadableTrue } from '$lib/storeUtils'; -import { apiToUser } from '$lib/users/types'; +import { apiToUser, type LoadableUser } from '$lib/users/types'; import { upsertUsers } from '$lib/users/usersSlice'; import type { HttpClient } from '$lib/network/httpClient'; import type { AppDispatch } from '$lib/redux/store.svelte'; @@ -42,7 +42,14 @@ export class FeedService { const query = lastPostTimestamp ? `?from_created_at=${lastPostTimestamp}` : ''; const apiFeed = await this.httpClient.get(`feed/project/${identifier}${query}`); this.appDispatch.dispatch(upsertPosts(apiFeed.map(apiToPost))); - this.appDispatch.dispatch(upsertUsers(apiFeed.map((apiPost) => apiToUser(apiPost.user)))); + const users = apiFeed.map( + (apiPost): LoadableUser => ({ + type: 'found', + value: apiToUser(apiPost.user), + id: apiPost.user.login + }) + ); + this.appDispatch.dispatch(upsertUsers(users)); const actionArguments = { identifier, postIds: apiFeed.map((post) => post.uuid) }; if (lastPostTimestamp) { @@ -103,12 +110,13 @@ export class FeedService { const posts = [post, ...apiPostWithReplies.replies.map(apiToPost)]; this.appDispatch.dispatch(upsertPosts(posts)); - this.appDispatch.dispatch( - upsertUsers( - [apiPostWithReplies, ...apiPostWithReplies.replies].map((apiPost) => - apiToUser(apiPost.user) - ) - ) + const users = [apiPostWithReplies, ...apiPostWithReplies.replies].map( + (apiPost): LoadableUser => ({ + type: 'found', + id: apiPost.user.login, + value: apiToUser(apiPost.user) + }) ); + this.appDispatch.dispatch(upsertUsers(users)); } } diff --git a/packages/shared/src/lib/organizations/OrganizationModal.svelte b/packages/shared/src/lib/organizations/OrganizationModal.svelte index 3a167f7538..c6b758f507 100644 --- a/packages/shared/src/lib/organizations/OrganizationModal.svelte +++ b/packages/shared/src/lib/organizations/OrganizationModal.svelte @@ -60,12 +60,16 @@ roundedTop={index === 0} orientation="row" > - -

{user.current?.name}

+ + {#snippet children(user)} + +

{user?.name}

+ {/snippet} +
{/each}
diff --git a/packages/shared/src/lib/users/types.ts b/packages/shared/src/lib/users/types.ts index 01e3cec2a6..670c819033 100644 --- a/packages/shared/src/lib/users/types.ts +++ b/packages/shared/src/lib/users/types.ts @@ -1,3 +1,5 @@ +import type { LoadableData } from '$lib/network/types'; + export type ApiUser = { id: number; login: string; @@ -13,6 +15,8 @@ export type User = { avatarUrl?: string; }; +export type LoadableUser = LoadableData; + export function apiToUser(apiUser: ApiUser): User { return { login: apiUser.login, diff --git a/packages/shared/src/lib/users/userService.ts b/packages/shared/src/lib/users/userService.ts index 32f61d436f..2215c3a9f8 100644 --- a/packages/shared/src/lib/users/userService.ts +++ b/packages/shared/src/lib/users/userService.ts @@ -1,7 +1,8 @@ import { InterestStore, type Interest } from '$lib/interest/intrestStore'; +import { errorToLoadable } from '$lib/network/loadable'; import { POLLING_SLOW } from '$lib/polling'; import { apiToUser, type ApiUser } from '$lib/users/types'; -import { upsertUser } from '$lib/users/usersSlice'; +import { addUser, upsertUser } from '$lib/users/usersSlice'; import type { HttpClient } from '$lib/network/httpClient'; import type { AppDispatch } from '$lib/redux/store.svelte'; @@ -16,9 +17,15 @@ export class UserService { getUserInterest(login: string): Interest { return this.userInterests .findOrCreateSubscribable({ login }, async () => { - const apiUser = await this.httpClient.get(`user/${login}`); - const user = apiToUser(apiUser); - this.appDispatch.dispatch(upsertUser(user)); + this.appDispatch.dispatch(addUser({ type: 'loading', id: login })); + + try { + const apiUser = await this.httpClient.get(`user/${login}`); + const user = apiToUser(apiUser); + this.appDispatch.dispatch(upsertUser({ type: 'found', id: login, value: user })); + } catch (error: unknown) { + this.appDispatch.dispatch(upsertUser(errorToLoadable(error, login))); + } }) .createInterest(); } diff --git a/packages/shared/src/lib/users/usersPreview.svelte.ts b/packages/shared/src/lib/users/usersPreview.svelte.ts new file mode 100644 index 0000000000..5af6ffce4a --- /dev/null +++ b/packages/shared/src/lib/users/usersPreview.svelte.ts @@ -0,0 +1,21 @@ +import { registerInterest } from '$lib/interest/registerInterestFunction.svelte'; +import { UserService } from '$lib/users/userService'; +import { usersSelectors } from '$lib/users/usersSlice'; +import type { AppUsersState } from '$lib/redux/store.svelte'; +import type { Reactive } from '$lib/storeUtils'; +import type { LoadableUser } from '$lib/users/types'; + +export function getUserByLogin( + appState: AppUsersState, + userService: UserService, + login: string +): Reactive { + registerInterest(userService.getUserInterest(login)); + const current = $derived(usersSelectors.selectById(appState.users, login)); + + return { + get current() { + return current; + } + }; +} diff --git a/packages/shared/src/lib/users/usersSlice.ts b/packages/shared/src/lib/users/usersSlice.ts index 641aa450cd..7c7cc91770 100644 --- a/packages/shared/src/lib/users/usersSlice.ts +++ b/packages/shared/src/lib/users/usersSlice.ts @@ -1,9 +1,9 @@ +import { loadableUpsert, loadableUpsertMany } from '$lib/network/loadable'; import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import type { User } from '$lib/users/types'; +import type { LoadableUser } from '$lib/users/types'; -const usersAdapter = createEntityAdapter({ - selectId: (user: User) => user.login, - sortComparer: (a: User, b: User) => a.login.localeCompare(b.login) +const usersAdapter = createEntityAdapter({ + selectId: (user: LoadableUser) => user.id }); const usersSlice = createSlice({ @@ -14,8 +14,8 @@ const usersSlice = createSlice({ addUsers: usersAdapter.addMany, removeUser: usersAdapter.removeOne, removeUsers: usersAdapter.removeMany, - upsertUser: usersAdapter.upsertOne, - upsertUsers: usersAdapter.upsertMany + upsertUser: loadableUpsert(usersAdapter), + upsertUsers: loadableUpsertMany(usersAdapter) } }); From 71717dab7f127d0d404b2a44fe69507286b6f3f6 Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Thu, 9 Jan 2025 13:04:06 +0100 Subject: [PATCH 5/5] rename type to status --- .../src/lib/backend/projectCloudSync.svelte.ts | 2 +- apps/desktop/src/lib/feeds/CreatePost.svelte | 4 ++-- .../userPreferences/ProjectConnectModal.svelte | 6 +++--- .../src/routes/[projectId]/feed/+page.svelte | 4 ++-- packages/shared/src/lib/feeds/service.ts | 4 ++-- packages/shared/src/lib/network/Loading.svelte | 8 ++++---- packages/shared/src/lib/network/loadable.ts | 14 +++++++------- packages/shared/src/lib/network/types.ts | 6 +++--- .../src/lib/organizations/organizationService.ts | 16 ++++++++++------ .../src/lib/organizations/projectService.ts | 8 ++++---- .../lib/organizations/projectsPreview.svelte.ts | 4 ++-- packages/shared/src/lib/persisted.ts | 1 - packages/shared/src/lib/users/userService.ts | 4 ++-- 13 files changed, 42 insertions(+), 39 deletions(-) diff --git a/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts b/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts index 7d5663571b..044a61df2b 100644 --- a/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts +++ b/apps/desktop/src/lib/backend/projectCloudSync.svelte.ts @@ -32,7 +32,7 @@ export function projectCloudSync( ); $effect(() => { - if (!project.current?.api || !loadableCloudProject || loadableCloudProject.type !== 'found') + if (!project.current?.api || !loadableCloudProject || loadableCloudProject.status !== 'found') return; const cloudProject = loadableCloudProject.value; diff --git a/apps/desktop/src/lib/feeds/CreatePost.svelte b/apps/desktop/src/lib/feeds/CreatePost.svelte index d9abb15e92..6055a17994 100644 --- a/apps/desktop/src/lib/feeds/CreatePost.svelte +++ b/apps/desktop/src/lib/feeds/CreatePost.svelte @@ -73,8 +73,8 @@ // Post creation let newPostContent = $derived(persisted('', `postContent--${feedIdentity}--${replyTo}`)); function createPost() { - if (feedIdentity?.current.type !== 'found') return; - if (parentProject?.current?.type !== 'found') return; + if (feedIdentity?.current.status !== 'found') return; + if (parentProject?.current?.status !== 'found') return; feedService.createPost( $newPostContent, diff --git a/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte b/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte index c0ccf2ee21..7b47275ede 100644 --- a/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte +++ b/apps/desktop/src/lib/settings/userPreferences/ProjectConnectModal.svelte @@ -35,7 +35,7 @@ ); const organizationProjects = $derived.by(() => { - if (chosenOrganization?.type !== 'found') return []; + if (chosenOrganization?.status !== 'found') return []; return ( chosenOrganization.value.projectRepositoryIds?.map((repositoryId) => ({ project: projectsSelectors.selectById(appState.projects, repositoryId), @@ -45,7 +45,7 @@ }); function connectToOrganization(projectSlug?: string) { - if (targetProject?.type !== 'found' || chosenOrganization?.type !== 'found') return; + if (targetProject?.status !== 'found' || chosenOrganization?.status !== 'found') return; projectsService.connectProjectToOrganization( targetProject.value.repositoryId, @@ -55,7 +55,7 @@ } const title = $derived.by(() => { - if (targetProject?.type !== 'found' || chosenOrganization?.type !== 'found') return; + if (targetProject?.status !== 'found' || chosenOrganization?.status !== 'found') return; return `Join ${targetProject.value.name} into ${chosenOrganization.value.name}`; }); diff --git a/apps/desktop/src/routes/[projectId]/feed/+page.svelte b/apps/desktop/src/routes/[projectId]/feed/+page.svelte index bc3e7f1b62..748c2bd6e0 100644 --- a/apps/desktop/src/routes/[projectId]/feed/+page.svelte +++ b/apps/desktop/src/routes/[projectId]/feed/+page.svelte @@ -23,7 +23,7 @@ ); const feed = $derived.by(() => { - if (feedIdentity?.current.type !== 'found') return; + if (feedIdentity?.current.status !== 'found') return; return getFeed(appState, feedService, feedIdentity?.current.value); }); @@ -38,7 +38,7 @@ if ( entries[0]?.isIntersecting && lastPost.current?.createdAt && - feedIdentity?.current.type === 'found' + feedIdentity?.current.status === 'found' ) { feedService.getFeedPage(feedIdentity.current.value, lastPost.current.createdAt); } diff --git a/packages/shared/src/lib/feeds/service.ts b/packages/shared/src/lib/feeds/service.ts index 5b3f039390..70efb2c84d 100644 --- a/packages/shared/src/lib/feeds/service.ts +++ b/packages/shared/src/lib/feeds/service.ts @@ -44,7 +44,7 @@ export class FeedService { this.appDispatch.dispatch(upsertPosts(apiFeed.map(apiToPost))); const users = apiFeed.map( (apiPost): LoadableUser => ({ - type: 'found', + status: 'found', value: apiToUser(apiPost.user), id: apiPost.user.login }) @@ -112,7 +112,7 @@ export class FeedService { this.appDispatch.dispatch(upsertPosts(posts)); const users = [apiPostWithReplies, ...apiPostWithReplies.replies].map( (apiPost): LoadableUser => ({ - type: 'found', + status: 'found', id: apiPost.user.login, value: apiToUser(apiPost.user) }) diff --git a/packages/shared/src/lib/network/Loading.svelte b/packages/shared/src/lib/network/Loading.svelte index 109384182e..b4f641c3ca 100644 --- a/packages/shared/src/lib/network/Loading.svelte +++ b/packages/shared/src/lib/network/Loading.svelte @@ -13,13 +13,13 @@ {#if !loadable}

Uninitialized...

-{:else if loadable.type === 'found'} +{:else if loadable.status === 'found'} {@render children(loadable.value)} -{:else if loadable.type === 'loading'} +{:else if loadable.status === 'loading'}

Loading...

-{:else if loadable.type === 'not-found'} +{:else if loadable.status === 'not-found'}

Not found

-{:else if loadable.type === 'error'} +{:else if loadable.status === 'error'}

{loadable.error.message}

{:else}

Unknown state

diff --git a/packages/shared/src/lib/network/loadable.ts b/packages/shared/src/lib/network/loadable.ts index 51e1fa1fbd..6d6ba4c9ef 100644 --- a/packages/shared/src/lib/network/loadable.ts +++ b/packages/shared/src/lib/network/loadable.ts @@ -2,22 +2,22 @@ import { ApiError, type Loadable, type LoadableData } from '$lib/network/types'; import type { EntityId, EntityAdapter, EntityState } from '@reduxjs/toolkit'; export function isFound(loadable?: Loadable): loadable is { - type: 'found'; + status: 'found'; value: T; } { - return loadable?.type === 'found'; + return loadable?.status === 'found'; } export function errorToLoadable(error: unknown, id: Id): LoadableData { if (error instanceof Error) { if (error instanceof ApiError && error.response.status === 404) { - return { type: 'not-found', id }; + return { status: 'not-found', id }; } - return { type: 'error', id, error }; + return { status: 'error', id, error }; } - return { type: 'error', id, error: new Error(String(error)) }; + return { status: 'error', id, error: new Error(String(error)) }; } export function loadableUpsert( @@ -44,12 +44,12 @@ export function loadableUpsertMany( return payload; } - if (!(value.type === 'found' && payload.type === 'found')) { + if (!(value.status === 'found' && payload.status === 'found')) { return payload; } const newValue: LoadableData = { - type: 'found', + status: 'found', id: payload.id, value: { ...value, ...payload.value } }; diff --git a/packages/shared/src/lib/network/types.ts b/packages/shared/src/lib/network/types.ts index ed49c8801f..11063c34c7 100644 --- a/packages/shared/src/lib/network/types.ts +++ b/packages/shared/src/lib/network/types.ts @@ -8,8 +8,8 @@ export class ApiError extends Error { } export type Loadable = - | { type: 'loading' | 'not-found' } - | { type: 'found'; value: T } - | { type: 'error'; error: Error }; + | { status: 'loading' | 'not-found' } + | { status: 'found'; value: T } + | { status: 'error'; error: Error }; export type LoadableData = Loadable & { id: Id }; diff --git a/packages/shared/src/lib/organizations/organizationService.ts b/packages/shared/src/lib/organizations/organizationService.ts index 53842e0ef4..8f382ecb3c 100644 --- a/packages/shared/src/lib/organizations/organizationService.ts +++ b/packages/shared/src/lib/organizations/organizationService.ts @@ -33,7 +33,7 @@ export class OrganizationService { .findOrCreateSubscribable(undefined, async () => { const apiOrganizations = await this.httpClient.get('organization'); const organizations = apiOrganizations.map((apiOrganizations) => ({ - type: 'found', + status: 'found', id: apiOrganizations.slug, value: apiToOrganization(apiOrganizations) })); @@ -46,7 +46,7 @@ export class OrganizationService { getOrganizationWithDetailsInterest(slug: string): Interest { return this.orgnaizationInterests .findOrCreateSubscribable({ slug }, async () => { - this.appDispatch.dispatch(addOrganization({ type: 'loading', id: slug })); + this.appDispatch.dispatch(addOrganization({ status: 'loading', id: slug })); try { const apiOrganization = await this.httpClient.get( @@ -54,7 +54,7 @@ export class OrganizationService { ); const projects = apiOrganization.projects.map((apiProject) => ({ - type: 'found', + status: 'found', id: apiProject.repository_id, value: apiToProject(apiProject) })); @@ -62,7 +62,7 @@ export class OrganizationService { this.appDispatch.dispatch( upsertOrganization({ - type: 'found', + status: 'found', id: slug, value: apiToOrganization(apiOrganization) }) @@ -87,7 +87,9 @@ export class OrganizationService { } }); const organization = apiToOrganization(apiOrganization); - this.appDispatch.dispatch(upsertOrganization({ type: 'found', id: slug, value: organization })); + this.appDispatch.dispatch( + upsertOrganization({ status: 'found', id: slug, value: organization }) + ); return organization; } @@ -101,7 +103,9 @@ export class OrganizationService { ); const organization = apiToOrganization(apiOrganization); - this.appDispatch.dispatch(upsertOrganization({ type: 'found', id: slug, value: organization })); + this.appDispatch.dispatch( + upsertOrganization({ status: 'found', id: slug, value: organization }) + ); return organization; } diff --git a/packages/shared/src/lib/organizations/projectService.ts b/packages/shared/src/lib/organizations/projectService.ts index 82840cb538..1b7f8b4a23 100644 --- a/packages/shared/src/lib/organizations/projectService.ts +++ b/packages/shared/src/lib/organizations/projectService.ts @@ -17,13 +17,13 @@ export class ProjectService { getProjectInterest(repositoryId: string): Interest { return this.projectInterests .findOrCreateSubscribable({ repositoryId }, async () => { - this.appDispatch.dispatch(addProject({ type: 'loading', id: repositoryId })); + this.appDispatch.dispatch(addProject({ status: 'loading', id: repositoryId })); try { const apiProject = await this.httpClient.get(`projects/${repositoryId}`); this.appDispatch.dispatch( - upsertProject({ type: 'found', id: repositoryId, value: apiToProject(apiProject) }) + upsertProject({ status: 'found', id: repositoryId, value: apiToProject(apiProject) }) ); } catch (error: unknown) { this.appDispatch.dispatch(upsertProject(errorToLoadable(error, repositoryId))); @@ -39,7 +39,7 @@ export class ProjectService { const project = apiToProject(apiProject); this.appDispatch.dispatch( - upsertProject({ type: 'found', id: project.repositoryId, value: project }) + upsertProject({ status: 'found', id: project.repositoryId, value: project }) ); return project; @@ -59,7 +59,7 @@ export class ProjectService { const project = apiToProject(apiProject); this.appDispatch.dispatch( - upsertProject({ type: 'found', id: project.repositoryId, value: project }) + upsertProject({ status: 'found', id: project.repositoryId, value: project }) ); return project; diff --git a/packages/shared/src/lib/organizations/projectsPreview.svelte.ts b/packages/shared/src/lib/organizations/projectsPreview.svelte.ts index 3cbed33e66..673ae01658 100644 --- a/packages/shared/src/lib/organizations/projectsPreview.svelte.ts +++ b/packages/shared/src/lib/organizations/projectsPreview.svelte.ts @@ -56,10 +56,10 @@ export function getFeedIdentityForRepositoryId( ); const current = $derived.by>(() => { - if (!isFound(parentProject.current)) return parentProject.current || { type: 'loading' }; + if (!isFound(parentProject.current)) return parentProject.current || { status: 'loading' }; return { - type: 'found', + status: 'found', value: `${parentProject.current.value.owner}/${parentProject.current.value.slug}` }; }); diff --git a/packages/shared/src/lib/persisted.ts b/packages/shared/src/lib/persisted.ts index 42b816c135..ca3afa096e 100644 --- a/packages/shared/src/lib/persisted.ts +++ b/packages/shared/src/lib/persisted.ts @@ -20,7 +20,6 @@ export function setStorageItem(key: string, value: unknown): void { export function persisted(initial: T, key: string): Persisted { function setAndPersist(value: T, set: (value: T) => void) { - console.log(key, value); setStorageItem(key, value); set(value); } diff --git a/packages/shared/src/lib/users/userService.ts b/packages/shared/src/lib/users/userService.ts index 2215c3a9f8..cbed3df6fa 100644 --- a/packages/shared/src/lib/users/userService.ts +++ b/packages/shared/src/lib/users/userService.ts @@ -17,12 +17,12 @@ export class UserService { getUserInterest(login: string): Interest { return this.userInterests .findOrCreateSubscribable({ login }, async () => { - this.appDispatch.dispatch(addUser({ type: 'loading', id: login })); + this.appDispatch.dispatch(addUser({ status: 'loading', id: login })); try { const apiUser = await this.httpClient.get(`user/${login}`); const user = apiToUser(apiUser); - this.appDispatch.dispatch(upsertUser({ type: 'found', id: login, value: user })); + this.appDispatch.dispatch(upsertUser({ status: 'found', id: login, value: user })); } catch (error: unknown) { this.appDispatch.dispatch(upsertUser(errorToLoadable(error, login))); }