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} + +