From 8faf88e93bb31bf549343b426970c583556b3f5a Mon Sep 17 00:00:00 2001 From: sdrejkarz <123745148+sdrejkarz@users.noreply.github.com> Date: Thu, 9 May 2024 16:58:34 +0200 Subject: [PATCH] feat: multi tenancy - add tenant removal form (#542) * feat: add a danger zone into General Settings of the organization with removal mutation --------- Co-authored-by: Michal Kleszcz --- .../src/graphql/__generated/gql/gql.ts | 5 + .../src/graphql/__generated/gql/graphql.ts | 8 ++ packages/webapp-libs/webapp-core/package.json | 1 + .../__tests__/alertDialog.component.spec.tsx | 59 ++++++++ .../alertDialog/alertDialog.component.tsx | 9 ++ .../alertDialog/alertDialog.stories.tsx | 45 +++++++ .../alertDialogAction.component.tsx | 14 ++ .../alertDialog/alertDialogAction/index.ts | 1 + .../alertDialogCancel.component.tsx | 18 +++ .../alertDialog/alertDialogCancel/index.ts | 1 + .../alertDialogContent.component.tsx | 25 ++++ .../alertDialog/alertDialogContent/index.ts | 1 + .../alertDialogDescription.component.tsx | 13 ++ .../alertDialogDescription/index.ts | 1 + .../alertDialogFooter.component.tsx | 9 ++ .../alertDialog/alertDialogFooter/index.ts | 1 + .../alertDialogHeader.component.tsx | 9 ++ .../alertDialog/alertDialogHeader/index.ts | 1 + .../alertDialogOverlay.component.tsx | 19 +++ .../alertDialog/alertDialogOverlay/index.ts | 1 + .../alertDialogTitle.component.tsx | 13 ++ .../alertDialog/alertDialogTitle/index.ts | 1 + .../src/components/alertDialog/index.ts | 9 ++ .../webapp-core/src/tests/mocks/icons.tsx | 1 + .../tenantDangerZone.component.spec.tsx | 126 ++++++++++++++++++ .../src/components/tenantDangerZone/index.ts | 1 + .../tenantDangerZone.component.tsx | 49 +++++++ .../tenantDangerZone.graphql.ts | 11 ++ .../tenantDangerZone/tenantDangerZone.hook.ts | 58 ++++++++ .../tenantDangerZone.stories.tsx | 44 ++++++ .../tenantDeleteAlert.component.spec.tsx | 50 +++++++ .../src/components/tenantDeleteAlert/index.ts | 1 + .../tenantDeleteAlert.component.tsx | 61 +++++++++ .../tenantDeleteAlert.stories.tsx | 33 +++++ .../tenantGeneralSettings.component.tsx | 7 +- pnpm-lock.yaml | 29 ++++ 36 files changed, 734 insertions(+), 1 deletion(-) create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/__tests__/alertDialog.component.spec.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.stories.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/alertDialogAction.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/alertDialogCancel.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/alertDialogContent.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/alertDialogDescription.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/alertDialogFooter.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/alertDialogHeader.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/alertDialogOverlay.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/alertDialogTitle.component.tsx create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/index.ts create mode 100644 packages/webapp-libs/webapp-core/src/components/alertDialog/index.ts create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/__tests__/tenantDangerZone.component.spec.tsx create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/index.ts create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.component.tsx create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.graphql.ts create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.hook.ts create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.stories.tsx create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/__tests__/tenantDeleteAlert.component.spec.tsx create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/index.ts create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.component.tsx create mode 100644 packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.stories.tsx diff --git a/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/gql.ts b/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/gql.ts index 51579053b..79b7b4e5b 100644 --- a/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/gql.ts +++ b/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/gql.ts @@ -64,6 +64,7 @@ const documents = { "\n fragment notificationsListContentFragment on Query {\n hasUnreadNotifications\n allNotifications(first: $count, after: $cursor) {\n edges {\n node {\n id\n ...notificationsListItemFragment\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": types.NotificationsListContentFragmentFragmentDoc, "\n fragment notificationsListItemFragment on NotificationType {\n id\n data\n createdAt\n readAt\n type\n issuer {\n id\n avatar\n email\n }\n }\n": types.NotificationsListItemFragmentFragmentDoc, "\n mutation notificationsListMarkAsReadMutation($input: MarkReadAllNotificationsMutationInput!) {\n markReadAllNotifications(input: $input) {\n ok\n }\n }\n": types.NotificationsListMarkAsReadMutationDocument, + "\n mutation deleteTenantMutation($input: DeleteTenantMutationInput!) {\n deleteTenant(input: $input) {\n deletedIds\n clientMutationId\n }\n }\n": types.DeleteTenantMutationDocument, "\n mutation updateTenantMembershipMutation($input: UpdateTenantMembershipMutationInput!) {\n updateTenantMembership(input: $input) {\n tenantMembership {\n id\n }\n }\n }\n": types.UpdateTenantMembershipMutationDocument, "\n mutation deleteTenantMembershipMutation($input: DeleteTenantMembershipMutationInput!) {\n deleteTenantMembership(input: $input) {\n deletedIds\n clientMutationId\n }\n }\n": types.DeleteTenantMembershipMutationDocument, "\n query tenantMembersListQuery($id: ID!) {\n tenant(id: $id) {\n userMemberships {\n id\n role\n invitationAccepted\n inviteeEmailAddress\n userId\n firstName\n lastName\n userEmail\n avatar\n }\n }\n }\n": types.TenantMembersListQueryDocument, @@ -305,6 +306,10 @@ export function gql(source: "\n fragment notificationsListItemFragment on Notif * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql(source: "\n mutation notificationsListMarkAsReadMutation($input: MarkReadAllNotificationsMutationInput!) {\n markReadAllNotifications(input: $input) {\n ok\n }\n }\n"): (typeof documents)["\n mutation notificationsListMarkAsReadMutation($input: MarkReadAllNotificationsMutationInput!) {\n markReadAllNotifications(input: $input) {\n ok\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation deleteTenantMutation($input: DeleteTenantMutationInput!) {\n deleteTenant(input: $input) {\n deletedIds\n clientMutationId\n }\n }\n"): (typeof documents)["\n mutation deleteTenantMutation($input: DeleteTenantMutationInput!) {\n deleteTenant(input: $input) {\n deletedIds\n clientMutationId\n }\n }\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/graphql.ts b/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/graphql.ts index 03d656a95..aa732d1a7 100644 --- a/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/graphql.ts +++ b/packages/webapp-libs/webapp-api-client/src/graphql/__generated/gql/graphql.ts @@ -3250,6 +3250,13 @@ export type NotificationsListMarkAsReadMutationMutationVariables = Exact<{ export type NotificationsListMarkAsReadMutationMutation = { __typename?: 'ApiMutation', markReadAllNotifications?: { __typename?: 'MarkReadAllNotificationsMutationPayload', ok?: boolean | null } | null }; +export type DeleteTenantMutationMutationVariables = Exact<{ + input: DeleteTenantMutationInput; +}>; + + +export type DeleteTenantMutationMutation = { __typename?: 'ApiMutation', deleteTenant?: { __typename?: 'DeleteTenantMutationPayload', deletedIds?: Array | null, clientMutationId?: string | null } | null }; + export type UpdateTenantMembershipMutationMutationVariables = Exact<{ input: UpdateTenantMembershipMutationInput; }>; @@ -3452,6 +3459,7 @@ export const NotificationMutationDocument = {"kind":"Document","definitions":[{" export const NotificationsListQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"notificationsListQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"count"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"notificationsListContentFragment"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"notificationsButtonContent"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"notificationsListItemFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"readAt"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"issuer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"notificationsListContentFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasUnreadNotifications"}},{"kind":"Field","name":{"kind":"Name","value":"allNotifications"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"count"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"notificationsListItemFragment"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"notificationsButtonContent"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasUnreadNotifications"}}]}}]} as unknown as DocumentNode; export const NotificationCreatedSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"NotificationCreatedSubscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notificationCreated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notification"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"notificationsListItemFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"notificationsListItemFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"readAt"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"issuer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]} as unknown as DocumentNode; export const NotificationsListMarkAsReadMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"notificationsListMarkAsReadMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MarkReadAllNotificationsMutationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markReadAllNotifications"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const DeleteTenantMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"deleteTenantMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteTenantMutationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteTenant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletedIds"}},{"kind":"Field","name":{"kind":"Name","value":"clientMutationId"}}]}}]}}]} as unknown as DocumentNode; export const UpdateTenantMembershipMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateTenantMembershipMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateTenantMembershipMutationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateTenantMembership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tenantMembership"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteTenantMembershipMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"deleteTenantMembershipMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteTenantMembershipMutationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteTenantMembership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletedIds"}},{"kind":"Field","name":{"kind":"Name","value":"clientMutationId"}}]}}]}}]} as unknown as DocumentNode; export const TenantMembersListQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"tenantMembersListQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tenant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMemberships"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitationAccepted"}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmailAddress"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/webapp-libs/webapp-core/package.json b/packages/webapp-libs/webapp-core/package.json index eeeaa557e..16f8905ca 100644 --- a/packages/webapp-libs/webapp-core/package.json +++ b/packages/webapp-libs/webapp-core/package.json @@ -9,6 +9,7 @@ "type-check": "tsc --noEmit --project tsconfig.lib.json" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/__tests__/alertDialog.component.spec.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/__tests__/alertDialog.component.spec.tsx new file mode 100644 index 000000000..f67ef2551 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/__tests__/alertDialog.component.spec.tsx @@ -0,0 +1,59 @@ +import { fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '../'; +import { render } from '../../../tests/utils/rendering'; + +const triggerText = 'Open'; +const titleText = 'Title'; +const descriptionText = 'Description'; +const cancelCTA = 'Cancel'; +const actionCTA = 'Continue'; + +const Component = () => ( + + {triggerText} + + + {titleText} + {descriptionText} + + + {cancelCTA} + {actionCTA} + + + +); + +describe('AlertDialog: Component', () => { + it('should render trigger only when not pressed', async () => { + render(); + + expect(await screen.findByText(triggerText)).toBeInTheDocument(); + expect(screen.queryByText(titleText)).not.toBeInTheDocument(); + }); + + it('should render content when pressed', async () => { + render(); + + expect(screen.queryByText(titleText)).not.toBeInTheDocument(); + expect(screen.queryByText(descriptionText)).not.toBeInTheDocument(); + + const button = await screen.findByText(triggerText); + fireEvent.click(button); + + expect(await screen.findByText(titleText)).toBeInTheDocument(); + expect(await screen.findByText(descriptionText)).toBeInTheDocument(); + }); +}); diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.component.tsx new file mode 100644 index 000000000..7150f1f49 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.component.tsx @@ -0,0 +1,9 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +export { AlertDialog, AlertDialogPortal, AlertDialogTrigger }; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.stories.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.stories.tsx new file mode 100644 index 000000000..2ceda8995 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialog.stories.tsx @@ -0,0 +1,45 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from './'; + +type Story = StoryObj; + +const meta: Meta = { + title: 'Core/AlertDialog', + component: AlertDialog, +}; + +export default meta; + +export const Default: Story = { + render: () => ( +
+ + Open + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account and remove your data from our + servers. + + + + Cancel + Continue + + + +
+ ), +}; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/alertDialogAction.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/alertDialogAction.component.tsx new file mode 100644 index 000000000..26b8d1557 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/alertDialogAction.component.tsx @@ -0,0 +1,14 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; +import { buttonVariants } from '../../buttons/button/button.styles'; + +export const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/index.ts new file mode 100644 index 000000000..e01d72107 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogAction/index.ts @@ -0,0 +1 @@ +export * from './alertDialogAction.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/alertDialogCancel.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/alertDialogCancel.component.tsx new file mode 100644 index 000000000..bc125dade --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/alertDialogCancel.component.tsx @@ -0,0 +1,18 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; +import { buttonVariants } from '../../buttons/button/button.styles'; + +export const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/index.ts new file mode 100644 index 000000000..a9e5f6718 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogCancel/index.ts @@ -0,0 +1 @@ +export * from './alertDialogCancel.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/alertDialogContent.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/alertDialogContent.component.tsx new file mode 100644 index 000000000..eef46f962 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/alertDialogContent.component.tsx @@ -0,0 +1,25 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; +import { AlertDialogPortal } from '../alertDialog.component'; +import { AlertDialogOverlay } from '../alertDialogOverlay'; + +export const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); + +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/index.ts new file mode 100644 index 000000000..752329301 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogContent/index.ts @@ -0,0 +1 @@ +export * from './alertDialogContent.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/alertDialogDescription.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/alertDialogDescription.component.tsx new file mode 100644 index 000000000..030e68d2b --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/alertDialogDescription.component.tsx @@ -0,0 +1,13 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; + +export const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/index.ts new file mode 100644 index 000000000..f1a3242b0 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogDescription/index.ts @@ -0,0 +1 @@ +export * from './alertDialogDescription.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/alertDialogFooter.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/alertDialogFooter.component.tsx new file mode 100644 index 000000000..4cb7b5851 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/alertDialogFooter.component.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; + +export const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); + +AlertDialogFooter.displayName = 'AlertDialogFooter'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/index.ts new file mode 100644 index 000000000..7bfbae3d0 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogFooter/index.ts @@ -0,0 +1 @@ +export * from './alertDialogFooter.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/alertDialogHeader.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/alertDialogHeader.component.tsx new file mode 100644 index 000000000..2a8324ac0 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/alertDialogHeader.component.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; + +export const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); + +AlertDialogHeader.displayName = 'AlertDialogHeader'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/index.ts new file mode 100644 index 000000000..5db64c93f --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogHeader/index.ts @@ -0,0 +1 @@ +export * from './alertDialogHeader.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/alertDialogOverlay.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/alertDialogOverlay.component.tsx new file mode 100644 index 000000000..ae936db56 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/alertDialogOverlay.component.tsx @@ -0,0 +1,19 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; + +export const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/index.ts new file mode 100644 index 000000000..704e38f32 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogOverlay/index.ts @@ -0,0 +1 @@ +export * from './alertDialogOverlay.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/alertDialogTitle.component.tsx b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/alertDialogTitle.component.tsx new file mode 100644 index 000000000..83a72d5da --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/alertDialogTitle.component.tsx @@ -0,0 +1,13 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import * as React from 'react'; + +import { cn } from '../../../lib/utils'; + +export const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/index.ts new file mode 100644 index 000000000..9485c8ee8 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/alertDialogTitle/index.ts @@ -0,0 +1 @@ +export * from './alertDialogTitle.component'; diff --git a/packages/webapp-libs/webapp-core/src/components/alertDialog/index.ts b/packages/webapp-libs/webapp-core/src/components/alertDialog/index.ts new file mode 100644 index 000000000..8e047e9b2 --- /dev/null +++ b/packages/webapp-libs/webapp-core/src/components/alertDialog/index.ts @@ -0,0 +1,9 @@ +export * from './alertDialog.component'; +export * from './alertDialogAction'; +export * from './alertDialogCancel'; +export * from './alertDialogContent'; +export * from './alertDialogDescription'; +export * from './alertDialogFooter'; +export * from './alertDialogHeader'; +export * from './alertDialogOverlay'; +export * from './alertDialogTitle'; diff --git a/packages/webapp-libs/webapp-core/src/tests/mocks/icons.tsx b/packages/webapp-libs/webapp-core/src/tests/mocks/icons.tsx index 39ab752b1..1376b8b5d 100644 --- a/packages/webapp-libs/webapp-core/src/tests/mocks/icons.tsx +++ b/packages/webapp-libs/webapp-core/src/tests/mocks/icons.tsx @@ -9,6 +9,7 @@ jest.mock('@iconify-icons/ion/star', () => IconMock); jest.mock('@iconify-icons/ion/document-text-outline', () => IconMock); jest.mock('@iconify-icons/ion/star-outline', () => IconMock); jest.mock('@iconify-icons/ion/camera-outline', () => IconMock); +jest.mock('@iconify-icons/ion/alert-circle-outline', () => IconMock); jest.mock('@iconify-icons/ion/mail-outline', () => IconMock); jest.mock('@iconify-icons/ion/mail-unread-outline', () => IconMock); jest.mock('@iconify-icons/ion/mail-open-outline', () => IconMock); diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/__tests__/tenantDangerZone.component.spec.tsx b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/__tests__/tenantDangerZone.component.spec.tsx new file mode 100644 index 000000000..6faf198e3 --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/__tests__/tenantDangerZone.component.spec.tsx @@ -0,0 +1,126 @@ +import { TenantUserRole } from '@sb/webapp-api-client'; +import { commonQueryCurrentUserQuery } from '@sb/webapp-api-client/providers'; +import { currentUserFactory, fillCommonQueryWithUser } from '@sb/webapp-api-client/tests/factories'; +import { composeMockedQueryResult } from '@sb/webapp-api-client/tests/utils'; +import { screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { RoutesConfig } from '../../../config/routes'; +import { membershipFactory, tenantFactory } from '../../../tests/factories/tenant'; +import { createMockRouterProps, render } from '../../../tests/utils/rendering'; +import { TenantDangerZone } from '../tenantDangerZone.component'; +import { deleteTenantMutation } from '../tenantDangerZone.graphql'; + +describe('TenantDangerSettings: Component', () => { + const Component = () => ; + + it('should render title', async () => { + const user = currentUserFactory({ + tenants: [ + tenantFactory({ + name: 'name', + id: '1', + }), + ], + }); + const commonQueryMock = fillCommonQueryWithUser(user); + const routerProps = createMockRouterProps(RoutesConfig.tenant.settings.general, { tenantId: '1' }); + + render(, { apolloMocks: [commonQueryMock], routerProps }); + + expect(await screen.findByText('Danger Zone')).toBeInTheDocument(); + }); + + it('should render delete organization', async () => { + const user = currentUserFactory({ + tenants: [ + tenantFactory({ + name: 'name', + id: '1', + }), + ], + }); + const commonQueryMock = fillCommonQueryWithUser(user); + const routerProps = createMockRouterProps(RoutesConfig.tenant.settings.general, { tenantId: '1' }); + + render(, { apolloMocks: [commonQueryMock], routerProps }); + + expect(await screen.findByText('Delete this organization')).toBeInTheDocument(); + expect(screen.getByText('Remove organisation')).toBeInTheDocument(); + }); + + it('should render delete organization subtitle about permissions', async () => { + const user = currentUserFactory({ + tenants: [ + tenantFactory({ + name: 'name', + id: '1', + membership: membershipFactory({ role: TenantUserRole.MEMBER }), + }), + ], + }); + const commonQueryMock = fillCommonQueryWithUser(user); + const routerProps = createMockRouterProps(RoutesConfig.tenant.settings.general, { tenantId: '1' }); + + render(, { apolloMocks: [commonQueryMock], routerProps }); + + const button = await screen.findByRole('button', { name: /remove organisation/i }); + expect(button).toBeInTheDocument(); + expect(screen.getByText('Only members with the Owner role can delete organization')).toBeInTheDocument(); + }); + + it('should delete organization', async () => { + const user = currentUserFactory({ + tenants: [ + tenantFactory({ + name: 'name', + id: '1', + membership: membershipFactory({ role: TenantUserRole.OWNER }), + }), + ], + }); + const commonQueryMock = fillCommonQueryWithUser(user); + + const variables = { + input: { id: '1' }, + }; + const data = { + deleteTenant: { + deletedIds: ['1'], + // clientMutationId prevents errors in the console + clientMutationId: '123', + }, + }; + const requestMock = composeMockedQueryResult(deleteTenantMutation, { + variables, + data, + }); + const currentUserRefetchData = { + ...user, + tenants: [], + }; + const refetchMock = composeMockedQueryResult(commonQueryCurrentUserQuery, { + data: currentUserRefetchData, + }); + requestMock.newData = jest.fn(() => ({ + data, + })); + refetchMock.newData = jest.fn(() => ({ + data: { + currentUser: currentUserRefetchData, + }, + })); + const routerProps = createMockRouterProps(RoutesConfig.tenant.settings.general, { tenantId: '1' }); + render(, { apolloMocks: [commonQueryMock, requestMock, refetchMock], routerProps }); + + const button = await screen.findByRole('button', { name: /remove organisation/i }); + await userEvent.click(button); + + const continueButton = await screen.findByRole('button', { name: /continue/i }); + await userEvent.click(continueButton); + + expect(requestMock.newData).toHaveBeenCalled(); + const toast = await screen.findByTestId('toast-1'); + expect(toast).toHaveTextContent('🎉 Tenant deleted successfully!'); + }); +}); diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/index.ts b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/index.ts new file mode 100644 index 000000000..23f4c27c0 --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/index.ts @@ -0,0 +1 @@ +export { TenantDangerZone } from './tenantDangerZone.component'; diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.component.tsx b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.component.tsx new file mode 100644 index 000000000..9e71b0a7c --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.component.tsx @@ -0,0 +1,49 @@ +import dangerIcon from '@iconify-icons/ion/alert-circle-outline'; +import { TenantUserRole } from '@sb/webapp-api-client'; +import { Icon } from '@sb/webapp-core/components/icons'; +import { H3, Paragraph } from '@sb/webapp-core/components/typography'; +import { FormattedMessage } from 'react-intl'; + +import { useCurrentTenant } from '../../providers'; +import { TenantDeleteAlert } from '../tenantDeleteAlert'; +import { useTenantDelete } from './tenantDangerZone.hook'; + +export const TenantDangerZone = () => { + const { data: currentTenant } = useCurrentTenant(); + const isOwner = currentTenant?.membership.role === TenantUserRole.OWNER; + + const { deleteTenant, loading } = useTenantDelete(); + + return ( +
+
+ +

+ +

+
+ +
+
+ + + + + {isOwner ? undefined : ( + + )} + + + + +
+
+
+ ); +}; diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.graphql.ts b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.graphql.ts new file mode 100644 index 000000000..f887f72c1 --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.graphql.ts @@ -0,0 +1,11 @@ +import { gql } from '@sb/webapp-api-client/graphql'; + + +export const deleteTenantMutation = gql(/* GraphQL */ ` + mutation deleteTenantMutation($input: DeleteTenantMutationInput!) { + deleteTenant(input: $input) { + deletedIds + clientMutationId + } + } +`); \ No newline at end of file diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.hook.ts b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.hook.ts new file mode 100644 index 000000000..171a45c80 --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.hook.ts @@ -0,0 +1,58 @@ +import { useMutation } from '@apollo/client'; +import { useCommonQuery } from '@sb/webapp-api-client/providers'; +import { RoutesConfig } from '@sb/webapp-core/config/routes'; +import { useGenerateLocalePath } from '@sb/webapp-core/hooks'; +import { trackEvent } from '@sb/webapp-core/services/analytics'; +import { useToast } from '@sb/webapp-core/toast'; +import { useIntl } from 'react-intl'; +import { useNavigate } from 'react-router'; + +import { useCurrentTenant } from '../../providers'; +import { deleteTenantMutation } from './tenantDangerZone.graphql'; + +export const useTenantDelete = () => { + const { data: currentTenant } = useCurrentTenant(); + const { reload: reloadCommonQuery } = useCommonQuery(); + const navigate = useNavigate(); + const { toast } = useToast(); + const intl = useIntl(); + + const generateLocalePath = useGenerateLocalePath(); + + const successDeleteMessage = intl.formatMessage({ + id: 'Tenant form / DeleteTenant / Success message', + defaultMessage: '🎉 Tenant deleted successfully!', + }); + + const failDeleteMessage = intl.formatMessage({ + id: 'Membership Entry / DeleteTenant / Fail message', + defaultMessage: 'Unable to delete the tenant.', + }); + + const [commitRemoveMutation, { loading }] = useMutation(deleteTenantMutation, { + onCompleted: (data) => { + const id = data.deleteTenant?.deletedIds?.[0]?.toString(); + reloadCommonQuery(); + trackEvent('tenant', 'delete', id); + toast({ description: successDeleteMessage }); + navigate(generateLocalePath(RoutesConfig.home), { replace: true }); + }, + onError: () => { + toast({ description: failDeleteMessage, variant: 'destructive' }); + }, + }); + + const deleteTenant = () => { + if (!currentTenant) return; + + commitRemoveMutation({ + variables: { + input: { + id: currentTenant.id, + }, + }, + }); + }; + + return { deleteTenant, loading }; +}; diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.stories.tsx b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.stories.tsx new file mode 100644 index 000000000..d1984dd1d --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDangerZone/tenantDangerZone.stories.tsx @@ -0,0 +1,44 @@ +import { TenantUserRole } from '@sb/webapp-api-client'; +import { currentUserFactory, fillCommonQueryWithUser } from '@sb/webapp-api-client/tests/factories'; +import { Meta, StoryFn, StoryObj } from '@storybook/react'; + +import { membershipFactory, tenantFactory } from '../../tests/factories/tenant'; +import { withProviders } from '../../utils/storybook'; +import { TenantDangerZone } from './tenantDangerZone.component'; + +const Template: StoryFn = () => { + return ; +}; + +const meta: Meta = { + title: 'Tenants / TenantDangerZone', + component: Template, +}; + +export default meta; + +export const Default: StoryObj = { + render: Template, + decorators: [withProviders({})], +}; + +const ownerUserResponse = fillCommonQueryWithUser( + currentUserFactory({ + tenants: [ + tenantFactory({ + name: 'name', + id: '1', + membership: membershipFactory({ role: TenantUserRole.OWNER }), + }), + ], + }) +); + +export const OwnerView: StoryObj = { + render: Template, + decorators: [ + withProviders({ + apolloMocks: [ownerUserResponse], + }), + ], +}; diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/__tests__/tenantDeleteAlert.component.spec.tsx b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/__tests__/tenantDeleteAlert.component.spec.tsx new file mode 100644 index 000000000..eef81ec16 --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/__tests__/tenantDeleteAlert.component.spec.tsx @@ -0,0 +1,50 @@ +import { screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { render } from '../../../tests/utils/rendering'; +import { TenantDeleteAlert, TenantDeleteAlertProps } from '../tenantDeleteAlert.component'; + +const defaultProps: TenantDeleteAlertProps = { + disabled: false, + onContinue: jest.fn(), +}; + +describe('TenantDeleteAlert: Component', () => { + const Component = (args: Partial) => ; + + it('should render alert when button is clicked', async () => { + render(); + + const button = await screen.findByRole('button', { name: /remove organisation/i }); + await userEvent.click(button); + + expect(await screen.findByText('Are you absolutely sure?')).toBeInTheDocument(); + }); + + it('should call onContinue', async () => { + const onContinueMock = jest.fn(); + render(); + + const button = await screen.findByRole('button', { name: /remove organisation/i }); + await userEvent.click(button); + + const continueButton = await screen.findByRole('button', { name: /continue/i }); + await userEvent.click(continueButton); + + expect(onContinueMock).toHaveBeenCalled(); + expect(screen.queryByText('Are you absolutely sure?')).not.toBeInTheDocument(); + }); + + it('should hide Alert on Cancel click', async () => { + const onContinueMock = jest.fn(); + render(); + + const button = await screen.findByRole('button', { name: /remove organisation/i }); + await userEvent.click(button); + + const continueButton = await screen.findByRole('button', { name: /cancel/i }); + await userEvent.click(continueButton); + + expect(screen.queryByText('Are you absolutely sure?')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/index.ts b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/index.ts new file mode 100644 index 000000000..55cf60e63 --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/index.ts @@ -0,0 +1 @@ +export { TenantDeleteAlert } from './tenantDeleteAlert.component'; diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.component.tsx b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.component.tsx new file mode 100644 index 000000000..9c54e3a6e --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.component.tsx @@ -0,0 +1,61 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@sb/webapp-core/components/alertDialog'; +import { buttonVariants } from '@sb/webapp-core/components/buttons/button/button.styles'; +import { FormattedMessage } from 'react-intl'; + +export type TenantDeleteAlertProps = { + onContinue: () => void; + disabled: boolean; +}; + +export const TenantDeleteAlert = ({ onContinue, disabled }: TenantDeleteAlertProps) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.stories.tsx b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.stories.tsx new file mode 100644 index 000000000..62b2963ec --- /dev/null +++ b/packages/webapp-libs/webapp-tenants/src/components/tenantDeleteAlert/tenantDeleteAlert.stories.tsx @@ -0,0 +1,33 @@ +import { action } from '@storybook/addon-actions'; +import { Meta, StoryFn, StoryObj } from '@storybook/react'; + +import { withProviders } from '../../utils/storybook'; +import { TenantDeleteAlert, TenantDeleteAlertProps } from './tenantDeleteAlert.component'; + +const defaultProps: TenantDeleteAlertProps = { + disabled: false, + onContinue: action('TenantDelete mutation'), +}; + +const Template: StoryFn = (args: TenantDeleteAlertProps) => { + return ; +}; + +const meta: Meta = { + title: 'Tenants / TenantDeleteAlert', + component: Template, +}; + +export default meta; + +export const Enabled: StoryObj = { + decorators: [withProviders({})], +}; + +export const Disabled: StoryObj = { + args: { + ...defaultProps, + disabled: true, + }, + decorators: [withProviders({})], +}; diff --git a/packages/webapp-libs/webapp-tenants/src/routes/tenantSettings/tenantGeneralSettings/tenantGeneralSettings.component.tsx b/packages/webapp-libs/webapp-tenants/src/routes/tenantSettings/tenantGeneralSettings/tenantGeneralSettings.component.tsx index 6f380ac37..2d04ec3c6 100644 --- a/packages/webapp-libs/webapp-tenants/src/routes/tenantSettings/tenantGeneralSettings/tenantGeneralSettings.component.tsx +++ b/packages/webapp-libs/webapp-tenants/src/routes/tenantSettings/tenantGeneralSettings/tenantGeneralSettings.component.tsx @@ -1,10 +1,12 @@ import { useMutation } from '@apollo/client'; +import { TenantType } from '@sb/webapp-api-client/constants'; import { useCommonQuery } from '@sb/webapp-api-client/providers'; import { PageHeadline } from '@sb/webapp-core/components/pageHeadline'; import { trackEvent } from '@sb/webapp-core/services/analytics'; import { useToast } from '@sb/webapp-core/toast'; import { FormattedMessage, useIntl } from 'react-intl'; +import { TenantDangerZone } from '../../../components/tenantDangerZone'; import { TenantForm } from '../../../components/tenantForm'; import { TenantFormFields } from '../../../components/tenantForm/tenantForm.component'; import { useCurrentTenant } from '../../../providers'; @@ -13,7 +15,6 @@ import { updateTenantMutation } from './tenantGeneralSettings.graphql'; export const TenantGeneralSettings = () => { const { data: currentTenant } = useCurrentTenant(); const { reload: reloadCommonQuery } = useCommonQuery(); - const { toast } = useToast(); const intl = useIntl(); @@ -27,6 +28,8 @@ export const TenantGeneralSettings = () => { defaultMessage: 'Unable to change the tenant data.', }); + const isOrganizationType = currentTenant?.type === TenantType.ORGANIZATION; + const [commitUpdateMutation, { loading, error }] = useMutation(updateTenantMutation, { onCompleted: (data) => { const id = data.updateTenant?.tenant?.id; @@ -69,6 +72,8 @@ export const TenantGeneralSettings = () => { onSubmit={onFormSubmit} initialData={currentTenant?.name ? { name: currentTenant.name } : undefined} /> + + {isOrganizationType && }
); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 987e76664..241b699c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -798,6 +798,9 @@ importers: packages/webapp-libs/webapp-core: dependencies: + '@radix-ui/react-alert-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.2.9)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.9)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) @@ -9907,6 +9910,32 @@ packages: dependencies: '@babel/runtime': 7.23.5 + /@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.9)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.5 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.9)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.9)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.24)(react@18.2.0) + '@types/react': 18.2.24 + '@types/react-dom': 18.2.9 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.9)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: