Skip to content

Commit

Permalink
Show organizations in summary section on project page (#921)
Browse files Browse the repository at this point in the history
Implements features that show organizations in summary on project page if any. If the project does not belong to any organizations, there would be a button to add an organization, but you have to be a project manager AND a member of the organization that you are trying to add the project to.

* show org in project summary
* show name of the org
* create button to add org to project
* only project manager can add org
* add error handling & refactor type
* extract getOrgs into its own method
* cache invalidation for addProjectToOrg mutation
  • Loading branch information
psh0078 authored Jul 9, 2024
1 parent 87b65ce commit d6a647f
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 0 deletions.
4 changes: 4 additions & 0 deletions frontend/src/lib/gql/gql-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
type SoftDeleteProjectMutationVariables,
type BulkAddProjectMembersMutationVariables,
type DeleteDraftProjectMutationVariables,
type MutationAddProjectToOrgArgs,
} from './types';
import type {Readable, Unsubscriber} from 'svelte/store';
import {derived} from 'svelte/store';
Expand Down Expand Up @@ -71,6 +72,9 @@ function createGqlClient(_gqlEndpoint?: string): Client {
},
leaveProject: (result, args: LeaveProjectMutationVariables, cache, _info) => {
cache.invalidate({__typename: 'Project', id: args.input.projectId});
},
addProjectToOrg: (result, args: MutationAddProjectToOrgArgs, cache, _info) => {
cache.invalidate({__typename: 'Project', id: args.input.projectId});
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia
"user_needs_to_relogin": "Added members will need to log out and back in again before they see the new project.",
"invalid_email_address": "Invalid email address: {email}",
},
"add_org": {
"add_button": "Add Organization",
"modal_title": "Choose organization",
"submit_button": "Add Organization",
"org_not_found": "Organization not found. Please refresh the page.",
"project_not_found": "Project not found. Please refresh the page."
},
"bulk_add_members": {
"add_button": "Bulk Add/Create Members",
"explanation": "Adds all the entered logins and emails to this project. New accounts will be created automatically for users that don't have one yet.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import { DetailItem, EditableDetailItem } from '$lib/layout';
import MembersList from './MembersList.svelte';
import DetailsPage from '$lib/layout/DetailsPage.svelte';
import OrgList from './OrgList.svelte';
import AddOrganization from './AddOrganization.svelte';
export let data: PageData;
$: user = data.user;
Expand Down Expand Up @@ -343,6 +345,16 @@
</svelte:fragment>

<div class="space-y-4">
<OrgList
organizations={project.organizations}
>
<svelte:fragment slot="extraButtons">
{#if canManage}
<AddOrganization projectId={project.id} userIsAdmin={user.isAdmin} />
{/if}
</svelte:fragment>
</OrgList>

<MembersList
projectId={project.id}
{members}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type {
$OpResult,
AddProjectMemberInput,
AddProjectMemberMutation,
AddProjectToOrgInput,
AddProjectToOrgMutation,
BulkAddProjectMembersInput,
BulkAddProjectMembersMutation,
ChangeProjectDescriptionInput,
Expand All @@ -12,6 +14,7 @@ import type {
ChangeProjectNameMutation,
DeleteProjectUserMutation,
LeaveProjectMutation,
Organization,
ProjectPageQuery,
SetProjectConfidentialityInput,
SetProjectConfidentialityMutation,
Expand All @@ -26,6 +29,7 @@ import { tryMakeNonNullable } from '$lib/util/store';
export type Project = NonNullable<ProjectPageQuery['projectByCode']>;
export type ProjectUser = Project['users'][number];
export type User = ProjectUser['user'];
export type Org = Pick<Organization, 'id' | 'name'>;

export async function load(event: PageLoadEvent) {
const client = getClient();
Expand Down Expand Up @@ -77,6 +81,10 @@ export async function load(event: PageLoadEvent) {
flexProjectMetadata {
lexEntryCount
}
organizations {
id
name
}
}
}
`),
Expand Down Expand Up @@ -124,6 +132,55 @@ export async function load(event: PageLoadEvent) {
};
}

export async function _getOrgs(userIsAdmin: boolean): Promise<Org[]> {
const client = getClient();
if (userIsAdmin) {
const orgsResult = await client.query(graphql(`
query loadOrgs {
orgs {
id
name
}
}
`), {}, {});
return orgsResult.data?.orgs ?? [];
} else {
const myOrgsResult = await client.query(graphql(`
query loadMyOrgs {
myOrgs {
id
name
}
}
`), {}, {});
return myOrgsResult.data?.myOrgs ?? [];
}
}

export async function _addProjectToOrg(input: AddProjectToOrgInput): $OpResult<AddProjectToOrgMutation> {
//language=GraphQL
const result = await getClient()
.mutation(
graphql(`
mutation AddProjectToOrg($input: AddProjectToOrgInput!) {
addProjectToOrg(input: $input) {
organization {
id
}
errors {
__typename
... on Error {
message
}
}
}
}
`),
{ input: input }
);
return result;
}

export async function _addProjectMember(input: AddProjectMemberInput): $OpResult<AddProjectMemberMutation> {
//language=GraphQL
const result = await getClient()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import { z } from 'zod';
import t from '$lib/i18n';
import { Select } from '$lib/forms';
import { _addProjectToOrg, _getOrgs } from './+page';
import type { Organization } from '$lib/gql/types';
import { FormModal } from '$lib/components/modals';
import { BadgeButton } from '$lib/components/Badges';
type Org = Pick<Organization, 'id' | 'name'>;
export let projectId: string;
export let userIsAdmin: boolean;
let orgList: Org[] = [];
const schema = z.object({
orgId: z.string().trim()
});
type Schema = typeof schema;
let formModal: FormModal<Schema>;
$: form = formModal?.form();
async function openModal(): Promise<void> {
orgList = await _getOrgs(userIsAdmin);
await formModal.open(async () => {
const { error } = await _addProjectToOrg({
projectId,
orgId: $form.orgId,
})
if (error?.byType('NotFoundError')) {
if (error.message === 'Organization not found') return $t('project_page.add_org.org_not_found');
if (error.message === 'Project not found') return $t('project_page.add_org.project_not_found');
}
});
}
</script>

<BadgeButton variant="badge-success" icon="i-mdi-account-plus-outline" on:click={openModal}>
{$t('project_page.add_org.add_button')}
</BadgeButton>

<FormModal bind:this={formModal} {schema} let:errors>
<span slot="title">{$t('project_page.add_org.modal_title')}</span>
<Select
id="org"
label={$t('project_page.organization.title')}
bind:value={$form.orgId}
error={errors.orgId}
>
{#each orgList as org}
<option value={org.id}>{org.name}</option>
{/each}
</Select>
<span slot="submitText">{$t('project_page.add_org.submit_button')}</span>
</FormModal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import t from '$lib/i18n';
import { Badge, BadgeList } from '$lib/components/Badges';
import type { Organization } from '$lib/gql/types';
type Org = Pick<Organization, 'id' | 'name'>;
export let organizations: Org[] = [];
const TRUNCATED_MEMBER_COUNT = 5;
</script>


<div>
<p class="text-2xl mb-4 flex items-baseline gap-4 max-sm:flex-col">
{$t('project_page.organization.title')}
</p>

<BadgeList grid={organizations.length > TRUNCATED_MEMBER_COUNT}>
{#if !organizations.length}
<span class="text-secondary mx-2 my-1">{$t('common.none')}</span>
<div class="flex grow flex-wrap place-self-end gap-3 place-content-end" style="grid-column: -2 / -1">
<slot name="extraButtons" />
</div>
{/if}
{#each organizations as org (org.id)}
<Badge>
{org.name}
</Badge>
{/each}
</BadgeList>
</div>

0 comments on commit d6a647f

Please sign in to comment.