Skip to content

Commit

Permalink
Added project switcher
Browse files Browse the repository at this point in the history
  • Loading branch information
Caleb-T-Owens committed Jan 20, 2025
1 parent 3131277 commit 91b9306
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 19 deletions.
51 changes: 50 additions & 1 deletion apps/web/src/lib/components/breadcrumbs/Breadcrumbs.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,56 @@
<script lang="ts">
import { getBreadcrumbsContext } from '$lib/components/breadcrumbs/breadcrumbsContext.svelte';
import { UserService } from '$lib/user/userService';
import { getContext } from '@gitbutler/shared/context';
import Loading from '@gitbutler/shared/network/Loading.svelte';
import { OrganizationService } from '@gitbutler/shared/organizations/organizationService';
import { ProjectService } from '@gitbutler/shared/organizations/projectService';
import { getAllUserRelatedProjects } from '@gitbutler/shared/organizations/projectsPreview.svelte';
import { AppState } from '@gitbutler/shared/redux/store.svelte';
import Button from '@gitbutler/ui/Button.svelte';
import ContextMenu from '@gitbutler/ui/ContextMenu.svelte';
import ContextMenuItem from '@gitbutler/ui/ContextMenuItem.svelte';
import ContextMenuSection from '@gitbutler/ui/ContextMenuSection.svelte';
const breadcrumbsContext = getBreadcrumbsContext();
const appState = getContext(AppState);
const projectService = getContext(ProjectService);
const organizationService = getContext(OrganizationService);
const userService = getContext(UserService);
const user = userService.user;
$inspect(breadcrumbsContext);
let projectSwitcher = $state<ContextMenu>();
let leftClickTrigger = $state<HTMLElement>();
const allProjects = $derived(
projectSwitcher?.isOpen() && $user?.login
? getAllUserRelatedProjects(appState, projectService, organizationService, $user.login)
: undefined
);
function openProjectSwitcher() {
projectSwitcher?.open();
}
</script>

<div bind:this={leftClickTrigger}>
<Button kind="ghost" icon="select-chevron" onclick={openProjectSwitcher}>
{#if breadcrumbsContext.current.ownerSlug && breadcrumbsContext.current.projectSlug}
{breadcrumbsContext.current.ownerSlug}/{breadcrumbsContext.current.projectSlug}
{:else}
Select project
{/if}
</Button>
</div>

<ContextMenu {leftClickTrigger} bind:this={projectSwitcher}>
<ContextMenuSection>
{#each allProjects?.current || [] as project}
<Loading loadable={project}>
{#snippet children(project)}
<ContextMenuItem label="{project.owner}/{project.slug}" onclick={() => {}} />
{/snippet}
</Loading>
{/each}
</ContextMenuSection>
</ContextMenu>
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function registerInterestInView(interest: Interest, element?: HTMLElement

const observer = new IntersectionObserver(
(entries) => {
console.log(entries);
inView = entries[0]?.isIntersecting || false;
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { registerInterest, type InView } from '$lib/interest/registerInterestFunction.svelte';
import { map } from '$lib/network/loadable';
import { organizationsSelectors } from '$lib/organizations/organizationsSlice';
import { projectsSelectors } from '$lib/organizations/projectsSlice';
import type { OrganizationService } from '$lib/organizations/organizationService';
import type { LoadableOrganization } from '$lib/organizations/types';
import type { AppOrganizationsState } from '$lib/redux/store.svelte';
import type { LoadableOrganization, LoadableProject } from '$lib/organizations/types';
import type { AppOrganizationsState, AppProjectsState } from '$lib/redux/store.svelte';
import type { Reactive } from '$lib/storeUtils';

export function getOrganizations(
Expand Down Expand Up @@ -35,3 +37,28 @@ export function getOrganizationBySlug(
}
};
}

export function getOrganizationProjects(
appState: AppProjectsState & AppOrganizationsState,
organizationService: OrganizationService,
slug: string,
inView?: InView
): Reactive<LoadableProject[] | undefined> {
registerInterest(organizationService.getOrganizationWithDetailsInterest(slug), inView);
const organization = $derived(organizationsSelectors.selectById(appState.organizations, slug));
const projects = $derived(
map(
organization,
(organization) =>
(organization.projectRepositoryIds || [])
.map((id) => projectsSelectors.selectById(appState.projects, id))
.filter((a) => a !== undefined) as LoadableProject[]
)
);

return {
get current() {
return projects;
}
};
}
26 changes: 12 additions & 14 deletions packages/shared/src/lib/organizations/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import type { AppDispatch } from '$lib/redux/store.svelte';

export class ProjectService {
private readonly projectInterests = new InterestStore<{ repositoryId: string }>(POLLING_REGULAR);
private readonly userProjectsInterests = new InterestStore<{ user: string }>(POLLING_GLACIALLY);
private readonly userProjectsInterests = new InterestStore<{ unused: 'unused' }>(
POLLING_GLACIALLY
);

constructor(
private readonly httpClient: HttpClient,
Expand All @@ -38,22 +40,18 @@ export class ProjectService {
.createInterest();
}

getAllProjectsInterest(user: string): Interest {
getAllProjectsInterest(): Interest {
return this.userProjectsInterests
.findOrCreateSubscribable({ user }, async () => {
try {
const apiProjects = await this.httpClient.get<ApiProject[]>('projects');
.findOrCreateSubscribable({ unused: 'unused' }, async () => {
const apiProjects = await this.httpClient.get<ApiProject[]>('projects');

const projects: LoadableProject[] = apiProjects.map((apiProject) => ({
status: 'found',
id: apiProject.repository_id,
value: apiToProject(apiProject)
}));
const projects: LoadableProject[] = apiProjects.map((apiProject) => ({
status: 'found',
id: apiProject.repository_id,
value: apiToProject(apiProject)
}));

this.appDispatch.dispatch(upsertProjects(projects));
} catch (error: unknown) {
this.appDispatch.dispatch(upsertProject(errorToLoadable(error, user)));
}
this.appDispatch.dispatch(upsertProjects(projects));
})
.createInterest();
}
Expand Down
55 changes: 53 additions & 2 deletions packages/shared/src/lib/organizations/projectsPreview.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { registerInterest, type InView } from '$lib/interest/registerInterestFunction.svelte';
import { isFound } from '$lib/network/loadable';
import {
getOrganizationProjects,
getOrganizations
} from '$lib/organizations/organizationsPreview.svelte';
import { projectsSelectors } from '$lib/organizations/projectsSlice';
import type { Loadable } from '$lib/network/types';
import type { OrganizationService } from '$lib/organizations/organizationService';
import type { ProjectService } from '$lib/organizations/projectService';
import type { LoadableProject } from '$lib/organizations/types';
import type { LoadableOrganization, LoadableProject } from '$lib/organizations/types';
import type { AppOrganizationsState, AppProjectsState } from '$lib/redux/store.svelte';
import type { Reactive } from '$lib/storeUtils';

Expand All @@ -29,7 +34,7 @@ export function getAllUserProjects(
projectService: ProjectService,
inView?: InView
): Reactive<LoadableProject[]> {
registerInterest(projectService.getAllProjectsInterest(user), inView);
registerInterest(projectService.getAllProjectsInterest(), inView);
const current = $derived.by(() => {
const allProjects = projectsSelectors.selectAll(appState.projects);
return allProjects.filter((project) => isFound(project) && project.value.owner === user);
Expand All @@ -42,6 +47,52 @@ export function getAllUserProjects(
};
}

export function getAllUserRelatedProjects(
appState: AppProjectsState & AppOrganizationsState,
projectService: ProjectService,
organizationService: OrganizationService,
user: string,
inView?: InView
): Reactive<LoadableProject[]> {
registerInterest(projectService.getAllProjectsInterest(), inView);
const userProjects = $derived.by(() => {
const allProjects = projectsSelectors.selectAll(appState.projects);
return allProjects.filter(
(project) => isFound(project) && project.value.owner === user
) as LoadableProject[];
});

const organizations = $derived(getOrganizations(appState, organizationService));
const reactiveOrganizationProjects = $derived.by(() => {
if (!organizations.current) return [];

const foundOrganizations = organizations.current.filter((organization) =>
isFound(organization)
) as (LoadableOrganization & { status: 'found' })[];

return foundOrganizations.flatMap((organization) => {
return getOrganizationProjects(
appState,
organizationService,
organization.value.slug,
inView
);
});
});
const organizationProjects = $derived(
reactiveOrganizationProjects
.map((a) => a.current)
.filter((a) => a !== undefined)
.flat() as LoadableProject[]
);

return {
get current() {
return [...userProjects, ...organizationProjects];
}
};
}

export function getParentForRepositoryId(
appState: AppProjectsState & AppOrganizationsState,
projectService: ProjectService,
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/ContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@
if (verticalAlign === 'top') return horizontalAlign === 'left' ? 'bottom left' : 'bottom right';
return horizontalAlign === 'left' ? 'top left' : 'top right';
}
export function isOpen() {
return isVisible;
}
</script>

{#snippet contextMenu()}
Expand Down

0 comments on commit 91b9306

Please sign in to comment.