Remove team account related services and actions
Removed services and actions related to team account deletion as well as updated paths within other dependent files, better reflecting their new locations. Also, added a new service titled 'AccountBillingService' for handling billing-related operations and restructured the form layout and handled translation in 'team-account-danger-zone' component.
This commit is contained in:
@@ -30,8 +30,8 @@ type AccountModel = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const features = {
|
const features = {
|
||||||
enableOrganizationAccounts: featureFlagsConfig.enableOrganizationAccounts,
|
enableTeamAccounts: featureFlagsConfig.enableTeamAccounts,
|
||||||
enableOrganizationCreation: featureFlagsConfig.enableOrganizationCreation,
|
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppSidebar(props: {
|
export function AppSidebar(props: {
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ import featureFlagsConfig from '~/config/feature-flags.config';
|
|||||||
import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config';
|
import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config';
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
|
||||||
|
const features = {
|
||||||
|
enableTeamAccounts: featureFlagsConfig.enableTeamAccounts,
|
||||||
|
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
|
||||||
|
};
|
||||||
|
|
||||||
export const MobileAppNavigation = (
|
export const MobileAppNavigation = (
|
||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -161,12 +166,7 @@ function OrganizationsModal() {
|
|||||||
router.replace(path);
|
router.replace(path);
|
||||||
}}
|
}}
|
||||||
accounts={[]}
|
accounts={[]}
|
||||||
features={{
|
features={features}
|
||||||
enableOrganizationAccounts:
|
|
||||||
featureFlagsConfig.enableOrganizationAccounts,
|
|
||||||
enableOrganizationCreation:
|
|
||||||
featureFlagsConfig.enableOrganizationCreation,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import pathsConfig from '~/config/paths.config';
|
|||||||
*
|
*
|
||||||
* @param accountSlug
|
* @param accountSlug
|
||||||
*/
|
*/
|
||||||
export const loadOrganizationWorkspace = cache(async (accountSlug: string) => {
|
export const loadTeamWorkspace = cache(async (accountSlug: string) => {
|
||||||
const client = getSupabaseServerComponentClient();
|
const client = getSupabaseServerComponentClient();
|
||||||
|
|
||||||
const accountPromise = client.rpc('organization_account_workspace', {
|
const accountPromise = client.rpc('organization_account_workspace', {
|
||||||
@@ -8,7 +8,7 @@ import { If } from '@kit/ui/if';
|
|||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace';
|
import { loadTeamWorkspace } from '~/(dashboard)/home/[account]/_lib/load-team-account-workspace';
|
||||||
import { createBillingPortalSession } from '~/(dashboard)/home/[account]/billing/server-actions';
|
import { createBillingPortalSession } from '~/(dashboard)/home/[account]/billing/server-actions';
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
@@ -22,7 +22,7 @@ interface Params {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function OrganizationAccountBillingPage({ params }: Params) {
|
async function OrganizationAccountBillingPage({ params }: Params) {
|
||||||
const workspace = await loadOrganizationWorkspace(params.account);
|
const workspace = await loadTeamWorkspace(params.account);
|
||||||
const accountId = workspace.account.id;
|
const accountId = workspace.account.id;
|
||||||
const [subscription, customerId] = await loadAccountData(accountId);
|
const [subscription, customerId] = await loadAccountData(accountId);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getSupabaseServerComponentClient } from '@kit/supabase/server-component
|
|||||||
import { Page } from '@kit/ui/page';
|
import { Page } from '@kit/ui/page';
|
||||||
|
|
||||||
import { AppSidebar } from '~/(dashboard)/home/[account]/_components/app-sidebar';
|
import { AppSidebar } from '~/(dashboard)/home/[account]/_components/app-sidebar';
|
||||||
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace';
|
import { loadTeamWorkspace } from '~/(dashboard)/home/[account]/_lib/load-team-account-workspace';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
@@ -20,7 +20,7 @@ function OrganizationWorkspaceLayout({
|
|||||||
params: Params;
|
params: Params;
|
||||||
}>) {
|
}>) {
|
||||||
const [data, session] = use(
|
const [data, session] = use(
|
||||||
Promise.all([loadOrganizationWorkspace(params.account), loadSession()]),
|
Promise.all([loadTeamWorkspace(params.account), loadSession()]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ui = getUIStateCookies();
|
const ui = getUIStateCookies();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace';
|
import { loadTeamWorkspace } from '~/(dashboard)/home/[account]/_lib/load-team-account-workspace';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
@@ -60,7 +60,7 @@ async function OrganizationAccountMembersPage({ params }: Params) {
|
|||||||
const slug = params.account;
|
const slug = params.account;
|
||||||
|
|
||||||
const [{ account, user }, members, invitations] = await Promise.all([
|
const [{ account, user }, members, invitations] = await Promise.all([
|
||||||
loadOrganizationWorkspace(slug),
|
loadTeamWorkspace(slug),
|
||||||
loadAccountMembers(slug),
|
loadAccountMembers(slug),
|
||||||
loadInvitations(slug),
|
loadInvitations(slug),
|
||||||
]);
|
]);
|
||||||
|
|||||||
62
apps/web/app/(dashboard)/home/[account]/settings/page.tsx
Normal file
62
apps/web/app/(dashboard)/home/[account]/settings/page.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { TeamAccountSettingsContainer } from '@kit/team-accounts/components';
|
||||||
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { loadTeamWorkspace } from '~/(dashboard)/home/[account]/_lib/load-team-account-workspace';
|
||||||
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
const i18n = await createI18nServerInstance();
|
||||||
|
const title = i18n.t('accounts:settings:pageTitle');
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
params: {
|
||||||
|
account: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
teamAccountSettings: pathsConfig.app.accountSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function TeamAccountSettingsPage(props: Props) {
|
||||||
|
const data = await loadTeamWorkspace(props.params.account);
|
||||||
|
const account = {
|
||||||
|
id: data.account.id,
|
||||||
|
name: data.account.name,
|
||||||
|
pictureUrl: data.account.picture_url,
|
||||||
|
slug: data.account.slug,
|
||||||
|
primaryOwnerUserId: data.account.primary_owner_user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title={<Trans i18nKey={'teams:settings.pageTitle'} />}
|
||||||
|
description={<Trans i18nKey={'teams:settings.pageDescription'} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageBody>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'container mx-auto flex w-full max-w-4xl flex-1 flex-col items-center'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TeamAccountSettingsContainer
|
||||||
|
userId={data.user.id}
|
||||||
|
account={account}
|
||||||
|
paths={paths}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TeamAccountSettingsPage;
|
||||||
@@ -8,8 +8,8 @@ import featureFlagsConfig from '~/config/feature-flags.config';
|
|||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
|
||||||
const features = {
|
const features = {
|
||||||
enableOrganizationAccounts: featureFlagsConfig.enableOrganizationAccounts,
|
enableTeamAccounts: featureFlagsConfig.enableTeamAccounts,
|
||||||
enableOrganizationCreation: featureFlagsConfig.enableOrganizationCreation,
|
enableTeamCreation: featureFlagsConfig.enableTeamCreation,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HomeSidebarAccountSelector(props: {
|
export function HomeSidebarAccountSelector(props: {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import type { Session } from '@supabase/supabase-js';
|
import type { Session } from '@supabase/supabase-js';
|
||||||
|
|||||||
@@ -23,13 +23,12 @@ export default async function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const i18n = await createI18nServerInstance();
|
const { language } = await createI18nServerInstance();
|
||||||
const lang = i18n.language;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={lang} className={getClassName()}>
|
<html lang={language} className={getClassName()}>
|
||||||
<body>
|
<body>
|
||||||
<RootProviders lang={lang}>{children}</RootProviders>
|
<RootProviders lang={language}>{children}</RootProviders>
|
||||||
<Toaster richColors={false} />
|
<Toaster richColors={false} />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { z } from 'zod';
|
|||||||
const FeatureFlagsSchema = z.object({
|
const FeatureFlagsSchema = z.object({
|
||||||
enableThemeSwitcher: z.boolean(),
|
enableThemeSwitcher: z.boolean(),
|
||||||
enableAccountDeletion: z.boolean(),
|
enableAccountDeletion: z.boolean(),
|
||||||
enableOrganizationDeletion: z.boolean(),
|
enableTeamDeletion: z.boolean(),
|
||||||
enableOrganizationAccounts: z.boolean(),
|
enableTeamAccounts: z.boolean(),
|
||||||
enableOrganizationCreation: z.boolean(),
|
enableTeamCreation: z.boolean(),
|
||||||
enablePersonalAccountBilling: z.boolean(),
|
enablePersonalAccountBilling: z.boolean(),
|
||||||
enableOrganizationBilling: z.boolean(),
|
enableTeamAccountBilling: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const featuresFlagConfig = FeatureFlagsSchema.parse({
|
const featuresFlagConfig = FeatureFlagsSchema.parse({
|
||||||
@@ -16,23 +16,23 @@ const featuresFlagConfig = FeatureFlagsSchema.parse({
|
|||||||
process.env.NEXT_PUBLIC_ENABLE_ACCOUNT_DELETION,
|
process.env.NEXT_PUBLIC_ENABLE_ACCOUNT_DELETION,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
enableOrganizationDeletion: getBoolean(
|
enableTeamDeletion: getBoolean(
|
||||||
process.env.NEXT_PUBLIC_ENABLE_ORGANIZATION_DELETION,
|
process.env.NEXT_PUBLIC_ENABLE_TEAM_DELETION,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
enableOrganizationAccounts: getBoolean(
|
enableTeamAccounts: getBoolean(
|
||||||
process.env.NEXT_PUBLIC_ENABLE_ORGANIZATION_ACCOUNTS,
|
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
enableOrganizationCreation: getBoolean(
|
enableTeamCreation: getBoolean(
|
||||||
process.env.NEXT_PUBLIC_ENABLE_ORGANIZATION_CREATION,
|
process.env.NEXT_PUBLIC_ENABLE_TEAMS_CREATION,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
enablePersonalAccountBilling: getBoolean(
|
enablePersonalAccountBilling: getBoolean(
|
||||||
process.env.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING,
|
process.env.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
enableOrganizationBilling: getBoolean(
|
enableTeamAccountBilling: getBoolean(
|
||||||
process.env.NEXT_PUBLIC_ENABLE_ORGANIZATION_BILLING,
|
process.env.NEXT_PUBLIC_ENABLE_ORGANIZATION_BILLING,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,31 +17,31 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@epic-web/invariant": "^1.0.0",
|
"@epic-web/invariant": "^1.0.0",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@kit/accounts": "^0.1.0",
|
"@kit/accounts": "workspace:^",
|
||||||
"@kit/admin": "^0.1.0",
|
"@kit/admin": "workspace:^",
|
||||||
"@kit/auth": "^0.1.0",
|
"@kit/auth": "workspace:^",
|
||||||
"@kit/billing": "^0.1.0",
|
"@kit/billing": "workspace:^",
|
||||||
"@kit/billing-gateway": "^0.1.0",
|
"@kit/billing-gateway": "workspace:^",
|
||||||
"@kit/email-templates": "^0.1.0",
|
"@kit/email-templates": "workspace:^",
|
||||||
"@kit/i18n": "^0.1.0",
|
"@kit/i18n": "workspace:^",
|
||||||
"@kit/mailers": "^0.1.0",
|
"@kit/mailers": "workspace:^",
|
||||||
"@kit/shared": "^0.1.0",
|
"@kit/shared": "workspace:^",
|
||||||
"@kit/supabase": "^0.1.0",
|
"@kit/supabase": "workspace:^",
|
||||||
"@kit/team-accounts": "^0.1.0",
|
"@kit/team-accounts": "workspace:^",
|
||||||
"@kit/ui": "^0.1.0",
|
"@kit/ui": "workspace:^",
|
||||||
"@next/mdx": "^14.1.0",
|
"@next/mdx": "^14.1.4",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@supabase/ssr": "^0.1.0",
|
"@supabase/ssr": "^0.1.0",
|
||||||
"@supabase/supabase-js": "^2.40.0",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"@tanstack/react-query": "5.28.6",
|
"@tanstack/react-query": "5.28.6",
|
||||||
"@tanstack/react-query-next-experimental": "^5.28.6",
|
"@tanstack/react-query-next-experimental": "^5.28.9",
|
||||||
"@tanstack/react-table": "^8.11.3",
|
"@tanstack/react-table": "^8.15.0",
|
||||||
"contentlayer": "0.3.4",
|
"contentlayer": "0.3.4",
|
||||||
"date-fns": "^3.2.0",
|
"date-fns": "^3.6.0",
|
||||||
"edge-csrf": "^1.0.9",
|
"edge-csrf": "^1.0.9",
|
||||||
"i18next": "^23.10.1",
|
"i18next": "^23.10.1",
|
||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"next": "14.2.0-canary.44",
|
"next": "14.2.0-canary.46",
|
||||||
"next-contentlayer": "0.3.4",
|
"next-contentlayer": "0.3.4",
|
||||||
"next-sitemap": "^4.2.3",
|
"next-sitemap": "^4.2.3",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.2",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"recharts": "^2.10.3",
|
"recharts": "^2.12.3",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
@@ -57,21 +57,21 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "^0.2.0",
|
"@kit/eslint-config": "workspace:^",
|
||||||
"@kit/prettier-config": "^0.1.0",
|
"@kit/prettier-config": "workspace:^",
|
||||||
"@kit/tailwind-config": "^0.1.0",
|
"@kit/tailwind-config": "workspace:^",
|
||||||
"@kit/tsconfig": "^0.1.0",
|
"@kit/tsconfig": "workspace:^",
|
||||||
"@next/bundle-analyzer": "14.2.0-canary.44",
|
"@next/bundle-analyzer": "14.2.0-canary.44",
|
||||||
"@types/mdx": "^2.0.10",
|
"@types/mdx": "^2.0.12",
|
||||||
"@types/node": "^20.11.5",
|
"@types/node": "^20.11.30",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.73",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.22",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.19",
|
||||||
"dotenv-cli": "^7.3.0",
|
"dotenv-cli": "^7.4.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.3"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
{
|
{
|
||||||
"generalTabLabel": "General",
|
"settings": {
|
||||||
"generalTabLabelSubheading": "Manage your Organization",
|
"pageTitle": "Settings",
|
||||||
|
"pageDescription": "Manage your Team details"
|
||||||
|
},
|
||||||
|
"yourTeam": "Your Teams",
|
||||||
|
"createTeam": "Create Team",
|
||||||
"membersTabLabel": "Members",
|
"membersTabLabel": "Members",
|
||||||
"emailSettingsTab": "Email",
|
"emailSettingsTab": "Email",
|
||||||
"membersTabSubheading": "Manage and Invite members",
|
"membersTabSubheading": "Manage and Invite members",
|
||||||
"inviteMembersPageSubheading": "Invite members to your organization",
|
"inviteMembersPageSubheading": "Invite members to your Team",
|
||||||
"createOrganizationModalHeading": "Create Organization",
|
"createTeamModalHeading": "Create Team",
|
||||||
"organizationNameLabel": "Organization Name",
|
"createTeamModalDescription": "Create a new Team to manage your projects and members.",
|
||||||
"createOrganizationSubmitLabel": "Create Organization",
|
"teamNameLabel": "Team Name",
|
||||||
"createOrganizationSuccess": "Organization created successfully",
|
"teamNameDescription": "Your team name should be unique and descriptive",
|
||||||
"createOrganizationError": "Organization not created. Please try again.",
|
"createTeamSubmitLabel": "Create Team",
|
||||||
"createOrganizationLoading": "Creating organization...",
|
"createTeamSuccess": "Team created successfully",
|
||||||
|
"createTeamError": "Team not created. Please try again.",
|
||||||
|
"createTeamLoading": "Creating team...",
|
||||||
"settingsPageLabel": "General",
|
"settingsPageLabel": "General",
|
||||||
"createOrganizationDropdownLabel": "New organization",
|
"createTeamDropdownLabel": "New team",
|
||||||
"changeRole": "Change Role",
|
"changeRole": "Change Role",
|
||||||
"removeMember": "Remove",
|
"removeMember": "Remove",
|
||||||
"inviteMembersSuccess": "Members invited successfully!",
|
"inviteMembersSuccess": "Members invited successfully!",
|
||||||
@@ -26,7 +32,7 @@
|
|||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
||||||
"removeMemberLoadingMessage": "Removing member...",
|
"removeMemberLoadingMessage": "Removing member...",
|
||||||
"removeMemberSubmitLabel": "Remove User from Organization",
|
"removeMemberSubmitLabel": "Remove User from Team",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"chooseDifferentRoleError": "Role is the same as the current one",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"updateRoleLoadingMessage": "Updating role...",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"updateRoleSuccessMessage": "Role updated successfully",
|
||||||
@@ -40,20 +46,20 @@
|
|||||||
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
"deleteInviteErrorMessage": "Invite not deleted. Please try again.",
|
||||||
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
"deleteInviteLoadingMessage": "Deleting invite. Please wait...",
|
||||||
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
"confirmDeletingMemberInvite": "You are deleting the invite to <b>{{ email }}</b>",
|
||||||
"transferOwnershipDisclaimer": "You are transferring ownership of the selected organization to <b>{{ member }}</b>.",
|
"transferOwnershipDisclaimer": "You are transferring ownership of the selected team to <b>{{ member }}</b>.",
|
||||||
"transferringOwnership": "Transferring ownership...",
|
"transferringOwnership": "Transferring ownership...",
|
||||||
"transferOwnershipSuccess": "Ownership successfully transferred",
|
"transferOwnershipSuccess": "Ownership successfully transferred",
|
||||||
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
"transferOwnershipError": "Sorry, we could not transfer ownership to the selected member. Please try again.",
|
||||||
"deleteInviteSubmitLabel": "Delete Invite",
|
"deleteInviteSubmitLabel": "Delete Invite",
|
||||||
"youBadgeLabel": "You",
|
"youBadgeLabel": "You",
|
||||||
"updateOrganizationLoadingMessage": "Updating Organization...",
|
"updateTeamLoadingMessage": "Updating Team...",
|
||||||
"updateOrganizationSuccessMessage": "Organization successfully updated",
|
"updateTeamSuccessMessage": "Team successfully updated",
|
||||||
"updateOrganizationErrorMessage": "Could not update Organization. Please try again.",
|
"updateTeamErrorMessage": "Could not update Team. Please try again.",
|
||||||
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
"updateLogoErrorMessage": "Could not update Logo. Please try again.",
|
||||||
"organizationNameInputLabel": "Organization Name",
|
"teamNameInputLabel": "Team Name",
|
||||||
"organizationLogoInputHeading": "Upload your organization's Logo",
|
"teamLogoInputHeading": "Upload your team's Logo",
|
||||||
"organizationLogoInputSubheading": "Please choose a photo to upload as your organization logo.",
|
"teamLogoInputSubheading": "Please choose a photo to upload as your team logo.",
|
||||||
"updateOrganizationSubmitLabel": "Update Organization",
|
"updateTeamSubmitLabel": "Update Team",
|
||||||
"inviteMembersPageHeading": "Invite Members",
|
"inviteMembersPageHeading": "Invite Members",
|
||||||
"goBackToMembersPage": "Go back to members",
|
"goBackToMembersPage": "Go back to members",
|
||||||
"membersPageHeading": "Members",
|
"membersPageHeading": "Members",
|
||||||
@@ -62,31 +68,32 @@
|
|||||||
"pendingInvitesSubheading": "Manage invites not yet accepted",
|
"pendingInvitesSubheading": "Manage invites not yet accepted",
|
||||||
"noPendingInvites": "No pending invites found",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading members...",
|
"loadingMembers": "Loading members...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your organization's members.",
|
"loadMembersError": "Sorry, we couldn't fetch your team's members.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your organization's invited members.",
|
"loadInvitedMembersError": "Sorry, we couldn't fetch your team's invited members.",
|
||||||
"loadingInvitedMembers": "Loading invited members...",
|
"loadingInvitedMembers": "Loading invited members...",
|
||||||
"invitedBadge": "Invited",
|
"invitedBadge": "Invited",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"duplicateInviteEmailError": "You have already entered this email address",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"invitingOwnAccountError": "Hey, that's your email!",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneSubheading": "Delete or leave your organization",
|
"dangerZoneSubheading": "Delete or leave your team",
|
||||||
"deleteOrganization": "Delete Organization",
|
"deleteTeam": "Delete Team",
|
||||||
"deleteOrganizationDescription": "This action cannot be undone. All data associated with this organization will be deleted.",
|
"deleteTeamDescription": "This action cannot be undone. All data associated with this team will be deleted.",
|
||||||
"deletingOrganization": "Deleting organization",
|
"deletingTeam": "Deleting team",
|
||||||
"deleteOrganizationModalHeading": "Deleting Organization",
|
"deleteTeamModalHeading": "Deleting Team",
|
||||||
"deleteOrganizationInputField": "Type the name of the organization to confirm",
|
"deletingTeamDescription": "You are about to delete the team {{ teamName }}. This action cannot be undone.",
|
||||||
"leaveOrganization": "Leave Organization",
|
"deleteTeamInputField": "Type the name of the team to confirm",
|
||||||
"leavingOrganizationModalHeading": "Leaving Organization",
|
"leaveTeam": "Leave Team",
|
||||||
"leaveOrganizationDescription": "You will no longer have access to this organization.",
|
"leavingTeamModalHeading": "Leaving Team",
|
||||||
"deleteOrganizationDisclaimer": "You are deleting the organization {{ organizationName }}. This action cannot be undone.",
|
"leaveTeamDescription": "You will no longer have access to this team.",
|
||||||
"leaveOrganizationDisclaimer": "You are leaving the organization {{ organizationName }}. You will no longer have access to it.",
|
"deleteTeamDisclaimer": "You are deleting the team {{ teamName }}. This action cannot be undone.",
|
||||||
"deleteOrganizationErrorHeading": "Sorry, we couldn't delete your organization.",
|
"leaveTeamDisclaimer": "You are leaving the team {{ teamName }}. You will no longer have access to it.",
|
||||||
"leaveOrganizationErrorHeading": "Sorry, we couldn't leave your organization.",
|
"deleteTeamErrorHeading": "Sorry, we couldn't delete your team.",
|
||||||
|
"leaveTeamErrorHeading": "Sorry, we couldn't leave your team.",
|
||||||
"searchMembersPlaceholder": "Search members",
|
"searchMembersPlaceholder": "Search members",
|
||||||
"createOrganizationErrorHeading": "Sorry, we couldn't create your organization.",
|
"createTeamErrorHeading": "Sorry, we couldn't create your team.",
|
||||||
"createOrganizationErrorMessage": "We encountered an error creating your organization. Please try again.",
|
"createTeamErrorMessage": "We encountered an error creating your team. Please try again.",
|
||||||
"transferOrganizationErrorHeading": "Sorry, we couldn't transfer ownership of your organization.",
|
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.",
|
||||||
"transferOrganizationErrorMessage": "We encountered an error transferring ownership of your organization. Please try again.",
|
"transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.",
|
||||||
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
|
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again."
|
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,11 +35,12 @@
|
|||||||
"supabase"
|
"supabase"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@manypkg/cli": "^0.21.2",
|
"@manypkg/cli": "^0.21.3",
|
||||||
"@turbo/gen": "^1.11.3",
|
"@turbo/gen": "^1.13.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"pnpm": "^8.15.5",
|
"pnpm": "^8.15.5",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"turbo": "^1.13.0"
|
"turbo": "^1.13.0",
|
||||||
|
"yarn": "^1.22.22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,15 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/billing": "*",
|
"@kit/billing": "^0.1.0",
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/shared": "*",
|
"@kit/shared": "^0.1.0",
|
||||||
"@kit/stripe": "*",
|
"@kit/stripe": "^0.1.0",
|
||||||
"@kit/supabase": "*",
|
"@kit/supabase": "^0.1.0",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "*",
|
"@kit/ui": "^0.1.0",
|
||||||
"@supabase/supabase-js": "^2.40.0",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './services/billing-gateway/billing-gateway.service';
|
export * from './server/services/billing-gateway/billing-gateway.service';
|
||||||
export * from './services/billing-gateway/billing-gateway-provider-factory';
|
export * from './server/services/billing-gateway/billing-gateway-provider-factory';
|
||||||
export * from './services/billing-event-handler/billing-gateway-provider-factory';
|
export * from './server/services/billing-event-handler/billing-gateway-provider-factory';
|
||||||
|
export * from './server/services/account-billing.service';
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { Logger } from '@kit/shared/logger';
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
import { BillingGatewayService } from './billing-gateway/billing-gateway.service';
|
||||||
|
|
||||||
|
export class AccountBillingService {
|
||||||
|
private readonly namespace = 'accounts.billing';
|
||||||
|
|
||||||
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
async cancelAllAccountSubscriptions({
|
||||||
|
accountId,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
accountId: string;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
accountId,
|
||||||
|
name: this.namespace,
|
||||||
|
},
|
||||||
|
'Cancelling all subscriptions for account...',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: subscriptions } = await this.client
|
||||||
|
.from('subscriptions')
|
||||||
|
.select('*')
|
||||||
|
.eq('account_id', accountId);
|
||||||
|
|
||||||
|
const cancellationRequests = [];
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
subscriptions: subscriptions?.length ?? 0,
|
||||||
|
name: this.namespace,
|
||||||
|
},
|
||||||
|
'Cancelling all account subscriptions...',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const subscription of subscriptions ?? []) {
|
||||||
|
const gateway = new BillingGatewayService(subscription.billing_provider);
|
||||||
|
|
||||||
|
cancellationRequests.push(
|
||||||
|
gateway.cancelSubscription({
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
invoiceNow: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute all cancellation requests
|
||||||
|
await Promise.all(cancellationRequests);
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
subscriptions: subscriptions?.length ?? 0,
|
||||||
|
name: this.namespace,
|
||||||
|
},
|
||||||
|
'Subscriptions cancelled successfully',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/supabase": "0.1.0",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "0.1.0",
|
"@kit/ui": "workspace:*",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
"@react-email/components": "0.0.15"
|
"@react-email/components": "0.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0"
|
"@kit/tsconfig": "workspace:*"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
} from '@react-email/components';
|
} from '@react-email/components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
organizationName: string;
|
teamName: string;
|
||||||
organizationLogo?: string;
|
teamLogo?: string;
|
||||||
inviter: string | undefined;
|
inviter: string | undefined;
|
||||||
invitedUserEmail: string;
|
invitedUserEmail: string;
|
||||||
link: string;
|
link: string;
|
||||||
@@ -38,7 +38,7 @@ export function renderInviteEmail(props: Props) {
|
|||||||
<Body className="mx-auto my-auto bg-gray-50 font-sans">
|
<Body className="mx-auto my-auto bg-gray-50 font-sans">
|
||||||
<Container className="mx-auto my-[40px] w-[465px] rounded-lg border border-solid border-[#eaeaea] bg-white p-[20px]">
|
<Container className="mx-auto my-[40px] w-[465px] rounded-lg border border-solid border-[#eaeaea] bg-white p-[20px]">
|
||||||
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-black">
|
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-black">
|
||||||
Join <strong>{props.organizationName}</strong> on{' '}
|
Join <strong>{props.teamName}</strong> on{' '}
|
||||||
<strong>{props.productName}</strong>
|
<strong>{props.productName}</strong>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text className="text-[14px] leading-[24px] text-black">
|
<Text className="text-[14px] leading-[24px] text-black">
|
||||||
@@ -46,16 +46,16 @@ export function renderInviteEmail(props: Props) {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className="text-[14px] leading-[24px] text-black">
|
<Text className="text-[14px] leading-[24px] text-black">
|
||||||
<strong>{props.inviter}</strong> has invited you to the{' '}
|
<strong>{props.inviter}</strong> has invited you to the{' '}
|
||||||
<strong>{props.organizationName}</strong> team on{' '}
|
<strong>{props.teamName}</strong> team on{' '}
|
||||||
<strong>{props.productName}</strong>.
|
<strong>{props.productName}</strong>.
|
||||||
</Text>
|
</Text>
|
||||||
{props.organizationLogo && (
|
{props.teamLogo && (
|
||||||
<Section>
|
<Section>
|
||||||
<Row>
|
<Row>
|
||||||
<Column align="center">
|
<Column align="center">
|
||||||
<Img
|
<Img
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
src={props.organizationLogo}
|
src={props.teamLogo}
|
||||||
width="64"
|
width="64"
|
||||||
height="64"
|
height="64"
|
||||||
/>
|
/>
|
||||||
@@ -68,7 +68,7 @@ export function renderInviteEmail(props: Props) {
|
|||||||
className="rounded bg-[#000000] px-[20px] py-[12px] text-center text-[12px] font-semibold text-white no-underline"
|
className="rounded bg-[#000000] px-[20px] py-[12px] text-center text-[12px] font-semibold text-white no-underline"
|
||||||
href={props.link}
|
href={props.link}
|
||||||
>
|
>
|
||||||
Join {props.organizationName}
|
Join {props.teamName}
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
<Text className="text-[14px] leading-[24px] text-black">
|
<Text className="text-[14px] leading-[24px] text-black">
|
||||||
|
|||||||
@@ -15,20 +15,23 @@
|
|||||||
"./hooks/*": "./src/hooks/*.ts"
|
"./hooks/*": "./src/hooks/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/billing-gateway": "*",
|
|
||||||
"@kit/email-templates": "*",
|
|
||||||
"@kit/mailers": "*",
|
|
||||||
"@kit/eslint-config": "0.2.0",
|
|
||||||
"@kit/prettier-config": "0.1.0",
|
|
||||||
"@kit/shared": "*",
|
|
||||||
"@kit/supabase": "*",
|
|
||||||
"@kit/tailwind-config": "0.1.0",
|
|
||||||
"@kit/tsconfig": "0.1.0",
|
|
||||||
"@kit/ui": "*",
|
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
|
"@kit/billing-gateway": "^0.1.0",
|
||||||
|
"@kit/email-templates": "^0.1.0",
|
||||||
|
"@kit/eslint-config": "workspace:*",
|
||||||
|
"@kit/mailers": "^0.1.0",
|
||||||
|
"@kit/prettier-config": "workspace:*",
|
||||||
|
"@kit/shared": "^0.1.0",
|
||||||
|
"@kit/supabase": "^0.1.0",
|
||||||
|
"@kit/tailwind-config": "workspace:*",
|
||||||
|
"@kit/tsconfig": "workspace:*",
|
||||||
|
"@kit/ui": "^0.1.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@tanstack/react-query": "5.28.6",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.2",
|
||||||
|
"react-i18next": "^14.1.0",
|
||||||
|
"sonner": "^1.4.41",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -36,7 +39,8 @@
|
|||||||
"@kit/supabase": "0.1.0",
|
"@kit/supabase": "0.1.0",
|
||||||
"@kit/ui": "0.1.0",
|
"@kit/ui": "0.1.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"lucide-react": "^0.363.0"
|
"lucide-react": "^0.363.0",
|
||||||
|
"sonner": "^1.4.41"
|
||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config",
|
"prettier": "@kit/prettier-config",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from '@kit/ui/command';
|
} from '@kit/ui/command';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@kit/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@kit/ui/popover';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { CreateTeamAccountDialog } from '../../../team-accounts/src/components/create-team-account-dialog';
|
import { CreateTeamAccountDialog } from '../../../team-accounts/src/components/create-team-account-dialog';
|
||||||
@@ -29,8 +30,8 @@ interface AccountSelectorProps {
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
features: {
|
features: {
|
||||||
enableOrganizationAccounts: boolean;
|
enableTeamAccounts: boolean;
|
||||||
enableOrganizationCreation: boolean;
|
enableTeamCreation: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
selectedAccount?: string;
|
selectedAccount?: string;
|
||||||
@@ -46,8 +47,8 @@ export function AccountSelector({
|
|||||||
selectedAccount,
|
selectedAccount,
|
||||||
onAccountChange,
|
onAccountChange,
|
||||||
features = {
|
features = {
|
||||||
enableOrganizationAccounts: true,
|
enableTeamAccounts: true,
|
||||||
enableOrganizationCreation: true,
|
enableTeamCreation: true,
|
||||||
},
|
},
|
||||||
collapsed = false,
|
collapsed = false,
|
||||||
}: React.PropsWithChildren<AccountSelectorProps>) {
|
}: React.PropsWithChildren<AccountSelectorProps>) {
|
||||||
@@ -75,6 +76,10 @@ export function AccountSelector({
|
|||||||
|
|
||||||
const selected = accounts.find((account) => account.value === value);
|
const selected = accounts.find((account) => account.value === value);
|
||||||
|
|
||||||
|
if (!features.enableTeamAccounts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
@@ -150,9 +155,9 @@ export function AccountSelector({
|
|||||||
|
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
|
|
||||||
<If condition={features.enableOrganizationAccounts}>
|
<If condition={features.enableTeamAccounts}>
|
||||||
<If condition={accounts.length > 0}>
|
<If condition={accounts.length > 0}>
|
||||||
<CommandGroup heading={'Your Organizations'}>
|
<CommandGroup heading={<Trans i18nKey={'teams:yourTeams'} />}>
|
||||||
{(accounts ?? []).map((account) => (
|
{(accounts ?? []).map((account) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={account.value}
|
key={account.value}
|
||||||
@@ -185,7 +190,7 @@ export function AccountSelector({
|
|||||||
</If>
|
</If>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={features.enableOrganizationCreation}>
|
<If condition={features.enableTeamCreation}>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<Button
|
<Button
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
@@ -198,7 +203,9 @@ export function AccountSelector({
|
|||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
|
||||||
<span>Create Organization</span>
|
<span>
|
||||||
|
<Trans i18nKey={'teams:createTeam'} />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</If>
|
</If>
|
||||||
@@ -207,7 +214,7 @@ export function AccountSelector({
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<If condition={features.enableOrganizationCreation}>
|
<If condition={features.enableTeamCreation}>
|
||||||
<CreateTeamAccountDialog
|
<CreateTeamAccountDialog
|
||||||
isOpen={isCreatingAccount}
|
isOpen={isCreatingAccount}
|
||||||
setIsOpen={setIsCreatingAccount}
|
setIsOpen={setIsCreatingAccount}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@kit/ui/alert-dialog';
|
} from '@kit/ui/alert-dialog';
|
||||||
@@ -166,18 +167,20 @@ function ConfirmUnenrollFactorModal(
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogAction
|
<AlertDialogFooter>
|
||||||
className={'w-full'}
|
<AlertDialogCancel>
|
||||||
type={'button'}
|
<Trans i18nKey={'common:cancel'} />
|
||||||
disabled={unEnroll.isPending}
|
</AlertDialogCancel>
|
||||||
onClick={() => onUnenrollRequested(props.factorId)}
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
|
|
||||||
</AlertDialogAction>
|
|
||||||
|
|
||||||
<AlertDialogCancel>
|
<AlertDialogAction
|
||||||
<Trans i18nKey={'common:cancel'} />
|
className={'w-full'}
|
||||||
</AlertDialogCancel>
|
type={'button'}
|
||||||
|
disabled={unEnroll.isPending}
|
||||||
|
onClick={() => onUnenrollRequested(props.factorId)}
|
||||||
|
>
|
||||||
|
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -212,20 +212,22 @@ function MultiFactorAuthSetupForm({
|
|||||||
name={'verificationCode'}
|
name={'verificationCode'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<div className={'flex space-x-2'}>
|
||||||
disabled={!verificationCodeForm.formState.isValid}
|
<Button type={'button'} variant={'ghost'} onClick={onCancel}>
|
||||||
type={'submit'}
|
<Trans i18nKey={'common:cancel'} />
|
||||||
>
|
</Button>
|
||||||
{state.loading ? (
|
|
||||||
<Trans i18nKey={'account:verifyingCode'} />
|
|
||||||
) : (
|
|
||||||
<Trans i18nKey={'account:enableMfaFactor'} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type={'button'} variant={'ghost'} onClick={onCancel}>
|
<Button
|
||||||
<Trans i18nKey={'common:cancel'} />
|
disabled={!verificationCodeForm.formState.isValid}
|
||||||
</Button>
|
type={'submit'}
|
||||||
|
>
|
||||||
|
{state.loading ? (
|
||||||
|
<Trans i18nKey={'account:verifyingCode'} />
|
||||||
|
) : (
|
||||||
|
<Trans i18nKey={'account:enableMfaFactor'} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -362,13 +364,15 @@ function FactorNameForm(
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type={'submit'}>
|
<div className={'flex space-x-2'}>
|
||||||
<Trans i18nKey={'account:factorNameSubmitLabel'} />
|
<Button type={'button'} variant={'ghost'} onClick={props.onCancel}>
|
||||||
</Button>
|
<Trans i18nKey={'common:cancel'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button type={'button'} variant={'ghost'} onClick={props.onCancel}>
|
<Button type={'submit'}>
|
||||||
<Trans i18nKey={'common:cancel'} />
|
<Trans i18nKey={'account:factorNameSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
usePersonalAccountData,
|
usePersonalAccountData,
|
||||||
useRevalidatePersonalAccountDataQuery,
|
useRevalidatePersonalAccountDataQuery,
|
||||||
@@ -8,7 +10,11 @@ import { UpdateAccountDetailsForm } from './update-account-details-form';
|
|||||||
|
|
||||||
export function UpdateAccountDetailsFormContainer() {
|
export function UpdateAccountDetailsFormContainer() {
|
||||||
const user = usePersonalAccountData();
|
const user = usePersonalAccountData();
|
||||||
const invalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
||||||
|
|
||||||
|
if (user.isLoading) {
|
||||||
|
return <LoadingOverlay fullPage={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.data) {
|
if (!user.data) {
|
||||||
return null;
|
return null;
|
||||||
@@ -18,7 +24,7 @@ export function UpdateAccountDetailsFormContainer() {
|
|||||||
<UpdateAccountDetailsForm
|
<UpdateAccountDetailsForm
|
||||||
displayName={user.data.name ?? ''}
|
displayName={user.data.name ?? ''}
|
||||||
userId={user.data.id}
|
userId={user.data.id}
|
||||||
onUpdate={invalidateUserDataQuery}
|
onUpdate={revalidateUserDataQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Logger } from '@kit/shared/logger';
|
|||||||
import { requireAuth } from '@kit/supabase/require-auth';
|
import { requireAuth } from '@kit/supabase/require-auth';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
|
||||||
import { PersonalAccountsService } from './services/personal-accounts.service';
|
import { DeletePersonalAccountService } from './services/delete-personal-account.service';
|
||||||
|
|
||||||
const emailSettings = getEmailSettingsFromEnvironment();
|
const emailSettings = getEmailSettingsFromEnvironment();
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export async function deletePersonalAccountAction(formData: FormData) {
|
|||||||
const userEmail = session.data.user.email ?? null;
|
const userEmail = session.data.user.email ?? null;
|
||||||
|
|
||||||
// create a new instance of the personal accounts service
|
// create a new instance of the personal accounts service
|
||||||
const service = new PersonalAccountsService(client);
|
const service = new DeletePersonalAccountService();
|
||||||
|
|
||||||
// delete the user's account and cancel all subscriptions
|
// delete the user's account and cancel all subscriptions
|
||||||
await service.deletePersonalAccount({
|
await service.deletePersonalAccount({
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { BillingGatewayService } from '@kit/billing-gateway';
|
import { AccountBillingService } from '@kit/billing-gateway';
|
||||||
import { Mailer } from '@kit/mailers';
|
import { Mailer } from '@kit/mailers';
|
||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name PersonalAccountsService
|
* @name DeletePersonalAccountService
|
||||||
* @description Service for managing accounts in the application
|
* @description Service for managing accounts in the application
|
||||||
* @param Database - The Supabase database type to use
|
* @param Database - The Supabase database type to use
|
||||||
* @example
|
* @example
|
||||||
* const client = getSupabaseClient();
|
* const client = getSupabaseClient();
|
||||||
* const accountsService = new AccountsService(client);
|
* const accountsService = new DeletePersonalAccountService();
|
||||||
*/
|
*/
|
||||||
export class PersonalAccountsService {
|
export class DeletePersonalAccountService {
|
||||||
private namespace = 'account';
|
private namespace = 'accounts.delete';
|
||||||
|
|
||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name deletePersonalAccount
|
* @name deletePersonalAccount
|
||||||
* Delete personal account of a user.
|
* Delete personal account of a user.
|
||||||
* This will delete the user from the authentication provider and cancel all subscriptions.
|
* This will delete the user from the authentication provider and cancel all subscriptions.
|
||||||
|
*
|
||||||
|
* Permissions are not checked here, as they are checked in the server action.
|
||||||
|
* USE WITH CAUTION. THE USER MUST HAVE THE NECESSARY PERMISSIONS.
|
||||||
*/
|
*/
|
||||||
async deletePersonalAccount(params: {
|
async deletePersonalAccount(params: {
|
||||||
adminClient: SupabaseClient<Database>;
|
adminClient: SupabaseClient<Database>;
|
||||||
@@ -39,6 +40,11 @@ export class PersonalAccountsService {
|
|||||||
'User requested deletion. Processing...',
|
'User requested deletion. Processing...',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Cancel all user subscriptions
|
||||||
|
const billingService = new AccountBillingService(params.adminClient);
|
||||||
|
|
||||||
|
await billingService.cancelAllAccountSubscriptions(params.userId);
|
||||||
|
|
||||||
// execute the deletion of the user
|
// execute the deletion of the user
|
||||||
try {
|
try {
|
||||||
await params.adminClient.auth.admin.deleteUser(params.userId);
|
await params.adminClient.auth.admin.deleteUser(params.userId);
|
||||||
@@ -55,17 +61,6 @@ export class PersonalAccountsService {
|
|||||||
throw new Error('Error deleting user');
|
throw new Error('Error deleting user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel all user subscriptions
|
|
||||||
try {
|
|
||||||
await this.cancelAllUserSubscriptions(params.userId);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error({
|
|
||||||
userId: params.userId,
|
|
||||||
error,
|
|
||||||
name: this.namespace,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send account deletion email
|
// Send account deletion email
|
||||||
if (params.userEmail) {
|
if (params.userEmail) {
|
||||||
try {
|
try {
|
||||||
@@ -117,53 +112,4 @@ export class PersonalAccountsService {
|
|||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async cancelAllUserSubscriptions(userId: string) {
|
|
||||||
Logger.info(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
name: this.namespace,
|
|
||||||
},
|
|
||||||
'Cancelling all subscriptions for user...',
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: subscriptions } = await this.client
|
|
||||||
.from('subscriptions')
|
|
||||||
.select('*')
|
|
||||||
.eq('account_id', userId);
|
|
||||||
|
|
||||||
const cancellationRequests = [];
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
subscriptions: subscriptions?.length ?? 0,
|
|
||||||
name: this.namespace,
|
|
||||||
},
|
|
||||||
'Cancelling subscriptions...',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const subscription of subscriptions ?? []) {
|
|
||||||
const gateway = new BillingGatewayService(subscription.billing_provider);
|
|
||||||
|
|
||||||
cancellationRequests.push(
|
|
||||||
gateway.cancelSubscription({
|
|
||||||
subscriptionId: subscription.id,
|
|
||||||
invoiceNow: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute all cancellation requests
|
|
||||||
await Promise.all(cancellationRequests);
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
subscriptions: subscriptions?.length ?? 0,
|
|
||||||
name: this.namespace,
|
|
||||||
},
|
|
||||||
'Subscriptions cancelled successfully',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
"@kit/ui": "0.1.0"
|
"@kit/ui": "0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/supabase": "^0.1.0",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/ui": "*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/supabase": "*",
|
"@kit/ui": "^0.1.0",
|
||||||
"@supabase/supabase-js": "2.40.0",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"lucide-react": "^0.363.0"
|
"lucide-react": "^0.363.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -16,13 +16,14 @@
|
|||||||
"./mfa": "./src/mfa.ts"
|
"./mfa": "./src/mfa.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/shared": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/supabase": "0.1.0",
|
"@kit/shared": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/ui": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
|
"@kit/ui": "workspace:*",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@tanstack/react-query": "5.28.6",
|
"@tanstack/react-query": "5.28.6",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
|
|||||||
@@ -12,18 +12,21 @@
|
|||||||
"./components": "./src/components/index.ts"
|
"./components": "./src/components/index.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/accounts": "*",
|
|
||||||
"@kit/email-templates": "*",
|
|
||||||
"@kit/eslint-config": "0.2.0",
|
|
||||||
"@kit/mailers": "*",
|
|
||||||
"@kit/prettier-config": "0.1.0",
|
|
||||||
"@kit/shared": "*",
|
|
||||||
"@kit/supabase": "*",
|
|
||||||
"@kit/tailwind-config": "0.1.0",
|
|
||||||
"@kit/tsconfig": "0.1.0",
|
|
||||||
"@kit/ui": "*",
|
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"lucide-react": "^0.363.0"
|
"@kit/accounts": "^0.1.0",
|
||||||
|
"@kit/billing-gateway": "workspace:*",
|
||||||
|
"@kit/email-templates": "^0.1.0",
|
||||||
|
"@kit/eslint-config": "workspace:*",
|
||||||
|
"@kit/mailers": "^0.1.0",
|
||||||
|
"@kit/prettier-config": "workspace:*",
|
||||||
|
"@kit/shared": "^0.1.0",
|
||||||
|
"@kit/supabase": "^0.1.0",
|
||||||
|
"@kit/tailwind-config": "workspace:*",
|
||||||
|
"@kit/tsconfig": "workspace:*",
|
||||||
|
"@kit/ui": "^0.1.0",
|
||||||
|
"@tanstack/react-query": "5.28.6",
|
||||||
|
"lucide-react": "^0.363.0",
|
||||||
|
"react-i18next": "^14.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@kit/accounts": "0.1.0",
|
"@kit/accounts": "0.1.0",
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
|
||||||
|
|
||||||
import { DeleteTeamAccountSchema } from '../schema/delete-team-account.schema';
|
|
||||||
import { DeleteTeamAccountService } from '../services/delete-team-account.service';
|
|
||||||
|
|
||||||
export async function deleteTeamAccountAction(formData: FormData) {
|
|
||||||
const body = Object.fromEntries(formData.entries());
|
|
||||||
const params = DeleteTeamAccountSchema.parse(body);
|
|
||||||
const client = getSupabaseServerActionClient();
|
|
||||||
const service = new DeleteTeamAccountService(client);
|
|
||||||
|
|
||||||
await service.deleteTeamAccount(params);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,13 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Dialog, DialogContent, DialogTitle } from '@kit/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@kit/ui/dialog';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -20,8 +26,8 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createOrganizationAccountAction } from '../actions/create-team-account-server-actions';
|
|
||||||
import { CreateTeamSchema } from '../schema/create-team.schema';
|
import { CreateTeamSchema } from '../schema/create-team.schema';
|
||||||
|
import { createOrganizationAccountAction } from '../server/actions/create-team-account-server-actions';
|
||||||
|
|
||||||
export function CreateTeamAccountDialog(
|
export function CreateTeamAccountDialog(
|
||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
@@ -31,18 +37,27 @@ export function CreateTeamAccountDialog(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent
|
||||||
<DialogTitle>
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||||
<Trans i18nKey={'teams:createOrganizationModalHeading'} />
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
</DialogTitle>
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans i18nKey={'teams:createTeamModalHeading'} />
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
<CreateOrganizationAccountForm />
|
<DialogDescription>
|
||||||
|
<Trans i18nKey={'teams:createTeamModalDescription'} />
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<CreateOrganizationAccountForm onClose={() => props.setIsOpen(false)} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateOrganizationAccountForm() {
|
function CreateOrganizationAccountForm(props: { onClose: () => void }) {
|
||||||
const [error, setError] = useState<boolean>();
|
const [error, setError] = useState<boolean>();
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
|
|
||||||
@@ -77,12 +92,12 @@ function CreateOrganizationAccountForm() {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'teams:organizationNameLabel'} />
|
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
data-test={'create-organization-name-input'}
|
data-test={'create-team-name-input'}
|
||||||
required
|
required
|
||||||
minLength={2}
|
minLength={2}
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
@@ -92,7 +107,7 @@ function CreateOrganizationAccountForm() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Your organization name should be unique and descriptive.
|
<Trans i18nKey={'teams:teamNameDescription'} />
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -101,12 +116,20 @@ function CreateOrganizationAccountForm() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<div className={'flex justify-end space-x-2'}>
|
||||||
data-test={'confirm-create-organization-button'}
|
<Button
|
||||||
disabled={pending}
|
variant={'outline'}
|
||||||
>
|
disabled={pending}
|
||||||
<Trans i18nKey={'teams:createOrganizationSubmitLabel'} />
|
type={'button'}
|
||||||
</Button>
|
onClick={props.onClose}
|
||||||
|
>
|
||||||
|
<Trans i18nKey={'common:cancel'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button data-test={'confirm-create-team-button'} disabled={pending}>
|
||||||
|
<Trans i18nKey={'teams:createTeamSubmitLabel'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -117,11 +140,11 @@ function CreateOrganizationErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'teams:createOrganizationErrorHeading'} />
|
<Trans i18nKey={'teams:createTeamErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'teams:createOrganizationErrorMessage'} />
|
<Trans i18nKey={'teams:createTeamErrorMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export * from './members/account-members-table';
|
export * from './members/account-members-table';
|
||||||
export * from './update-organization-form';
|
|
||||||
export * from './members/invite-members-dialog-container';
|
export * from './members/invite-members-dialog-container';
|
||||||
export * from './team-account-danger-zone';
|
export * from './settings/team-account-danger-zone';
|
||||||
export * from './invitations/account-invitations-table';
|
export * from './invitations/account-invitations-table';
|
||||||
|
export * from './settings/team-account-settings-container';
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||||
|
|
||||||
import { RoleBadge } from '../role-badge';
|
import { RoleBadge } from '../members/role-badge';
|
||||||
import { DeleteInvitationDialog } from './delete-invitation-dialog';
|
import { DeleteInvitationDialog } from './delete-invitation-dialog';
|
||||||
import { UpdateInvitationDialog } from './update-invitation-dialog';
|
import { UpdateInvitationDialog } from './update-invitation-dialog';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { deleteInvitationAction } from '../../actions/account-invitations-server-actions';
|
import { deleteInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||||
|
|
||||||
export const DeleteInvitationDialog: React.FC<{
|
export const DeleteInvitationDialog: React.FC<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -24,7 +24,7 @@ export const DeleteInvitationDialog: React.FC<{
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey="organization:deleteInvitationDialogTitle" />
|
<Trans i18nKey="team:deleteInvitationDialogTitle" />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import {
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { updateInvitationAction } from '../../actions/account-invitations-server-actions';
|
|
||||||
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
||||||
import { MembershipRoleSelector } from '../membership-role-selector';
|
import { updateInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||||
|
import { MembershipRoleSelector } from '../members/membership-role-selector';
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
type Role = Database['public']['Enums']['account_role'];
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||||
|
|
||||||
import { RoleBadge } from '../role-badge';
|
|
||||||
import { RemoveMemberDialog } from './remove-member-dialog';
|
import { RemoveMemberDialog } from './remove-member-dialog';
|
||||||
|
import { RoleBadge } from './role-badge';
|
||||||
import { TransferOwnershipDialog } from './transfer-ownership-dialog';
|
import { TransferOwnershipDialog } from './transfer-ownership-dialog';
|
||||||
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
|
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ import {
|
|||||||
} from '@kit/ui/tooltip';
|
} from '@kit/ui/tooltip';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createInvitationsAction } from '../../actions/account-invitations-server-actions';
|
|
||||||
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||||
import { MembershipRoleSelector } from '../membership-role-selector';
|
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
||||||
|
import { MembershipRoleSelector } from './membership-role-selector';
|
||||||
|
|
||||||
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
||||||
|
|
||||||
@@ -59,8 +59,7 @@ export function InviteMembersDialogContainer({
|
|||||||
<DialogTitle>Invite Members to Organization</DialogTitle>
|
<DialogTitle>Invite Members to Organization</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Invite members to your organization by entering their email and
|
Invite members to your team by entering their email and role.
|
||||||
role.
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -89,7 +88,7 @@ function InviteMembersForm({
|
|||||||
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('organization');
|
const { t } = useTranslation('team');
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(InviteMembersSchema),
|
resolver: zodResolver(InviteMembersSchema),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { removeMemberFromAccountAction } from '../../actions/account-members-server-actions';
|
import { removeMemberFromAccountAction } from '../../server/actions/team-members-server-actions';
|
||||||
|
|
||||||
export const RemoveMemberDialog: React.FC<{
|
export const RemoveMemberDialog: React.FC<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -25,11 +25,11 @@ export const RemoveMemberDialog: React.FC<{
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey="organization:removeMemberModalHeading" />
|
<Trans i18nKey="team:removeMemberModalHeading" />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Remove this member from the organization.
|
Remove this member from the team.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { transferOwnershipAction } from '../../actions/account-members-server-actions';
|
|
||||||
import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema';
|
import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema';
|
||||||
|
import { transferOwnershipAction } from '../../server/actions/team-members-server-actions';
|
||||||
|
|
||||||
export const TransferOwnershipDialog: React.FC<{
|
export const TransferOwnershipDialog: React.FC<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -42,11 +42,11 @@ export const TransferOwnershipDialog: React.FC<{
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey="organization:transferOwnership" />
|
<Trans i18nKey="team:transferOwnership" />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Transfer ownership of the organization to another member.
|
Transfer ownership of the team to another member.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import {
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { updateMemberRoleAction } from '../../actions/account-members-server-actions';
|
|
||||||
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
||||||
import { MembershipRoleSelector } from '../membership-role-selector';
|
import { updateMemberRoleAction } from '../../server/actions/team-members-server-actions';
|
||||||
|
import { MembershipRoleSelector } from './membership-role-selector';
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
type Role = Database['public']['Enums']['account_role'];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useFormStatus } from 'react-dom';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@kit/ui/alert-dialog';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@kit/ui/form';
|
||||||
|
import { Input } from '@kit/ui/input';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { deleteTeamAccountAction } from '../../server/actions/delete-team-account-server-actions';
|
||||||
|
import { leaveTeamAccountAction } from '../../server/actions/leave-team-account-server-actions';
|
||||||
|
|
||||||
|
export function TeamAccountDangerZone({
|
||||||
|
account,
|
||||||
|
userIsPrimaryOwner,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
account: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
userIsPrimaryOwner: boolean;
|
||||||
|
}>) {
|
||||||
|
if (userIsPrimaryOwner) {
|
||||||
|
return <DeleteTeamContainer account={account} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LeaveTeamContainer account={account} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteTeamContainer(props: {
|
||||||
|
account: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col space-y-4'}>
|
||||||
|
<div className={'flex flex-col space-y-1'}>
|
||||||
|
<span className={'font-medium'}>
|
||||||
|
<Trans i18nKey={'teams:deleteTeam'} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p className={'text-sm text-gray-500'}>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'teams:deleteTeamDescription'}
|
||||||
|
values={{
|
||||||
|
teamName: props.account.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
data-test={'delete-team-button'}
|
||||||
|
type={'button'}
|
||||||
|
variant={'destructive'}
|
||||||
|
>
|
||||||
|
<Trans i18nKey={'teams:deleteTeam'} />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
|
||||||
|
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
<Trans i18nKey={'teams:deletingTeam'} />
|
||||||
|
</AlertDialogTitle>
|
||||||
|
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'teams:deletingTeamDescription'}
|
||||||
|
values={{
|
||||||
|
teamName: props.account.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<DeleteTeamConfirmationForm
|
||||||
|
name={props.account.name}
|
||||||
|
id={props.account.id}
|
||||||
|
/>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteTeamConfirmationForm({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}) {
|
||||||
|
const form = useForm({
|
||||||
|
mode: 'onChange',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
|
resolver: zodResolver(
|
||||||
|
z.object({
|
||||||
|
name: z.string().refine((value) => value === name, {
|
||||||
|
message: 'Name does not match',
|
||||||
|
path: ['name'],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary fallback={<DeleteTeamErrorAlert />}>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className={'flex flex-col space-y-4'}
|
||||||
|
action={deleteTeamAccountAction}
|
||||||
|
>
|
||||||
|
<div className={'flex flex-col space-y-2'}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'border-2 border-red-500 p-4 text-sm text-red-500' +
|
||||||
|
' flex flex-col space-y-2'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'teams:deleteTeamDisclaimer'}
|
||||||
|
values={{
|
||||||
|
teamName: name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'text-sm'}>
|
||||||
|
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" value={id} name={'accountId'} />
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'teams:teamNameInputLabel'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
data-test={'delete-team-input-field'}
|
||||||
|
required
|
||||||
|
type={'text'}
|
||||||
|
className={'w-full'}
|
||||||
|
placeholder={''}
|
||||||
|
pattern={name}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
<Trans i18nKey={'teams:deleteTeamInputField'} />
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
name={'confirm'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
<Trans i18nKey={'common:cancel'} />
|
||||||
|
</AlertDialogCancel>
|
||||||
|
|
||||||
|
<DeleteTeamSubmitButton />
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteTeamSubmitButton() {
|
||||||
|
const { pending } = useFormStatus();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-test={'confirm-delete-team-button'}
|
||||||
|
disabled={pending}
|
||||||
|
variant={'destructive'}
|
||||||
|
>
|
||||||
|
<Trans i18nKey={'teams:deleteTeam'} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LeaveTeamContainer(props: {
|
||||||
|
account: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col space-y-4'}>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'teams:leaveTeamDescription'}
|
||||||
|
values={{
|
||||||
|
teamName: props.account.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
data-test={'leave-team-button'}
|
||||||
|
type={'button'}
|
||||||
|
variant={'destructive'}
|
||||||
|
>
|
||||||
|
<Trans i18nKey={'teams:leaveTeam'} />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
<Trans i18nKey={'teams:leavingTeamModalHeading'} />
|
||||||
|
</AlertDialogTitle>
|
||||||
|
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Trans i18nKey={'teams:leavingTeamModalDescription'} />
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<ErrorBoundary fallback={<LeaveTeamErrorAlert />}>
|
||||||
|
<form action={leaveTeamAccountAction}>
|
||||||
|
<input type={'hidden'} value={props.account.id} name={'id'} />
|
||||||
|
|
||||||
|
<div className={'my-2 flex flex-col space-y-4'}>
|
||||||
|
<Trans
|
||||||
|
i18nKey={'teams:leaveTeamDisclaimer'}
|
||||||
|
values={{
|
||||||
|
teamName: props.account?.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</AlertDialogContent>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
<Trans i18nKey={'common:cancel'} />
|
||||||
|
</AlertDialogCancel>
|
||||||
|
|
||||||
|
<LeaveTeamSubmitButton />
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LeaveTeamSubmitButton() {
|
||||||
|
const { pending } = useFormStatus();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-test={'confirm-leave-organization-button'}
|
||||||
|
disabled={pending}
|
||||||
|
variant={'destructive'}
|
||||||
|
>
|
||||||
|
<Trans i18nKey={'teams:leaveOrganization'} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LeaveTeamErrorAlert() {
|
||||||
|
return (
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans i18nKey={'teams:leaveTeamErrorHeading'} />
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans i18nKey={'common:genericError'} />
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteTeamErrorAlert() {
|
||||||
|
return (
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans i18nKey={'teams:deleteTeamErrorHeading'} />
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans i18nKey={'common:genericError'} />
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@kit/ui/card';
|
||||||
|
|
||||||
|
import { TeamAccountDangerZone } from './team-account-danger-zone';
|
||||||
|
import { UpdateTeamAccountImage } from './update-team-account-image-container';
|
||||||
|
import { UpdateTeamAccountNameForm } from './update-team-account-name-form';
|
||||||
|
|
||||||
|
export function TeamAccountSettingsContainer(props: {
|
||||||
|
account: {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
pictureUrl: string | null;
|
||||||
|
primaryOwnerUserId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
paths: {
|
||||||
|
teamAccountSettings: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={'flex flex-col space-y-8'}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Team Logo</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
Update your team's logo to make it easier to identify
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<UpdateTeamAccountImage account={props.account} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Team Account Settings</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>Manage your team account settings</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<UpdateTeamAccountNameForm
|
||||||
|
path={props.paths.teamAccountSettings}
|
||||||
|
account={props.account}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={'border-destructive border-2'}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Danger Zone</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
Please be careful when making changes in this section as they are
|
||||||
|
irreversible.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<TeamAccountDangerZone
|
||||||
|
userIsPrimaryOwner={
|
||||||
|
props.userId === props.account.primaryOwnerUserId
|
||||||
|
}
|
||||||
|
account={props.account}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
|
import { ImageUploader } from '@kit/ui/image-uploader';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
const AVATARS_BUCKET = 'account_image';
|
||||||
|
|
||||||
|
export function UpdateTeamAccountImage(props: {
|
||||||
|
account: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
pictureUrl: string | null;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const client = useSupabase();
|
||||||
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
|
const createToaster = useCallback(
|
||||||
|
(promise: () => Promise<unknown>) => {
|
||||||
|
return toast.promise(promise, {
|
||||||
|
success: t(`updateTeamSuccessMessage`),
|
||||||
|
error: t(`updateTeamErrorMessage`),
|
||||||
|
loading: t(`updateTeamLoadingMessage`),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onValueChange = useCallback(
|
||||||
|
(file: File | null) => {
|
||||||
|
const removeExistingStorageFile = () => {
|
||||||
|
if (props.account.pictureUrl) {
|
||||||
|
return (
|
||||||
|
deleteProfilePhoto(client, props.account.pictureUrl) ??
|
||||||
|
Promise.resolve()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const promise = () =>
|
||||||
|
removeExistingStorageFile().then(() =>
|
||||||
|
uploadUserProfilePhoto(client, file, props.account.id).then(
|
||||||
|
(pictureUrl) => {
|
||||||
|
return client
|
||||||
|
.from('accounts')
|
||||||
|
.update({
|
||||||
|
picture_url: pictureUrl,
|
||||||
|
})
|
||||||
|
.eq('id', props.account.id)
|
||||||
|
.throwOnError();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
createToaster(promise);
|
||||||
|
} else {
|
||||||
|
const promise = () =>
|
||||||
|
removeExistingStorageFile().then(() => {
|
||||||
|
return client
|
||||||
|
.from('accounts')
|
||||||
|
.update({
|
||||||
|
picture_url: null,
|
||||||
|
})
|
||||||
|
.eq('id', props.account.id)
|
||||||
|
.throwOnError();
|
||||||
|
});
|
||||||
|
|
||||||
|
createToaster(promise);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, createToaster, props],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageUploader
|
||||||
|
value={props.account.pictureUrl}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
>
|
||||||
|
<div className={'flex flex-col space-y-1'}>
|
||||||
|
<span className={'text-sm'}>
|
||||||
|
<Trans i18nKey={'account:profilePictureHeading'} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className={'text-xs'}>
|
||||||
|
<Trans i18nKey={'account:profilePictureSubheading'} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ImageUploader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteProfilePhoto(client: SupabaseClient, url: string) {
|
||||||
|
const bucket = client.storage.from(AVATARS_BUCKET);
|
||||||
|
const fileName = url.split('/').pop()?.split('?')[0];
|
||||||
|
|
||||||
|
if (!fileName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.remove([fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadUserProfilePhoto(
|
||||||
|
client: SupabaseClient,
|
||||||
|
photoFile: File,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
const bytes = await photoFile.arrayBuffer();
|
||||||
|
const bucket = client.storage.from(AVATARS_BUCKET);
|
||||||
|
const extension = photoFile.name.split('.').pop();
|
||||||
|
const fileName = await getAvatarFileName(userId, extension);
|
||||||
|
|
||||||
|
const result = await bucket.upload(fileName, bytes);
|
||||||
|
|
||||||
|
if (!result.error) {
|
||||||
|
return bucket.getPublicUrl(fileName).data.publicUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvatarFileName(
|
||||||
|
userId: string,
|
||||||
|
extension: string | undefined,
|
||||||
|
) {
|
||||||
|
const { nanoid } = await import('nanoid');
|
||||||
|
const uniqueId = nanoid(16);
|
||||||
|
|
||||||
|
return `${userId}.${extension}?v=${uniqueId}`;
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useTransition } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useUpdateAccountData } from '@kit/accounts/hooks/use-update-account';
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -20,44 +17,42 @@ import {
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { updateTeamAccountName } from '../../server/actions/team-details-server-actions';
|
||||||
|
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateOrganizationForm = (props: {
|
export const UpdateTeamAccountNameForm = (props: {
|
||||||
accountId: string;
|
account: {
|
||||||
accountName: string;
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
path: string;
|
||||||
}) => {
|
}) => {
|
||||||
const updateAccountData = useUpdateAccountData(props.accountId);
|
const [pending, startTransition] = useTransition();
|
||||||
const { t } = useTranslation('organization');
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: props.accountName,
|
name: props.account.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateOrganizationData = useCallback(
|
|
||||||
(data: { name: string }) => {
|
|
||||||
const promise = updateAccountData.mutateAsync(data);
|
|
||||||
|
|
||||||
toast.promise(promise, {
|
|
||||||
loading: t(`updateOrganizationLoadingMessage`),
|
|
||||||
success: t(`updateOrganizationSuccessMessage`),
|
|
||||||
error: t(`updateOrganizationErrorMessage`),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[t, updateAccountData],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'space-y-8'}>
|
<div className={'space-y-8'}>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
className={'flex flex-col space-y-4'}
|
className={'flex flex-col space-y-4'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
updateOrganizationData(data);
|
startTransition(async () => {
|
||||||
|
await updateTeamAccountName({
|
||||||
|
slug: props.account.slug,
|
||||||
|
name: data.name,
|
||||||
|
path: props.path,
|
||||||
|
});
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -66,12 +61,12 @@ export const UpdateOrganizationForm = (props: {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'teams:organizationNameInputLabel'} />
|
<Trans i18nKey={'teams:teamNameInputLabel'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
data-test={'organization-name-input'}
|
data-test={'team-name-input'}
|
||||||
required
|
required
|
||||||
placeholder={''}
|
placeholder={''}
|
||||||
{...field}
|
{...field}
|
||||||
@@ -80,15 +75,15 @@ export const UpdateOrganizationForm = (props: {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
></FormField>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className={'w-full md:w-auto'}
|
className={'w-full md:w-auto'}
|
||||||
data-test={'update-organization-submit-button'}
|
data-test={'update-team-submit-button'}
|
||||||
disabled={updateAccountData.isPending}
|
disabled={pending}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'teams:updateOrganizationSubmitLabel'} />
|
<Trans i18nKey={'teams:updateTeamSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useFormStatus } from 'react-dom';
|
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@kit/ui/dialog';
|
|
||||||
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
|
||||||
import { Heading } from '@kit/ui/heading';
|
|
||||||
import { Input } from '@kit/ui/input';
|
|
||||||
import { Label } from '@kit/ui/label';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
import { deleteTeamAccountAction } from '../actions/delete-team-account-server-actions';
|
|
||||||
import { leaveTeamAccountAction } from '../actions/leave-team-account-server-actions';
|
|
||||||
|
|
||||||
type AccountData =
|
|
||||||
Database['public']['Functions']['organization_account_workspace']['Returns'][0];
|
|
||||||
|
|
||||||
export function TeamAccountDangerZone({
|
|
||||||
account,
|
|
||||||
userId,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
account: AccountData;
|
|
||||||
userId: string;
|
|
||||||
}>) {
|
|
||||||
const isPrimaryOwner = userId === account.primary_owner_user_id;
|
|
||||||
|
|
||||||
if (isPrimaryOwner) {
|
|
||||||
return <DeleteOrganizationContainer account={account} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <LeaveOrganizationContainer account={account} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteOrganizationContainer(props: { account: AccountData }) {
|
|
||||||
return (
|
|
||||||
<div className={'flex flex-col space-y-4'}>
|
|
||||||
<div className={'flex flex-col space-y-1'}>
|
|
||||||
<Heading level={6}>
|
|
||||||
<Trans i18nKey={'teams:deleteOrganization'} />
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<p className={'text-sm text-gray-500'}>
|
|
||||||
<Trans
|
|
||||||
i18nKey={'teams:deleteOrganizationDescription'}
|
|
||||||
values={{
|
|
||||||
organizationName: props.account.name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger>
|
|
||||||
<Button
|
|
||||||
data-test={'delete-organization-button'}
|
|
||||||
type={'button'}
|
|
||||||
variant={'destructive'}
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'teams:deleteOrganization'} />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
<Trans i18nKey={'teams:deletingOrganization'} />
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<DeleteOrganizationForm
|
|
||||||
name={props.account.name}
|
|
||||||
id={props.account.id}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteOrganizationForm({ name, id }: { name: string; id: string }) {
|
|
||||||
return (
|
|
||||||
<ErrorBoundary fallback={<DeleteOrganizationErrorAlert />}>
|
|
||||||
<form
|
|
||||||
className={'flex flex-col space-y-4'}
|
|
||||||
action={deleteTeamAccountAction}
|
|
||||||
>
|
|
||||||
<div className={'flex flex-col space-y-2'}>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'border-2 border-red-500 p-4 text-sm text-red-500' +
|
|
||||||
' flex flex-col space-y-2'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Trans
|
|
||||||
i18nKey={'teams:deleteOrganizationDisclaimer'}
|
|
||||||
values={{
|
|
||||||
organizationName: name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'text-sm'}>
|
|
||||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="hidden" value={id} name={'id'} />
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
<Trans i18nKey={'teams:organizationNameInputLabel'} />
|
|
||||||
|
|
||||||
<Input
|
|
||||||
name={'name'}
|
|
||||||
data-test={'delete-organization-input-field'}
|
|
||||||
required
|
|
||||||
type={'text'}
|
|
||||||
className={'w-full'}
|
|
||||||
placeholder={''}
|
|
||||||
pattern={name}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={'text-xs'}>
|
|
||||||
<Trans i18nKey={'teams:deleteOrganizationInputField'} />
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex justify-end space-x-2.5'}>
|
|
||||||
<DeleteOrganizationSubmitButton />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteOrganizationSubmitButton() {
|
|
||||||
const { pending } = useFormStatus();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-test={'confirm-delete-organization-button'}
|
|
||||||
disabled={pending}
|
|
||||||
variant={'destructive'}
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'teams:deleteOrganization'} />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LeaveOrganizationContainer(props: { account: AccountData }) {
|
|
||||||
return (
|
|
||||||
<div className={'flex flex-col space-y-4'}>
|
|
||||||
<p>
|
|
||||||
<Trans
|
|
||||||
i18nKey={'teams:leaveOrganizationDescription'}
|
|
||||||
values={{
|
|
||||||
organizationName: props.account.name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger>
|
|
||||||
<Button
|
|
||||||
data-test={'leave-organization-button'}
|
|
||||||
type={'button'}
|
|
||||||
variant={'destructive'}
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'teams:leaveOrganization'} />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
<Trans i18nKey={'teams:leavingOrganizationModalHeading'} />
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<ErrorBoundary fallback={<LeaveOrganizationErrorAlert />}>
|
|
||||||
<form action={leaveTeamAccountAction}>
|
|
||||||
<input type={'hidden'} value={props.account.id} name={'id'} />
|
|
||||||
|
|
||||||
<div className={'flex flex-col space-y-4'}>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Trans
|
|
||||||
i18nKey={'teams:leaveOrganizationDisclaimer'}
|
|
||||||
values={{
|
|
||||||
organizationName: props.account?.name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex justify-end space-x-2.5'}>
|
|
||||||
<LeaveOrganizationSubmitButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LeaveOrganizationSubmitButton() {
|
|
||||||
const { pending } = useFormStatus();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-test={'confirm-leave-organization-button'}
|
|
||||||
disabled={pending}
|
|
||||||
variant={'destructive'}
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'teams:leaveOrganization'} />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LeaveOrganizationErrorAlert() {
|
|
||||||
return (
|
|
||||||
<Alert variant={'destructive'}>
|
|
||||||
<AlertTitle>
|
|
||||||
<Trans i18nKey={'teams:leaveOrganizationErrorHeading'} />
|
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription>
|
|
||||||
<Trans i18nKey={'common:genericError'} />
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteOrganizationErrorAlert() {
|
|
||||||
return (
|
|
||||||
<Alert variant={'destructive'}>
|
|
||||||
<AlertTitle>
|
|
||||||
<Trans i18nKey={'teams:deleteOrganizationErrorHeading'} />
|
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription>
|
|
||||||
<Trans i18nKey={'common:genericError'} />
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import { Logger } from '@kit/shared/logger';
|
|||||||
import { requireAuth } from '@kit/supabase/require-auth';
|
import { requireAuth } from '@kit/supabase/require-auth';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
|
||||||
import { CreateTeamSchema } from '../schema/create-team.schema';
|
import { CreateTeamSchema } from '../../schema/create-team.schema';
|
||||||
import { CreateTeamAccountService } from '../services/create-team-account.service';
|
import { CreateTeamAccountService } from '../services/create-team-account.service';
|
||||||
|
|
||||||
const TEAM_ACCOUNTS_HOME_PATH = z
|
const TEAM_ACCOUNTS_HOME_PATH = z
|
||||||
@@ -45,10 +45,10 @@ export async function createOrganizationAccountAction(
|
|||||||
error: createAccountResponse.error,
|
error: createAccountResponse.error,
|
||||||
name: 'accounts',
|
name: 'accounts',
|
||||||
},
|
},
|
||||||
`Error creating organization account`,
|
`Error creating team account`,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error('Error creating organization account');
|
throw new Error('Error creating team account');
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountHomePath =
|
const accountHomePath =
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
import { requireAuth } from '@kit/supabase/require-auth';
|
||||||
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
|
||||||
|
import { DeleteTeamAccountSchema } from '../../schema/delete-team-account.schema';
|
||||||
|
import { DeleteTeamAccountService } from '../services/delete-team-account.service';
|
||||||
|
|
||||||
|
export async function deleteTeamAccountAction(formData: FormData) {
|
||||||
|
const params = DeleteTeamAccountSchema.parse(
|
||||||
|
Object.fromEntries(formData.entries()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = getSupabaseServerActionClient();
|
||||||
|
|
||||||
|
// Check if the user has the necessary permissions to delete the team account
|
||||||
|
await assertUserPermissionsToDeleteTeamAccount(client, params.accountId);
|
||||||
|
|
||||||
|
// Get the Supabase client and create a new service instance.
|
||||||
|
const service = new DeleteTeamAccountService();
|
||||||
|
|
||||||
|
// Delete the team account and all associated data.
|
||||||
|
await service.deleteTeamAccount(
|
||||||
|
getSupabaseServerActionClient({
|
||||||
|
admin: true,
|
||||||
|
}),
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect('/home');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertUserPermissionsToDeleteTeamAccount(
|
||||||
|
client: SupabaseClient<Database>,
|
||||||
|
accountId: string,
|
||||||
|
) {
|
||||||
|
const auth = await requireAuth(client);
|
||||||
|
|
||||||
|
if (auth.error ?? !auth.data.user.id) {
|
||||||
|
throw new Error('Authentication required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = auth.data.user.id;
|
||||||
|
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('accounts')
|
||||||
|
.select('id')
|
||||||
|
.eq('primary_owner_user_id', userId)
|
||||||
|
.eq('is_personal_account', false)
|
||||||
|
.eq('id', accountId);
|
||||||
|
|
||||||
|
if (error ?? !data) {
|
||||||
|
throw new Error('Account not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
|
||||||
import { LeaveTeamAccountSchema } from '../schema/leave-team-account.schema';
|
import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema';
|
||||||
import { LeaveAccountService } from '../services/leave-account.service';
|
import { LeaveAccountService } from '../services/leave-account.service';
|
||||||
|
|
||||||
export async function leaveTeamAccountAction(formData: FormData) {
|
export async function leaveTeamAccountAction(formData: FormData) {
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
|
|
||||||
|
export async function updateTeamAccountName(params: {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
path: string;
|
||||||
|
}) {
|
||||||
|
const client = getSupabaseServerComponentClient();
|
||||||
|
|
||||||
|
const { error, data } = await client
|
||||||
|
.from('accounts')
|
||||||
|
.update({
|
||||||
|
name: params.name,
|
||||||
|
slug: params.slug,
|
||||||
|
})
|
||||||
|
.match({
|
||||||
|
slug: params.slug,
|
||||||
|
})
|
||||||
|
.select('slug')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSlug = data.slug;
|
||||||
|
|
||||||
|
if (newSlug) {
|
||||||
|
const path = params.path.replace('[account]', newSlug);
|
||||||
|
|
||||||
|
redirect(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@ import { z } from 'zod';
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
|
||||||
import { DeleteInvitationSchema } from '../schema/delete-invitation.schema';
|
import { DeleteInvitationSchema } from '../../schema/delete-invitation.schema';
|
||||||
import { InviteMembersSchema } from '../schema/invite-members.schema';
|
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||||
import { UpdateInvitationSchema } from '../schema/update-invitation-schema';
|
import { UpdateInvitationSchema } from '../../schema/update-invitation-schema';
|
||||||
import { AccountInvitationsService } from '../services/account-invitations.service';
|
import { AccountInvitationsService } from '../services/account-invitations.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,9 +7,9 @@ import { Mailer } from '@kit/mailers';
|
|||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
import { DeleteInvitationSchema } from '../schema/delete-invitation.schema';
|
import { DeleteInvitationSchema } from '../../schema/delete-invitation.schema';
|
||||||
import { InviteMembersSchema } from '../schema/invite-members.schema';
|
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||||
import { UpdateInvitationSchema } from '../schema/update-invitation-schema';
|
import { UpdateInvitationSchema } from '../../schema/update-invitation-schema';
|
||||||
|
|
||||||
const invitePath = process.env.INVITATION_PAGE_PATH;
|
const invitePath = process.env.INVITATION_PAGE_PATH;
|
||||||
const siteURL = process.env.NEXT_PUBLIC_SITE_URL;
|
const siteURL = process.env.NEXT_PUBLIC_SITE_URL;
|
||||||
@@ -149,16 +149,14 @@ export class AccountInvitationsService {
|
|||||||
for (const invitation of responseInvitations) {
|
for (const invitation of responseInvitations) {
|
||||||
const promise = async () => {
|
const promise = async () => {
|
||||||
try {
|
try {
|
||||||
const { renderInviteEmail } = await import(
|
const { renderInviteEmail } = await import('@kit/email-templates');
|
||||||
'../../../../email-templates'
|
|
||||||
);
|
|
||||||
|
|
||||||
const html = renderInviteEmail({
|
const html = renderInviteEmail({
|
||||||
link: this.getInvitationLink(invitation.invite_token),
|
link: this.getInvitationLink(invitation.invite_token),
|
||||||
invitedUserEmail: invitation.email,
|
invitedUserEmail: invitation.email,
|
||||||
inviter: user.email,
|
inviter: user.email,
|
||||||
productName: env.productName,
|
productName: env.productName,
|
||||||
organizationName: accountResponse.data.name,
|
teamName: accountResponse.data.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
await mailer.sendEmail({
|
await mailer.sendEmail({
|
||||||
@@ -11,7 +11,7 @@ export class CreateTeamAccountService {
|
|||||||
createNewOrganizationAccount(params: { name: string; userId: string }) {
|
createNewOrganizationAccount(params: { name: string; userId: string }) {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
{ ...params, namespace: this.namespace },
|
{ ...params, namespace: this.namespace },
|
||||||
`Creating new organization account...`,
|
`Creating new team account...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.client.rpc('create_account', {
|
return this.client.rpc('create_account', {
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import 'server-only';
|
||||||
|
|
||||||
|
import { AccountBillingService } from '@kit/billing-gateway';
|
||||||
|
import { Logger } from '@kit/shared/logger';
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
export class DeleteTeamAccountService {
|
||||||
|
private readonly namespace = 'accounts.delete';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a team account. Permissions are not checked here, as they are
|
||||||
|
* checked in the server action.
|
||||||
|
*
|
||||||
|
* USE WITH CAUTION. THE USER MUST HAVE THE NECESSARY PERMISSIONS.
|
||||||
|
*
|
||||||
|
* @param adminClient
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
async deleteTeamAccount(
|
||||||
|
adminClient: SupabaseClient<Database>,
|
||||||
|
params: { accountId: string },
|
||||||
|
) {
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId: params.accountId,
|
||||||
|
},
|
||||||
|
`Requested team account deletion. Processing...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId: params.accountId,
|
||||||
|
},
|
||||||
|
`Deleting all account subscriptions...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// First - we want to cancel all Stripe active subscriptions
|
||||||
|
const billingService = new AccountBillingService(adminClient);
|
||||||
|
|
||||||
|
await billingService.cancelAllAccountSubscriptions(params.accountId);
|
||||||
|
|
||||||
|
// now we can use the admin client to delete the account.
|
||||||
|
const { error } = await adminClient
|
||||||
|
.from('accounts')
|
||||||
|
.delete()
|
||||||
|
.eq('id', params.accountId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId: params.accountId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
'Failed to delete team account',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error('Failed to delete team account');
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId: params.accountId,
|
||||||
|
},
|
||||||
|
'Successfully deleted team account',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
import 'server-only';
|
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
|
|
||||||
export class DeleteTeamAccountService {
|
|
||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
|
||||||
|
|
||||||
async deleteTeamAccount(params: { accountId: string }) {
|
|
||||||
// TODO
|
|
||||||
// implement this method
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,11 +16,11 @@
|
|||||||
"./provider": "./src/I18nProvider.tsx"
|
"./provider": "./src/I18nProvider.tsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/shared": "^0.1.0",
|
"@kit/shared": "workspace:^",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"i18next": "^23.10.1",
|
"i18next": "^23.10.1",
|
||||||
"i18next-browser-languagedetector": "7.2.0",
|
"i18next-browser-languagedetector": "7.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
"nodemailer": "^6.9.13"
|
"nodemailer": "^6.9.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@types/nodemailer": "6.4.14"
|
"@types/nodemailer": "6.4.14"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
"pino": "^8.19.0"
|
"pino": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0"
|
"@kit/tsconfig": "workspace:*"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|||||||
@@ -22,18 +22,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stripe/react-stripe-js": "^2.6.2",
|
"@stripe/react-stripe-js": "^2.6.2",
|
||||||
"@stripe/stripe-js": "^3.0.10",
|
"@stripe/stripe-js": "^3.1.0",
|
||||||
"stripe": "^14.21.0"
|
"stripe": "^14.22.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/billing": "*",
|
"@kit/billing": "^0.1.0",
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/shared": "*",
|
"@kit/shared": "^0.1.0",
|
||||||
"@kit/supabase": "*",
|
"@kit/supabase": "^0.1.0",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "*"
|
"@kit/ui": "^0.1.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|||||||
@@ -24,10 +24,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@epic-web/invariant": "^1.0.0",
|
"@epic-web/invariant": "^1.0.0",
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@supabase/ssr": "^0.1.0",
|
"@supabase/ssr": "^0.1.0",
|
||||||
"@supabase/supabase-js": "^2.40.0",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"@tanstack/react-query": "5.28.6"
|
"@tanstack/react-query": "5.28.6"
|
||||||
|
|||||||
@@ -28,10 +28,10 @@
|
|||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "1.0.7",
|
"@radix-ui/react-tooltip": "1.0.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "1.0.0",
|
||||||
"input-otp": "1.2.3",
|
"input-otp": "1.2.3",
|
||||||
"react-top-loading-bar": "2.3.1",
|
"react-top-loading-bar": "2.3.1",
|
||||||
"tailwind-merge": "^2.2.0"
|
"tailwind-merge": "^2.2.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@radix-ui/react-icons": "1.3.0",
|
"@radix-ui/react-icons": "1.3.0",
|
||||||
@@ -45,26 +45,26 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@tanstack/react-table": "^8.11.3",
|
"@tanstack/react-table": "^8.15.0",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.73",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.22",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"date-fns": "^3.2.0",
|
"date-fns": "^3.6.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"react-day-picker": "^8.10.0",
|
"react-day-picker": "^8.10.0",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.2",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.3",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
1544
pnpm-lock.yaml
generated
1544
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,6 @@
|
|||||||
"typegen": "supabase gen types typescript --local > ../packages/supabase/src/database.types.ts"
|
"typegen": "supabase gen types typescript --local > ../packages/supabase/src/database.types.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"supabase": "^1.150.0"
|
"supabase": "^1.151.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,23 +15,23 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/eslint-plugin-next": "^14.1.0",
|
"@next/eslint-plugin-next": "^14.1.4",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@types/eslint": "^8.56.2",
|
"@types/eslint": "^8.56.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||||
"@typescript-eslint/parser": "^7.3.1",
|
"@typescript-eslint/parser": "^7.4.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-config-turbo": "^1.12.5",
|
"eslint-config-turbo": "^1.13.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0"
|
"eslint-plugin-react-hooks": "^4.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/prettier-config": "^0.1.0",
|
"@kit/prettier-config": "workspace:^",
|
||||||
"@kit/tsconfig": "^0.1.0",
|
"@kit/tsconfig": "workspace:^",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.3"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.11"
|
"prettier-plugin-tailwindcss": "^0.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/tsconfig": "^0.1.0",
|
"@kit/tsconfig": "workspace:^",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.3"
|
||||||
},
|
},
|
||||||
"prettier": "./index.mjs"
|
"prettier": "./index.mjs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,17 +14,17 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "8.4.33",
|
"postcss": "8.4.33",
|
||||||
"tailwindcss": "3.4.1"
|
"tailwindcss": "3.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "^0.2.0",
|
"@kit/eslint-config": "workspace:^",
|
||||||
"@kit/prettier-config": "^0.1.0",
|
"@kit/prettier-config": "workspace:^",
|
||||||
"@kit/tsconfig": "^0.1.0",
|
"@kit/tsconfig": "workspace:^",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.3"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user