Skip to content

Commit

Permalink
remove project from org (#972)
Browse files Browse the repository at this point in the history
this PR implements feature of removing project from org on project page and org page. On project page, both project manager, org manager, and admin should be able to remove the project from org, but on org page, only org manager and admin can execute the action.
  • Loading branch information
psh0078 authored Jul 24, 2024
1 parent 1930cfd commit 43baf7c
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 21 deletions.
6 changes: 3 additions & 3 deletions frontend/src/lib/components/modals/DeleteModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
}
</script>
<ConfirmModal bind:this={modal}
title={isRemoveDialog ?$t('delete_modal.remove', { entityName }): $t('delete_modal.delete', { entityName })}
submitText={isRemoveDialog ? $t('delete_modal.remove', { entityName }): $t('delete_modal.delete', { entityName })}
title={isRemoveDialog ? $t('delete_modal.remove', { entityName }) : $t('delete_modal.delete', { entityName })}
submitText={isRemoveDialog ? $t('delete_modal.remove', { entityName }) : $t('delete_modal.delete', { entityName })}
submitIcon="i-mdi-trash-can"
submitVariant="btn-error"
cancelText={isRemoveDialog?$t('delete_modal.dont_remove'): $t('delete_modal.dont_delete')}>
cancelText={isRemoveDialog ? $t('delete_modal.dont_remove') : $t('delete_modal.dont_delete')}>
<slot/>
</ConfirmModal>
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 @@ -28,6 +28,7 @@ import {
type BulkAddProjectMembersMutationVariables,
type DeleteDraftProjectMutationVariables,
type MutationAddProjectToOrgArgs,
type MutationRemoveProjectFromOrgArgs,
type BulkAddOrgMembersMutationVariables,
type ChangeOrgMemberRoleMutationVariables,
type AddOrgMemberMutationVariables,
Expand Down Expand Up @@ -87,6 +88,9 @@ function createGqlClient(_gqlEndpoint?: string): Client {
},
addProjectToOrg: (result, args: MutationAddProjectToOrgArgs, cache, _info) => {
cache.invalidate({__typename: 'Project', id: args.input.projectId});
},
removeProjectFromOrg: (result, args: MutationRemoveProjectFromOrgArgs, cache, _info) => {
cache.invalidate({__typename: 'Project', id: args.input.projectId});
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia
"user_delete": "{name} has been removed.",
"rename_org": "Organization name set to {name}.",
"delete_org": "Organization {name} has been deleted.",
"remove_project_from_org": "You have successfully removed {projectName} from this organization",
"leave_org": "You have left the organization {name}.",
"leave_org_error": "An error occurred trying to remove you from organization {name}. Please try again later.",
"describe": "Organization description has been updated.",
Expand All @@ -310,6 +311,9 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia
"members_table_title": "Members",
"settings_view_title": "Settings",
"history_view_title": "History",
"remove_project_from_org_title": "Project",
"remove_project_from_org": "Remove",
"confirm_remove_project_from_org": "Would you like to remove {projectName} from {orgName}?",
"leave_org": "Leave Organization",
},
"project_page": {
Expand Down Expand Up @@ -393,9 +397,11 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia
"promote_project": "Project has been changed from draft status to real project.",
"describe": "Project description has been updated.",
"add_member": "{email} has been added to project.",
"member_invited": "{email} has been sent an invitation email to register and join the project."
"member_invited": "{email} has been sent an invitation email to register and join the project.",
"remove_project_from_org": "Your project has been removed from {orgName}"
},
"project_name_empty_error": "Project name cannot be empty",
"confirm_remove_org": "Would you like to remove your project from {orgName}?",
"confirm_remove": "Would you like to remove {userName} from this project?",
"history": "History",
"project_code": "Project Code",
Expand All @@ -418,8 +424,11 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia
"no_matching": "No matching members",
},
"add_description": "Add description...",
"view_org": "View {orgName}",
"remove_project_user_title": "Member",
"remove_user": "Remove",
"remove_project_from_org_title": "Project",
"remove_project_from_org": "Remove",
"change_role": "Change Role",
"view_user_details": "View details",
"hg": {
Expand Down
64 changes: 52 additions & 12 deletions frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
import type { PageData } from './$types';
import { OrgRole } from '$lib/gql/types';
import { useNotifications } from '$lib/notify';
import { _changeOrgName, _deleteOrgUser, _deleteOrg, _orgMemberById, type OrgSearchParams, type User, type OrgUser } from './+page';
import { _changeOrgName, _deleteOrgUser, _deleteOrg, _orgMemberById, type OrgSearchParams, type User, type OrgUser, _removeProjectFromOrg } from './+page';
import OrgTabs, { type OrgTabId } from './OrgTabs.svelte';
import { getSearchParams, queryParam } from '$lib/util/query-params';
import { Icon, TrashIcon } from '$lib/icons';
import ConfirmDeleteModal from '$lib/components/modals/ConfirmDeleteModal.svelte';
import DeleteModal from '$lib/components/modals/DeleteModal.svelte';
import { goto } from '$app/navigation';
import { DialogResponse } from '$lib/components/modals';
import AddOrgMemberModal from './AddOrgMemberModal.svelte';
Expand All @@ -21,6 +22,7 @@
import ProjectTable from '$lib/components/Projects/ProjectTable.svelte';
import type { UUID } from 'crypto';
import BulkAddOrgMembers from './BulkAddOrgMembers.svelte';
import Dropdown from '$lib/components/Dropdown.svelte';
export let data: PageData;
$: user = data.user;
Expand Down Expand Up @@ -80,6 +82,19 @@
}
}
let removeProjectFromOrgModal: DeleteModal;
let projectToRemove: string;
async function removeProjectFromOrg(projectId: string, projectName: string): Promise<void> {
projectToRemove = projectName;
const removed = await removeProjectFromOrgModal.prompt(async () => {
const { error } = await _removeProjectFromOrg(projectId, org.id);
return error?.message;
});
if (removed) {
notifyWarning($t('org_page.notifications.remove_project_from_org', {projectName: projectToRemove}));
}
}
async function leaveOrg(): Promise<void> {
const result = await _deleteOrgUser(org.id, user.id);
if (result.error) {
Expand Down Expand Up @@ -121,18 +136,43 @@
</div>
<div class="py-6 px-2">
{#if $queryParamValues.tab === 'projects'}
<ProjectTable
columns={['name', 'code', 'users', 'type']}
projects={org.projects}
/>
<ProjectTable
columns={['name', 'code', 'users', 'type']}
projects={org.projects}
>
<td class="p-0" slot="actions" let:project>
{#if canManage}
<Dropdown>
<button class="btn btn-ghost btn-square">
<span class="i-mdi-dots-vertical text-lg" />
</button>
<ul slot="content" class="menu">
<li>
<button class="text-error" on:click={() => removeProjectFromOrg(project.id, project.name)}>
<TrashIcon />
{$t('org_page.remove_project_from_org')}
</button>
</li>
</ul>
</Dropdown>
{/if}
<td/>
</ProjectTable>
<DeleteModal
bind:this={removeProjectFromOrgModal}
entityName={$t('org_page.remove_project_from_org_title')}
isRemoveDialog
>
{$t('org_page.confirm_remove_project_from_org', {projectName: projectToRemove, orgName: org.name})}
</DeleteModal>
{:else if $queryParamValues.tab === 'members'}
<OrgMemberTable
shownUsers={org.members}
showEmailColumn={canManage}
on:openUserModal={(event) => openUserModal(event.detail)}
on:removeMember={(event) => _deleteOrgUser(org.id, event.detail.id)}
on:changeMemberRole={(event) => openChangeMemberRoleModal(event.detail)}
/>
<OrgMemberTable
shownUsers={org.members}
showEmailColumn={canManage}
on:openUserModal={(event) => openUserModal(event.detail)}
on:removeMember={(event) => _deleteOrgUser(org.id, event.detail.id)}
on:changeMemberRole={(event) => openChangeMemberRoleModal(event.detail)}
/>
{:else if $queryParamValues.tab === 'history'}
<div class="space-y-2">
<DetailItem title={$t('org_page.details.created_at')} text={$date(org.createdDate)} />
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/routes/(authenticated)/org/[org_id]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
OrgMemberDto,
OrgPageQuery,
OrgRole,
RemoveProjectFromOrgMutation,
} from '$lib/gql/types';
import { getClient, graphql } from '$lib/gql';

Expand Down Expand Up @@ -175,6 +176,30 @@ export async function _bulkAddOrgMembers(orgId: UUID, usernames: string[], role:
return result;
}

export async function _removeProjectFromOrg(projectId: string, orgId: string): $OpResult<RemoveProjectFromOrgMutation> {
//language=GraphQL
const result = await getClient()
.mutation(
graphql(`
mutation RemoveProjectFromOrg($input: RemoveProjectFromOrgInput!) {
removeProjectFromOrg(input: $input) {
organization {
id
}
errors {
__typename
... on Error {
message
}
}
}
}
`),
{ input: { projectId: projectId, orgId: orgId } }
);
return result;
}

export async function _orgMemberById(orgId: UUID, userId: UUID): Promise<OrgMemberDto> {
//language=GraphQL
const result = await getClient()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
_changeProjectName,
_deleteProjectUser,
_leaveProject,
_removeProjectFromOrg,
type ProjectUser,
} from './+page';
import AddProjectMember from './AddProjectMember.svelte';
Expand Down Expand Up @@ -107,6 +108,19 @@
}
}
let removeProjectFromOrgModal: DeleteModal;
let orgToRemove: string;
async function removeProjectFromOrg(orgId: string, orgName: string): Promise<void> {
orgToRemove = orgName;
const removed = await removeProjectFromOrgModal.prompt(async () => {
const { error } = await _removeProjectFromOrg(project.id, orgId);
return error?.message;
});
if (removed) {
notifyWarning($t('project_page.notifications.remove_project_from_org', {orgName: orgToRemove}));
}
}
async function updateProjectName(newName: string): Promise<ErrorMessage> {
const result = await _changeProjectName({ projectId: project.id, name: newName });
if (result.error) {
Expand Down Expand Up @@ -391,12 +405,23 @@
</svelte:fragment>

<div class="space-y-4">
<OrgList organizations={project.organizations} >
<OrgList
canManage={canManage}
organizations={project.organizations}
on:removeProjectFromOrg={(event) => removeProjectFromOrg(event.detail.orgId, event.detail.orgName)}
>
<svelte:fragment slot="extraButtons">
{#if canManage}
<AddOrganization projectId={project.id} userIsAdmin={user.isAdmin} />
{/if}
</svelte:fragment>
<DeleteModal
bind:this={removeProjectFromOrgModal}
entityName={$t('project_page.remove_project_from_org_title')}
isRemoveDialog
>
{$t('project_page.confirm_remove_org', {orgName: orgToRemove})}
</DeleteModal>
</OrgList>
{#if members}
<MembersList
Expand Down Expand Up @@ -424,7 +449,6 @@
</DeleteModal>
</MembersList>
{/if}

<div class="divider" />
<div class="space-y-2">
<p class="text-2xl mb-4 flex gap-4 items-baseline">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
LeaveProjectMutation,
Organization,
ProjectPageQuery,
RemoveProjectFromOrgMutation,
SetProjectConfidentialityInput,
SetProjectConfidentialityMutation,
SetRetentionPolicyInput,
Expand Down Expand Up @@ -346,6 +347,30 @@ export async function _changeProjectDescription(input: ChangeProjectDescriptionI
return result;
}

export async function _removeProjectFromOrg(projectId: string, orgId: string): $OpResult<RemoveProjectFromOrgMutation> {
//language=GraphQL
const result = await getClient()
.mutation(
graphql(`
mutation RemoveProjectFromOrg($input: RemoveProjectFromOrgInput!) {
removeProjectFromOrg(input: $input) {
organization {
id
}
errors {
__typename
... on Error {
message
}
}
}
}
`),
{ input: { projectId: projectId, orgId: orgId } }
);
return result;
}

export async function _setProjectConfidentiality(input: SetProjectConfidentialityInput): $OpResult<SetProjectConfidentialityMutation> {
//language=GraphQL
const result = await getClient()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
<slot name="extraButtons" />
</div>
{/if}

<slot />

</BadgeList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@
import t from '$lib/i18n';
import { Badge, BadgeList } from '$lib/components/Badges';
import type { Organization } from '$lib/gql/types';
import { createEventDispatcher } from 'svelte';
import Dropdown from '$lib/components/Dropdown.svelte';
import { Icon, TrashIcon } from '$lib/icons';
import ActionBadge from '$lib/components/Badges/ActionBadge.svelte';
type Org = Pick<Organization, 'id' | 'name'>;
export let canManage: boolean;
export let organizations: Org[] = [];
const dispatch = createEventDispatcher<{
removeProjectFromOrg: { orgId: string; orgName: string };
}>();
const TRUNCATED_MEMBER_COUNT = 5;
</script>

Expand All @@ -23,9 +32,35 @@
</div>
{/if}
{#each organizations as org (org.id)}
<Badge>
{org.name}
</Badge>
{#if !canManage}
<Badge>
{org.name}
</Badge>
{:else}
<Dropdown>
<ActionBadge actionIcon="i-mdi-dots-vertical" on:action>
<span class="pr-3 whitespace-nowrap overflow-ellipsis overflow-x-clip" title={org.name}>
{org.name}
</span>
</ActionBadge>
<ul slot="content" class="menu">
<li>
<a href={`/org/${org.id}`}>
<Icon icon="i-mdi-link"/>
{$t('project_page.view_org', {orgName: org.name})}
</a>
</li>
<li>
<button class="text-error" on:click={() => dispatch('removeProjectFromOrg', {orgId: org.id, orgName: org.name})}>
<TrashIcon />
{$t('project_page.remove_project_from_org')}
</button>
</li>
</ul>
</Dropdown>
{/if}
{/each}
</BadgeList>

<slot />
</div>

0 comments on commit 43baf7c

Please sign in to comment.