Add end-to-end tests for user and team billing features
This commit introduces end-to-end tests for the user and team billing features. It also enhances existing billing configurations, logging, and error handling mechanisms. Refactoring has been done to simplify the code and make it more readable. Adjustments have also been made in the visual aspects of some components. The addition of these tests will help ensure the reliability of the billing features.
This commit is contained in:
5
.github/workflows/workflow.yml
vendored
5
.github/workflows/workflow.yml
vendored
@@ -32,11 +32,16 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Production Build (test env)
|
||||
run: pnpm --filter web build:test
|
||||
|
||||
test:
|
||||
name: ⚫️ Test
|
||||
timeout-minutes: 8
|
||||
|
||||
@@ -37,6 +37,12 @@ export class TeamAccountsPageObject {
|
||||
}).click();
|
||||
}
|
||||
|
||||
async goToBilling() {
|
||||
await this.page.locator('a', {
|
||||
hasText: 'Billing',
|
||||
}).click();
|
||||
}
|
||||
|
||||
async openAccountsSelector() {
|
||||
await this.page.click('[data-test="account-selector-trigger"]');
|
||||
}
|
||||
|
||||
18
apps/e2e/tests/team-billing/team-billing.po.ts
Normal file
18
apps/e2e/tests/team-billing/team-billing.po.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { StripePageObject } from '../utils/stripe.po';
|
||||
import { TeamAccountsPageObject } from '../team-accounts/team-accounts.po';
|
||||
|
||||
export class TeamBillingPageObject {
|
||||
private readonly teamAccounts: TeamAccountsPageObject;
|
||||
public readonly stripe: StripePageObject;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.teamAccounts = new TeamAccountsPageObject(page);
|
||||
this.stripe = new StripePageObject(page);
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.teamAccounts.setup();
|
||||
await this.teamAccounts.goToBilling();
|
||||
}
|
||||
}
|
||||
24
apps/e2e/tests/team-billing/team-billing.spec.ts
Normal file
24
apps/e2e/tests/team-billing/team-billing.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect, Page, test } from '@playwright/test';
|
||||
import { TeamBillingPageObject } from './team-billing.po';
|
||||
|
||||
test.describe('Team Billing', () => {
|
||||
let page: Page;
|
||||
let billing: TeamBillingPageObject;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
billing = new TeamBillingPageObject(page);
|
||||
|
||||
await billing.setup();
|
||||
});
|
||||
|
||||
test('a team can subscribe to a plan', async () => {
|
||||
await billing.stripe.selectPlan(0);
|
||||
await billing.stripe.proceedToCheckout();
|
||||
|
||||
await billing.stripe.fillForm();
|
||||
await billing.stripe.submitForm();
|
||||
|
||||
await expect(billing.stripe.successStatus()).toBeVisible();
|
||||
});
|
||||
});
|
||||
17
apps/e2e/tests/user-billing/user-billing.po.ts
Normal file
17
apps/e2e/tests/user-billing/user-billing.po.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { AuthPageObject } from '../authentication/auth.po';
|
||||
import { StripePageObject } from '../utils/stripe.po';
|
||||
|
||||
export class UserBillingPageObject {
|
||||
private readonly auth: AuthPageObject;
|
||||
public readonly stripe: StripePageObject;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.auth = new AuthPageObject(page);
|
||||
this.stripe = new StripePageObject(page);
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.auth.signUpFlow('/home/billing');
|
||||
}
|
||||
}
|
||||
24
apps/e2e/tests/user-billing/user-billing.spec.ts
Normal file
24
apps/e2e/tests/user-billing/user-billing.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect, Page, test } from '@playwright/test';
|
||||
import { UserBillingPageObject } from './user-billing.po';
|
||||
|
||||
test.describe('User Billing', () => {
|
||||
let page: Page;
|
||||
let billing: UserBillingPageObject;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
billing = new UserBillingPageObject(page);
|
||||
|
||||
await billing.setup();
|
||||
});
|
||||
|
||||
test('user can subscribe to a plan', async () => {
|
||||
await billing.stripe.selectPlan(0);
|
||||
await billing.stripe.proceedToCheckout();
|
||||
|
||||
await billing.stripe.fillForm();
|
||||
await billing.stripe.submitForm();
|
||||
|
||||
await expect(billing.stripe.successStatus()).toBeVisible();
|
||||
});
|
||||
});
|
||||
87
apps/e2e/tests/utils/stripe.po.ts
Normal file
87
apps/e2e/tests/utils/stripe.po.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class StripePageObject {
|
||||
private page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
plans() {
|
||||
return this.page.locator('[data-test-plan]');
|
||||
}
|
||||
|
||||
getStripeCheckoutIframe() {
|
||||
return this.page.frameLocator('[name="embedded-checkout"]');
|
||||
}
|
||||
|
||||
async fillForm(params: {
|
||||
billingName?: string;
|
||||
cardNumber?: string;
|
||||
expiry?: string;
|
||||
cvc?: string;
|
||||
billingCountry?: string;
|
||||
} = {}) {
|
||||
expect(() => {
|
||||
return this.getStripeCheckoutIframe().locator('form').isVisible();
|
||||
});
|
||||
|
||||
const billingName = this.billingName();
|
||||
const cardNumber = this.cardNumber();
|
||||
const expiry = this.expiry();
|
||||
const cvc = this.cvc();
|
||||
const billingCountry = this.billingCountry();
|
||||
|
||||
await billingName.fill(params.billingName ?? 'Mr Makerkit');
|
||||
await cardNumber.fill(params.cardNumber ?? '4242424242424242');
|
||||
await expiry.fill(params.expiry ?? '1228');
|
||||
await cvc.fill(params.cvc ?? '123');
|
||||
await billingCountry.selectOption(params.billingCountry ?? 'IT');
|
||||
}
|
||||
|
||||
submitForm() {
|
||||
return this.getStripeCheckoutIframe().locator('form button').click();
|
||||
}
|
||||
|
||||
cardNumber() {
|
||||
return this.getStripeCheckoutIframe().locator('#cardNumber');
|
||||
}
|
||||
|
||||
cvc() {
|
||||
return this.getStripeCheckoutIframe().locator('#cardCvc');
|
||||
}
|
||||
|
||||
expiry() {
|
||||
return this.getStripeCheckoutIframe().locator('#cardExpiry');
|
||||
}
|
||||
|
||||
billingName() {
|
||||
return this.getStripeCheckoutIframe().locator('#billingName');
|
||||
}
|
||||
|
||||
cardForm() {
|
||||
return this.getStripeCheckoutIframe().locator('form');
|
||||
}
|
||||
|
||||
billingCountry() {
|
||||
return this.getStripeCheckoutIframe().locator('#billingCountry');
|
||||
}
|
||||
|
||||
selectPlan(index: number = 0) {
|
||||
const plans = this.plans();
|
||||
|
||||
return plans.nth(index).click();
|
||||
}
|
||||
|
||||
manageBillingButton() {
|
||||
return this.page.locator('manage-billing-redirect-button');
|
||||
}
|
||||
|
||||
successStatus() {
|
||||
return this.page.locator('[data-test="payment-return-success"]');
|
||||
}
|
||||
|
||||
proceedToCheckout() {
|
||||
return this.page.click('[data-test="checkout-submit-button"]');
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,8 @@ export function SitePageHeader(props: {
|
||||
<div className={'container flex flex-col space-y-4'}>
|
||||
<h1 className={'font-base text-3xl xl:text-5xl'}>{props.title}</h1>
|
||||
|
||||
<h2
|
||||
className={
|
||||
'text-base text-secondary-foreground xl:text-lg 2xl:text-xl'
|
||||
}
|
||||
>
|
||||
<span className={'font-normal'}>{props.subtitle}</span>
|
||||
<h2 className={'text-muted-foreground xl:text-lg 2xl:text-xl'}>
|
||||
{props.subtitle}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import { Cms } from '@kit/cms';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { CoverImage } from '~/(marketing)/blog/_components/cover-image';
|
||||
@@ -17,25 +11,27 @@ export const PostHeader: React.FC<{
|
||||
const { title, publishedAt, description, image } = post;
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<div className={'flex flex-1 flex-col'}>
|
||||
<div className={cn('border-b py-8')}>
|
||||
<div className={'container flex flex-col space-y-4'}>
|
||||
<div className={'mx-auto flex max-w-3xl flex-col space-y-4'}>
|
||||
<h1 className={'text-3xl font-semibold xl:text-5xl'}>{title}</h1>
|
||||
|
||||
<h2 className={'text-base text-secondary-foreground xl:text-lg'}>
|
||||
<span
|
||||
className={'font-normal'}
|
||||
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
||||
/>
|
||||
</h2>
|
||||
<div>
|
||||
<span className={'text-muted-foreground'}>
|
||||
<DateFormatter dateString={publishedAt.toISOString()} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DateFormatter dateString={publishedAt.toISOString()} />
|
||||
<h2
|
||||
className={'text-base text-muted-foreground xl:text-lg'}
|
||||
dangerouslySetInnerHTML={{ __html: description ?? '' }}
|
||||
></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<If condition={image}>
|
||||
{(imageUrl) => (
|
||||
<div className="relative mx-auto mt-8 flex h-[378px] w-full max-w-2xl justify-center">
|
||||
<div className="relative mx-auto mt-8 flex h-[378px] w-full max-w-3xl justify-center">
|
||||
<CoverImage
|
||||
preloadImage
|
||||
className="rounded-md"
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Post: React.FC<{
|
||||
<div>
|
||||
<PostHeader post={post} />
|
||||
|
||||
<div className={'mx-auto flex max-w-2xl flex-col space-y-6 py-8'}>
|
||||
<div className={'mx-auto flex max-w-3xl flex-col space-y-6 py-8'}>
|
||||
<article className={styles.HTML}>
|
||||
<ContentRenderer content={content} />
|
||||
</article>
|
||||
|
||||
@@ -39,11 +39,13 @@ export async function POST(request: Request) {
|
||||
);
|
||||
|
||||
return new Response('OK', { status: 200 });
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
logger.error(
|
||||
{
|
||||
name: 'billing',
|
||||
error: e,
|
||||
error: JSON.stringify(error),
|
||||
},
|
||||
`Failed to process billing webhook`,
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ export default createBillingSchema({
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: '324646',
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
|
||||
name: 'Addon 2',
|
||||
cost: 9.99,
|
||||
type: 'flat',
|
||||
@@ -45,7 +45,7 @@ export default createBillingSchema({
|
||||
interval: 'year',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe1',
|
||||
id: 'starter-yearly',
|
||||
name: 'Base',
|
||||
cost: 99.99,
|
||||
type: 'flat',
|
||||
@@ -70,7 +70,7 @@ export default createBillingSchema({
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe2',
|
||||
id: 'price_pro',
|
||||
name: 'Base',
|
||||
cost: 19.99,
|
||||
type: 'flat',
|
||||
@@ -84,7 +84,7 @@ export default createBillingSchema({
|
||||
interval: 'year',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe3',
|
||||
id: 'price_pro_yearly',
|
||||
name: 'Base',
|
||||
cost: 199.99,
|
||||
type: 'flat',
|
||||
@@ -113,7 +113,7 @@ export default createBillingSchema({
|
||||
interval: 'month',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe4',
|
||||
id: 'price_enterprise-monthly',
|
||||
name: 'Base',
|
||||
cost: 29.99,
|
||||
type: 'flat',
|
||||
@@ -127,7 +127,7 @@ export default createBillingSchema({
|
||||
interval: 'year',
|
||||
lineItems: [
|
||||
{
|
||||
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe5',
|
||||
id: 'price_enterprise_yearly',
|
||||
name: 'Base',
|
||||
cost: 299.99,
|
||||
type: 'flat',
|
||||
|
||||
@@ -356,7 +356,7 @@ export function getLineItemTypeById(
|
||||
for (const product of config.products) {
|
||||
for (const plan of product.plans) {
|
||||
for (const lineItem of plan.lineItems) {
|
||||
if (lineItem.type === id) {
|
||||
if (lineItem.id === id) {
|
||||
return lineItem.type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +236,7 @@ export function PlanPicker(
|
||||
key={primaryLineItem.id}
|
||||
>
|
||||
<RadioGroupItem
|
||||
data-test-plan={plan.id}
|
||||
key={plan.id + selected}
|
||||
id={plan.id}
|
||||
value={plan.id}
|
||||
@@ -346,7 +347,10 @@ export function PlanPicker(
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button disabled={props.pending ?? !form.formState.isValid}>
|
||||
<Button
|
||||
data-test="checkout-submit-button"
|
||||
disabled={props.pending ?? !form.formState.isValid}
|
||||
>
|
||||
{props.pending ? (
|
||||
t('processing')
|
||||
) : (
|
||||
|
||||
@@ -26,15 +26,14 @@ export class BillingEventHandlerService {
|
||||
const client = this.clientProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
namespace: this.namespace,
|
||||
subscriptionId,
|
||||
};
|
||||
|
||||
// Handle the subscription deleted event
|
||||
// here we delete the subscription from the database
|
||||
logger.info(
|
||||
{
|
||||
namespace: this.namespace,
|
||||
subscriptionId,
|
||||
},
|
||||
'Processing subscription deleted event',
|
||||
);
|
||||
logger.info(ctx, 'Processing subscription deleted event');
|
||||
|
||||
const { error } = await client
|
||||
.from('subscriptions')
|
||||
@@ -42,16 +41,18 @@ export class BillingEventHandlerService {
|
||||
.match({ id: subscriptionId });
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
{
|
||||
error,
|
||||
...ctx,
|
||||
},
|
||||
`Failed to delete subscription`,
|
||||
);
|
||||
|
||||
throw new Error('Failed to delete subscription');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
namespace: this.namespace,
|
||||
subscriptionId,
|
||||
},
|
||||
'Successfully deleted subscription',
|
||||
);
|
||||
logger.info(ctx, 'Successfully deleted subscription');
|
||||
},
|
||||
onSubscriptionUpdated: async (subscription) => {
|
||||
const client = this.clientProvider();
|
||||
@@ -65,7 +66,7 @@ export class BillingEventHandlerService {
|
||||
customerId: subscription.target_customer_id,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Processing subscription updated event');
|
||||
logger.info(ctx, 'Processing subscription updated event ...');
|
||||
|
||||
// Handle the subscription updated event
|
||||
// here we update the subscription in the database
|
||||
@@ -139,15 +140,14 @@ export class BillingEventHandlerService {
|
||||
const client = this.clientProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
namespace: this.namespace,
|
||||
sessionId,
|
||||
};
|
||||
|
||||
// Handle the payment succeeded event
|
||||
// here we update the payment status in the database
|
||||
logger.info(
|
||||
{
|
||||
namespace: this.namespace,
|
||||
sessionId,
|
||||
},
|
||||
'Processing payment succeeded event',
|
||||
);
|
||||
logger.info(ctx, 'Processing payment succeeded event...');
|
||||
|
||||
const { error } = await client
|
||||
.from('orders')
|
||||
@@ -155,30 +155,31 @@ export class BillingEventHandlerService {
|
||||
.match({ session_id: sessionId });
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
{
|
||||
error,
|
||||
...ctx,
|
||||
},
|
||||
'Failed to update payment status',
|
||||
);
|
||||
|
||||
throw new Error('Failed to update payment status');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
namespace: this.namespace,
|
||||
sessionId,
|
||||
},
|
||||
'Successfully updated payment status',
|
||||
);
|
||||
logger.info(ctx, 'Successfully updated payment status');
|
||||
},
|
||||
onPaymentFailed: async (sessionId: string) => {
|
||||
const client = this.clientProvider();
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
namespace: this.namespace,
|
||||
sessionId,
|
||||
};
|
||||
|
||||
// Handle the payment failed event
|
||||
// here we update the payment status in the database
|
||||
logger.info(
|
||||
{
|
||||
namespace: this.namespace,
|
||||
sessionId,
|
||||
},
|
||||
'Processing payment failed event',
|
||||
);
|
||||
logger.info(ctx, 'Processing payment failed event');
|
||||
|
||||
const { error } = await client
|
||||
.from('orders')
|
||||
@@ -186,16 +187,18 @@ export class BillingEventHandlerService {
|
||||
.match({ session_id: sessionId });
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
{
|
||||
error,
|
||||
...ctx,
|
||||
},
|
||||
'Failed to update payment status',
|
||||
);
|
||||
|
||||
throw new Error('Failed to update payment status');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
namespace: this.namespace,
|
||||
sessionId,
|
||||
},
|
||||
'Successfully updated payment status',
|
||||
);
|
||||
logger.info(ctx, 'Successfully updated payment status');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,18 +26,19 @@ export async function createStripeCheckout(
|
||||
const mode: Stripe.Checkout.SessionCreateParams.Mode =
|
||||
params.plan.paymentType === 'recurring' ? 'subscription' : 'payment';
|
||||
|
||||
const isSubscription = mode === 'subscription';
|
||||
|
||||
// this should only be set if the mode is 'subscription'
|
||||
const subscriptionData:
|
||||
| Stripe.Checkout.SessionCreateParams.SubscriptionData
|
||||
| undefined =
|
||||
mode === 'subscription'
|
||||
? {
|
||||
trial_period_days: params.trialDays,
|
||||
metadata: {
|
||||
accountId: params.accountId,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
| undefined = isSubscription
|
||||
? {
|
||||
trial_period_days: params.trialDays,
|
||||
metadata: {
|
||||
accountId: params.accountId,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
const urls = getUrls({
|
||||
returnUrl: params.returnUrl,
|
||||
@@ -54,6 +55,10 @@ export async function createStripeCheckout(
|
||||
customer_email: params.customerEmail,
|
||||
};
|
||||
|
||||
const customerCreation = isSubscription
|
||||
? ({} as Record<string, string>)
|
||||
: { customer_creation: 'always' };
|
||||
|
||||
const lineItems = params.plan.lineItems.map((item) => {
|
||||
if (item.type === 'metered') {
|
||||
return {
|
||||
@@ -81,7 +86,7 @@ export async function createStripeCheckout(
|
||||
line_items: lineItems,
|
||||
client_reference_id: clientReferenceId,
|
||||
subscription_data: subscriptionData,
|
||||
customer_creation: 'always',
|
||||
...customerCreation,
|
||||
...customerData,
|
||||
...urls,
|
||||
});
|
||||
|
||||
@@ -282,17 +282,18 @@ export class StripeWebhookHandlerService
|
||||
|
||||
const lineItems = params.lineItems.map((item) => {
|
||||
const quantity = item.quantity ?? 1;
|
||||
const variantId = item.price?.id as string;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
quantity,
|
||||
subscription_id: params.id,
|
||||
product_id: item.price?.product as string,
|
||||
variant_id: item.price?.id,
|
||||
variant_id: variantId,
|
||||
price_amount: item.price?.unit_amount,
|
||||
interval: item.price?.recurring?.interval as string,
|
||||
interval_count: item.price?.recurring?.interval_count as number,
|
||||
type: getLineItemTypeById(this.config, item.id),
|
||||
type: getLineItemTypeById(this.config, variantId),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -47,30 +47,30 @@ export const UpdateInvitationDialog: React.FC<{
|
||||
userRoleHierarchy,
|
||||
account,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||
</DialogTitle>
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<UpdateInvitationForm
|
||||
account={account}
|
||||
invitationId={invitationId}
|
||||
userRole={userRole}
|
||||
userRoleHierarchy={userRoleHierarchy}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
<UpdateInvitationForm
|
||||
account={account}
|
||||
invitationId={invitationId}
|
||||
userRole={userRole}
|
||||
userRoleHierarchy={userRoleHierarchy}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
function UpdateInvitationForm({
|
||||
account,
|
||||
|
||||
@@ -47,37 +47,37 @@ export const UpdateMemberRoleDialog: React.FC<{
|
||||
userRole,
|
||||
userRoleHierarchy,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||
</DialogTitle>
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<RolesDataProvider
|
||||
accountId={accountId}
|
||||
maxRoleHierarchy={userRoleHierarchy}
|
||||
>
|
||||
{(data) => (
|
||||
<UpdateMemberForm
|
||||
setIsOpen={setIsOpen}
|
||||
userId={userId}
|
||||
accountId={accountId}
|
||||
userRole={userRole}
|
||||
roles={data}
|
||||
/>
|
||||
)}
|
||||
</RolesDataProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
<RolesDataProvider
|
||||
accountId={accountId}
|
||||
maxRoleHierarchy={userRoleHierarchy}
|
||||
>
|
||||
{(data) => (
|
||||
<UpdateMemberForm
|
||||
setIsOpen={setIsOpen}
|
||||
userId={userId}
|
||||
accountId={accountId}
|
||||
userRole={userRole}
|
||||
roles={data}
|
||||
/>
|
||||
)}
|
||||
</RolesDataProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
function UpdateMemberForm({
|
||||
userId,
|
||||
|
||||
@@ -26,7 +26,9 @@ export function GlobalLoader({
|
||||
</If>
|
||||
|
||||
<If condition={displaySpinner}>
|
||||
<div className={'flex flex-1 flex-col items-center justify-center py-48'}>
|
||||
<div
|
||||
className={'flex flex-1 flex-col items-center justify-center py-48'}
|
||||
>
|
||||
<LoadingOverlay displayLogo={displayLogo} fullPage={fullPage}>
|
||||
{Text}
|
||||
</LoadingOverlay>
|
||||
|
||||
Reference in New Issue
Block a user