Skip to content

Commit

Permalink
Convert project over to loadable data
Browse files Browse the repository at this point in the history
  • Loading branch information
Caleb-T-Owens committed Dec 17, 2024
1 parent 7c57ff7 commit edc9797
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 138 deletions.
7 changes: 5 additions & 2 deletions apps/desktop/src/lib/backend/projectCloudSync.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/lib/feeds/CreatePost.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -138,36 +139,44 @@
{/snippet}
</Section>

{#if !cloudProject?.parentProjectRepositoryId}
<Section>
{#snippet title()}
Link your project with an organization
{/snippet}

<RegisterInterest interest={usersOrganizationsInterest} />

<div>
{#each usersOrganizations as organization, index}
<SectionCard
roundedBottom={index === usersOrganizations.length - 1}
roundedTop={index === 0}
orientation="row"
centerAlign
>
{#snippet children()}
<h5 class="text-15 text-bold flex-grow">{organization.name || organization.slug}</h5>
{/snippet}
{#snippet actions()}
<ProjectConnectModal
organizationSlug={organization.slug}
projectRepositoryId={cloudProject.repositoryId}
/>
{/snippet}
</SectionCard>
{/each}
</div>
</Section>
{/if}
<Loading loadable={cloudProject}>
{#snippet children(cloudProject)}
<Section>
{#snippet title()}
Link your project with an organization
{/snippet}

<RegisterInterest interest={usersOrganizationsInterest} />

<div>
{#each usersOrganizations as loadableOrganization, index}
<SectionCard
roundedBottom={index === usersOrganizations.length - 1}
roundedTop={index === 0}
orientation="row"
centerAlign
>
{#snippet children()}
<Loading loadable={loadableOrganization}>
{#snippet children(organization)}
<h5 class="text-15 text-bold flex-grow">
{organization.name || organization.slug}
</h5>
{/snippet}
</Loading>
{/snippet}
{#snippet actions()}
<ProjectConnectModal
organizationSlug={loadableOrganization.id}
projectRepositoryId={cloudProject.repositoryId}
/>
{/snippet}
</SectionCard>
{/each}
</div>
</Section>
{/snippet}
</Loading>
{:else if !$project?.api?.repository_id}
<Section>
<Button onclick={createProject}>Create Gitbutler Project</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext } 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 } from '@gitbutler/shared/organizations/projectService';
Expand All @@ -26,53 +27,59 @@
);
const projectInterest = $derived(projectsService.getProjectInterest(projectRepositoryId));
const organization = $derived(
const chosenOrganization = $derived(
organizationsSelectors.selectById(appState.organizations, organizationSlug)
);
const project = $derived(projectsSelectors.selectById(appState.projects, projectRepositoryId));
const organizationProjects = $derived(
organization?.projectRepositoryIds?.map((repositoryId) => ({
project: projectsSelectors.selectById(appState.projects, repositoryId),
projectInterest: projectsService.getProjectInterest(repositoryId)
})) || []
const targetProject = $derived(
projectsSelectors.selectById(appState.projects, projectRepositoryId)
);
const organizationProjects = $derived.by(() => {
if (chosenOrganization?.type !== 'found') return [];
return (
chosenOrganization.value.projectRepositoryIds?.map((repositoryId) => ({
project: projectsSelectors.selectById(appState.projects, repositoryId),
interest: projectsService.getProjectInterest(repositoryId)
})) || []
);
});
function connectToOrganization(projectSlug?: string) {
if (!project || !organization) return;
if (targetProject?.type !== 'found' || chosenOrganization?.type !== 'found') return;
projectsService.connectProjectToOrganization(
project.repositoryId,
organization.slug,
targetProject.value.repositoryId,
chosenOrganization.value.slug,
projectSlug
);
}
const title = $derived.by(() => {
if (targetProject?.type !== 'found' || chosenOrganization?.type !== 'found') return;
return `Join ${targetProject.value.name} into ${chosenOrganization.value.name}`;
});
let modal = $state<Modal>();
</script>

<Modal bind:this={modal} title={`Join ${project?.name} into ${organization?.name}`}>
<Modal bind:this={modal} {title}>
<RegisterInterest interest={organizationInterest} />
<RegisterInterest interest={projectInterest} />

{#if organization}
{#each organizationProjects as { project, projectInterest }, index}
<RegisterInterest interest={projectInterest} />

<SectionCard
roundedTop={index === 0}
roundedBottom={index === organizationProjects.length - 1}
>
{#if project}
<h5>{project.name}</h5>

<Button onclick={() => connectToOrganization(project.slug)}>Connect</Button>
{:else}
<p>Loading...</p>
{/if}
</SectionCard>
{/each}
{/if}
{#each organizationProjects as { project: organizationProject, interest }, index}
<RegisterInterest {interest} />

<SectionCard roundedTop={index === 0} roundedBottom={index === organizationProjects.length - 1}>
<Loading loadable={organizationProject}>
{#snippet children(organizationProject)}
<h5>{organizationProject.name}</h5>

<Button onclick={() => connectToOrganization(organizationProject.slug)}>Connect</Button>
{/snippet}
</Loading>
</SectionCard>
{/each}

<Button onclick={() => connectToOrganization()}>Create organization project</Button>
</Modal>
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/routes/[projectId]/feed/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement | undefined>();
$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);
}
});
Expand All @@ -51,7 +58,7 @@

<hr />

{#if feed.current}
{#if feed?.current}
{#each feed.current.postIds as postId, index (postId)}
<div class="bleep-container">
{#if index < feed.current.postIds.length - 1 && lastPost.current && feedIdentity?.current}
Expand Down
41 changes: 23 additions & 18 deletions apps/web/src/routes/organizations/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { getContext } from '@gitbutler/shared/context';
import { HttpClient } from '@gitbutler/shared/network/httpClient';
import RegisterInterest from '@gitbutler/shared/interest/RegisterInterest.svelte';
import Loading from '@gitbutler/shared/network/Loading.svelte';
import { HttpClient } from '@gitbutler/shared/network/httpClient';
import CreateOrganizationModal from '@gitbutler/shared/organizations/CreateOrganizationModal.svelte';
import JoinOrganizationModal from '@gitbutler/shared/organizations/JoinOrganizationModal.svelte';
import OrganizationModal from '@gitbutler/shared/organizations/OrganizationModal.svelte';
Expand Down Expand Up @@ -32,25 +33,29 @@
<Button onclick={() => createOrganizationModal?.show()}>Create an Organizaton</Button>

<div>
{#each organizations as organization, index (organization.slug)}
<SectionCard
roundedTop={index === 0}
roundedBottom={index === organizations.length - 1}
orientation="row"
>
{#snippet children()}
<div class="inline">
<p class="text-15 text-bold">{organization.name || organization.slug}</p>
{#if organization.name}
<p class="text-13">{organization.slug}</p>
{/if}
</div>
{/snippet}
{#each organizations as organization, index (organization.id)}
<Loading loadable={organization}>
{#snippet children(organization)}
<SectionCard
roundedTop={index === 0}
roundedBottom={index === organizations.length - 1}
orientation="row"
>
{#snippet children()}
<div class="inline">
<p class="text-15 text-bold">{organization.name || organization.slug}</p>
{#if organization.name}
<p class="text-13">{organization.slug}</p>
{/if}
</div>
{/snippet}

{#snippet actions()}
<OrganizationModal slug={organization.slug} />
{#snippet actions()}
<OrganizationModal slug={organization.slug} />
{/snippet}
</SectionCard>
{/snippet}
</SectionCard>
</Loading>
{/each}
</div>

Expand Down
66 changes: 62 additions & 4 deletions packages/shared/src/lib/network/loadable.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,62 @@
export type LoadableData<T, Id> =
| { type: 'loading' | 'not-found'; id: Id; error?: undefined }
| { type: 'found'; id: Id; value: T; error?: undefined }
| { type: 'error'; id: Id; error: Error };
import { ApiError } from '$lib/network/types';
import type { EntityId, EntityAdapter, EntityState } from '@reduxjs/toolkit';

export type Loadable<T> =
| { type: 'loading' | 'not-found' }
| { type: 'found'; value: T }
| { type: 'error'; error: Error };

export type LoadableData<T, Id> = Loadable<T> & { id: Id };

export function errorToLoadable<T, Id>(error: unknown, id: Id): LoadableData<T, Id> {
if (error instanceof Error) {
if (error instanceof ApiError && error.response.status === 404) {
return { type: 'not-found', id };
}

return { type: 'error', id, error };
}

return { type: 'error', id, error: new Error(String(error)) };
}

export function loadableUpsert<T, Id extends EntityId>(
adapter: EntityAdapter<LoadableData<T, Id>, Id>
) {
return (
state: EntityState<LoadableData<T, Id>, Id>,
action: { payload: LoadableData<T, Id> }
) => {
loadableUpsertMany(adapter)(state, { payload: [action.payload] });
};
}

export function loadableUpsertMany<T, Id extends EntityId>(
adapter: EntityAdapter<LoadableData<T, Id>, Id>
) {
return (
state: EntityState<LoadableData<T, Id>, Id>,
action: { payload: LoadableData<T, Id>[] }
) => {
const values = action.payload.map((payload) => {
const value = state.entities[payload.id];
if (value === undefined) {
return payload;
}

if (!(value.type === 'found' && payload.type === 'found')) {
return payload;
}

const newValue: LoadableData<T, Id> = {
type: 'found',
id: payload.id,
value: { ...value, ...payload.value }
};

return newValue;
});

adapter.setMany(state, values);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@
roundedTop={index === 0}
orientation="row"
>
<p>{project?.name}</p>
<Loading loadable={project}>
{#snippet children(project)}
<p>{project.name}</p>
{/snippet}
</Loading>
</SectionCard>
{/each}
</div>
Expand Down
Loading

0 comments on commit edc9797

Please sign in to comment.