Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove project from org #972

Merged
merged 23 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions frontend/src/lib/components/Projects/ProjectTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import TrashIcon from '$lib/icons/TrashIcon.svelte';
import type { ProjectItemWithDraftStatus } from '$lib/components/Projects';
import Icon from '$lib/icons/Icon.svelte';
import Dropdown from '../Dropdown.svelte';
import { createEventDispatcher } from 'svelte';

export let canManage: boolean = false;
export let projects: ProjectItemWithDraftStatus[];

const allColumns = ['name', 'code', 'users', 'createdAt', 'lastChange', 'type', 'actions'] as const;
Expand All @@ -14,6 +17,10 @@
function isColumnVisible(column: ProjectTableColumn): boolean {
return columns.includes(column);
}

const dispatch = createEventDispatcher<{
removeProjectFromOrg: { projectId: string; projectName: string };
}>();
</script>

<div class="overflow-x-auto @container scroll-shadow">
Expand Down Expand Up @@ -43,6 +50,8 @@
{#if isColumnVisible('type')}
<th>{$t('project.table.type')}</th>
{/if}
<th>
</th>
psh0078 marked this conversation as resolved.
Show resolved Hide resolved
{#if $$slots.actions}
<th />
{/if}
Expand Down Expand Up @@ -118,9 +127,27 @@
</span>
</td>
{/if}
{#if canManage}
<td class="p-0">
<Dropdown>
psh0078 marked this conversation as resolved.
Show resolved Hide resolved
<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={() => dispatch('removeProjectFromOrg', {projectId: project.id, projectName: project.name})}>
<TrashIcon />
{$t('org_page.remove_project_from_org')}
</button>
</li>
</ul>
</Dropdown>
</td>
{/if}
{#if $$slots.actions}
<slot name="actions" {project} />
{/if}
<slot />
</tr>
{/each}
</tbody>
Expand Down
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
13 changes: 11 additions & 2 deletions 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,10 +397,12 @@ 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": "Would you like to remove {userName} from this project?",
"confirm_remove_project": "Would you like to remove {userName} from this project?",
"confirm_remove_org": "Would you like to remove your project from {orgName}?",
"history": "History",
"project_code": "Project Code",
"summary": "Summary",
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
28 changes: 26 additions & 2 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 Down Expand Up @@ -80,6 +81,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 @@ -122,9 +136,19 @@
<div class="py-6 px-2">
{#if $queryParamValues.tab === 'projects'}
<ProjectTable
canManage={canManage}
columns={['name', 'code', 'users', 'type']}
projects={org.projects}
/>
on:removeProjectFromOrg={(event) => removeProjectFromOrg(event.detail.projectId, event.detail.projectName)}
>
<DeleteModal
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
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>
</ProjectTable>
{:else if $queryParamValues.tab === 'members'}
<OrgMemberTable
shownUsers={org.members}
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 @@ -106,6 +107,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 @@ -385,13 +399,22 @@

<div class="space-y-4">
<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>

<MembersList
Expand All @@ -413,8 +436,8 @@
entityName={$t('project_page.remove_project_user_title')}
isRemoveDialog
>
{$t('project_page.confirm_remove', {
userName: userToDelete?.user.name ?? '',
{$t('project_page.confirm_remove_project', {
userName: userToDelete?.user.name ?? ''
})}
</DeleteModal>
</MembersList>
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,
} from '$lib/gql/types';
Expand Down Expand Up @@ -339,6 +340,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>
<mj-button href={`/org/${org.id}`}>
psh0078 marked this conversation as resolved.
Show resolved Hide resolved
<Icon icon="i-mdi-link"/>
{$t('project_page.view_org', {orgName: org.name})}
</mj-button>
</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>