From 43baf7c1e21fd5df4b15690f2e27047c765f271b Mon Sep 17 00:00:00 2001 From: seongho park <30375118+psh0078@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:42:40 +0700 Subject: [PATCH] remove project from org (#972) 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. --- .../lib/components/modals/DeleteModal.svelte | 6 +- frontend/src/lib/gql/gql-client.ts | 4 ++ frontend/src/lib/i18n/locales/en.json | 11 +++- .../(authenticated)/org/[org_id]/+page.svelte | 64 +++++++++++++++---- .../(authenticated)/org/[org_id]/+page.ts | 25 ++++++++ .../project/[project_code]/+page.svelte | 28 +++++++- .../project/[project_code]/+page.ts | 25 ++++++++ .../project/[project_code]/MembersList.svelte | 1 + .../project/[project_code]/OrgList.svelte | 41 +++++++++++- 9 files changed, 184 insertions(+), 21 deletions(-) diff --git a/frontend/src/lib/components/modals/DeleteModal.svelte b/frontend/src/lib/components/modals/DeleteModal.svelte index 31be78352..60ca5bffc 100644 --- a/frontend/src/lib/components/modals/DeleteModal.svelte +++ b/frontend/src/lib/components/modals/DeleteModal.svelte @@ -12,10 +12,10 @@ } + cancelText={isRemoveDialog ? $t('delete_modal.dont_remove') : $t('delete_modal.dont_delete')}> diff --git a/frontend/src/lib/gql/gql-client.ts b/frontend/src/lib/gql/gql-client.ts index 0d0cabb4a..044ab6171 100644 --- a/frontend/src/lib/gql/gql-client.ts +++ b/frontend/src/lib/gql/gql-client.ts @@ -28,6 +28,7 @@ import { type BulkAddProjectMembersMutationVariables, type DeleteDraftProjectMutationVariables, type MutationAddProjectToOrgArgs, + type MutationRemoveProjectFromOrgArgs, type BulkAddOrgMembersMutationVariables, type ChangeOrgMemberRoleMutationVariables, type AddOrgMemberMutationVariables, @@ -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}); } } } diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 3778e28fa..d2706c8ca 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -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.", @@ -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": { @@ -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", @@ -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": { diff --git a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte index b0899c2bf..d78529439 100644 --- a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte +++ b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte @@ -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'; @@ -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; @@ -80,6 +82,19 @@ } } + let removeProjectFromOrgModal: DeleteModal; + let projectToRemove: string; + async function removeProjectFromOrg(projectId: string, projectName: string): Promise { + 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 { const result = await _deleteOrgUser(org.id, user.id); if (result.error) { @@ -121,18 +136,43 @@
{#if $queryParamValues.tab === 'projects'} - + + + {#if canManage} + + + + + {/if} + + + + {$t('org_page.confirm_remove_project_from_org', {projectName: projectToRemove, orgName: org.name})} + {:else if $queryParamValues.tab === 'members'} - openUserModal(event.detail)} - on:removeMember={(event) => _deleteOrgUser(org.id, event.detail.id)} - on:changeMemberRole={(event) => openChangeMemberRoleModal(event.detail)} - /> + openUserModal(event.detail)} + on:removeMember={(event) => _deleteOrgUser(org.id, event.detail.id)} + on:changeMemberRole={(event) => openChangeMemberRoleModal(event.detail)} + /> {:else if $queryParamValues.tab === 'history'}
diff --git a/frontend/src/routes/(authenticated)/org/[org_id]/+page.ts b/frontend/src/routes/(authenticated)/org/[org_id]/+page.ts index f71077a46..35653ffc7 100644 --- a/frontend/src/routes/(authenticated)/org/[org_id]/+page.ts +++ b/frontend/src/routes/(authenticated)/org/[org_id]/+page.ts @@ -10,6 +10,7 @@ import type { OrgMemberDto, OrgPageQuery, OrgRole, + RemoveProjectFromOrgMutation, } from '$lib/gql/types'; import { getClient, graphql } from '$lib/gql'; @@ -175,6 +176,30 @@ export async function _bulkAddOrgMembers(orgId: UUID, usernames: string[], role: return result; } +export async function _removeProjectFromOrg(projectId: string, orgId: string): $OpResult { + //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 { //language=GraphQL const result = await getClient() diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte index 98699793a..a32cb57b9 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte +++ b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte @@ -13,6 +13,7 @@ _changeProjectName, _deleteProjectUser, _leaveProject, + _removeProjectFromOrg, type ProjectUser, } from './+page'; import AddProjectMember from './AddProjectMember.svelte'; @@ -107,6 +108,19 @@ } } + let removeProjectFromOrgModal: DeleteModal; + let orgToRemove: string; + async function removeProjectFromOrg(orgId: string, orgName: string): Promise { + 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 { const result = await _changeProjectName({ projectId: project.id, name: newName }); if (result.error) { @@ -391,12 +405,23 @@
- + removeProjectFromOrg(event.detail.orgId, event.detail.orgName)} + > {#if canManage} {/if} + + {$t('project_page.confirm_remove_org', {orgName: orgToRemove})} + {#if members} {/if} -

diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts b/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts index 8f5efda05..4b0e32a74 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts +++ b/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts @@ -16,6 +16,7 @@ import type { LeaveProjectMutation, Organization, ProjectPageQuery, + RemoveProjectFromOrgMutation, SetProjectConfidentialityInput, SetProjectConfidentialityMutation, SetRetentionPolicyInput, @@ -346,6 +347,30 @@ export async function _changeProjectDescription(input: ChangeProjectDescriptionI return result; } +export async function _removeProjectFromOrg(projectId: string, orgId: string): $OpResult { + //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 { //language=GraphQL const result = await getClient() diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/MembersList.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/MembersList.svelte index 3cf3e8232..1cdb0e1a3 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/MembersList.svelte +++ b/frontend/src/routes/(authenticated)/project/[project_code]/MembersList.svelte @@ -138,6 +138,7 @@

{/if} + diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/OrgList.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/OrgList.svelte index bfddbf1b8..0fc6d9928 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/OrgList.svelte +++ b/frontend/src/routes/(authenticated)/project/[project_code]/OrgList.svelte @@ -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; + export let canManage: boolean; export let organizations: Org[] = []; + const dispatch = createEventDispatcher<{ + removeProjectFromOrg: { orgId: string; orgName: string }; + }>(); + const TRUNCATED_MEMBER_COUNT = 5; @@ -23,9 +32,35 @@
{/if} {#each organizations as org (org.id)} - - {org.name} - + {#if !canManage} + + {org.name} + + {:else} + + + + {org.name} + + + + + {/if} {/each} + +