Compare commits
1 Commits
feat/partn
...
fix/select
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6878c5af5a |
@@ -103,55 +103,72 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storedWorkspaceId !== null) {
|
if (storedWorkspaceId !== null) {
|
||||||
const newData = data;
|
const selectedWorkspace = data.find(
|
||||||
const selectedWorkspace = newData.find(
|
|
||||||
({ workspace }) => workspace.publicId === storedWorkspaceId,
|
({ workspace }) => workspace.publicId === storedWorkspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedWorkspace?.workspace) {
|
if (!selectedWorkspace?.workspace) {
|
||||||
pollAttemptsRef.current += 1;
|
if (pendingWorkspaceId) {
|
||||||
if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
|
pollAttemptsRef.current += 1;
|
||||||
setPendingWorkspaceId(null);
|
if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
|
||||||
|
setPendingWorkspaceId(null);
|
||||||
|
localStorage.removeItem("workspacePublicId");
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear stale workspacePublicId from localStorage
|
||||||
|
localStorage.removeItem("workspacePublicId");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
pollAttemptsRef.current = 0;
|
||||||
|
setPendingWorkspaceId(null);
|
||||||
|
|
||||||
|
setWorkspace({
|
||||||
|
publicId: selectedWorkspace.workspace.publicId,
|
||||||
|
name: selectedWorkspace.workspace.name,
|
||||||
|
slug: selectedWorkspace.workspace.slug,
|
||||||
|
plan: selectedWorkspace.workspace.plan,
|
||||||
|
description: selectedWorkspace.workspace.description,
|
||||||
|
role: selectedWorkspace.role as "admin" | "member" | "guest",
|
||||||
|
weekStartDay: selectedWorkspace.workspace.weekStartDay as 0 | 1 | 6,
|
||||||
|
cardPrefix: selectedWorkspace.workspace.cardPrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workspacePublicId) {
|
||||||
|
router.push(`/boards`);
|
||||||
|
localStorage.setItem("workspacePublicId", workspacePublicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pollAttemptsRef.current = 0;
|
|
||||||
setPendingWorkspaceId(null);
|
|
||||||
|
|
||||||
setWorkspace({
|
|
||||||
publicId: selectedWorkspace.workspace.publicId,
|
|
||||||
name: selectedWorkspace.workspace.name,
|
|
||||||
slug: selectedWorkspace.workspace.slug,
|
|
||||||
plan: selectedWorkspace.workspace.plan,
|
|
||||||
description: selectedWorkspace.workspace.description,
|
|
||||||
role: selectedWorkspace.role,
|
|
||||||
weekStartDay: selectedWorkspace.workspace.weekStartDay as 0 | 1 | 6,
|
|
||||||
cardPrefix: selectedWorkspace.workspace.cardPrefix,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (workspacePublicId) {
|
|
||||||
router.push(`/boards`);
|
|
||||||
localStorage.setItem("workspacePublicId", workspacePublicId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const primaryWorkspace = data[0]?.workspace;
|
|
||||||
const primaryWorkspaceRole = data[0]?.role;
|
|
||||||
|
|
||||||
if (!primaryWorkspace || !primaryWorkspaceRole) return;
|
|
||||||
localStorage.setItem("workspacePublicId", primaryWorkspace.publicId);
|
|
||||||
setWorkspace({
|
|
||||||
publicId: primaryWorkspace.publicId,
|
|
||||||
name: primaryWorkspace.name,
|
|
||||||
slug: primaryWorkspace.slug,
|
|
||||||
plan: primaryWorkspace.plan,
|
|
||||||
description: primaryWorkspace.description,
|
|
||||||
role: primaryWorkspaceRole,
|
|
||||||
weekStartDay: primaryWorkspace.weekStartDay as 0 | 1 | 6,
|
|
||||||
cardPrefix: primaryWorkspace.cardPrefix,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [data, isLoading, isFetching, workspacePublicId, router]);
|
|
||||||
|
const primaryWorkspace = data[0]?.workspace;
|
||||||
|
const primaryWorkspaceRole = data[0]?.role;
|
||||||
|
|
||||||
|
if (!primaryWorkspace || !primaryWorkspaceRole) return;
|
||||||
|
localStorage.setItem("workspacePublicId", primaryWorkspace.publicId);
|
||||||
|
setWorkspace({
|
||||||
|
publicId: primaryWorkspace.publicId,
|
||||||
|
name: primaryWorkspace.name,
|
||||||
|
slug: primaryWorkspace.slug,
|
||||||
|
plan: primaryWorkspace.plan,
|
||||||
|
description: primaryWorkspace.description,
|
||||||
|
role: primaryWorkspaceRole as "admin" | "member" | "guest",
|
||||||
|
weekStartDay: primaryWorkspace.weekStartDay as 0 | 1 | 6,
|
||||||
|
cardPrefix: primaryWorkspace.cardPrefix,
|
||||||
|
});
|
||||||
|
setHasLoaded(true);
|
||||||
|
}, [
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
workspacePublicId,
|
||||||
|
pendingWorkspaceId,
|
||||||
|
router,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceContext.Provider
|
<WorkspaceContext.Provider
|
||||||
|
|||||||
@@ -36,17 +36,6 @@ export default function InvitePage() {
|
|||||||
return router.push(`/boards`);
|
return router.push(`/boards`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
error.data?.code === "FORBIDDEN" &&
|
|
||||||
error.message === "SEAT_LIMIT_REACHED"
|
|
||||||
) {
|
|
||||||
setError(
|
|
||||||
t`This workspace has reached its member limit. The workspace owner will need to upgrade their plan.`,
|
|
||||||
);
|
|
||||||
setIsProcessing(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(
|
setError(
|
||||||
error.message ||
|
error.message ||
|
||||||
t`Failed to accept invitation. Please try again later, or contact customer support.`,
|
t`Failed to accept invitation. Please try again later, or contact customer support.`,
|
||||||
|
|||||||
@@ -26,13 +26,9 @@ import { api } from "~/utils/api";
|
|||||||
export function InviteMemberForm({
|
export function InviteMemberForm({
|
||||||
subscriptions,
|
subscriptions,
|
||||||
unlimitedSeats,
|
unlimitedSeats,
|
||||||
memberCount,
|
|
||||||
seatLimit,
|
|
||||||
}: {
|
}: {
|
||||||
subscriptions: Subscription[] | undefined;
|
subscriptions: Subscription[] | undefined;
|
||||||
unlimitedSeats: boolean;
|
unlimitedSeats: boolean;
|
||||||
memberCount: number;
|
|
||||||
seatLimit: number | null;
|
|
||||||
}) {
|
}) {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isShareInviteLinkEnabled, setIsShareInviteLinkEnabled] =
|
const [isShareInviteLinkEnabled, setIsShareInviteLinkEnabled] =
|
||||||
@@ -81,8 +77,6 @@ export function InviteMemberForm({
|
|||||||
}
|
}
|
||||||
}, [activeInviteLink]);
|
}, [activeInviteLink]);
|
||||||
|
|
||||||
const isAtSeatLimit = seatLimit !== null && memberCount >= seatLimit;
|
|
||||||
|
|
||||||
const inviteMember = api.member.invite.useMutation({
|
const inviteMember = api.member.invite.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
@@ -99,15 +93,6 @@ export function InviteMemberForm({
|
|||||||
message: t`User is already a member of this workspace`,
|
message: t`User is already a member of this workspace`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
} else if (
|
|
||||||
error.data?.code === "FORBIDDEN" &&
|
|
||||||
error.message === "SEAT_LIMIT_REACHED"
|
|
||||||
) {
|
|
||||||
showPopup({
|
|
||||||
header: t`Seat limit reached`,
|
|
||||||
message: t`You've reached your ${seatLimit ?? 0}-seat limit. Please upgrade your plan to add more members.`,
|
|
||||||
icon: "error",
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: t`Error inviting member`,
|
header: t`Error inviting member`,
|
||||||
@@ -299,30 +284,22 @@ export function InviteMemberForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
|
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
|
||||||
<div className="mt-3 rounded-md bg-light-100 p-3 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900">
|
!isPartnerTier &&
|
||||||
{isPartnerTier && seatLimit !== null ? (
|
!unlimitedSeats && (
|
||||||
<div>
|
<div className="mt-3 rounded-md bg-light-100 p-3 text-xs text-light-900 dark:bg-dark-200 dark:text-dark-900">
|
||||||
<div className="flex items-center justify-between">
|
{hasTeamSubscription || hasProSubscription ? (
|
||||||
<span className="font-medium text-emerald-500 dark:text-emerald-400">
|
|
||||||
{hasTeamSubscription ? t`Team Plan` : t`Pro Plan`}
|
|
||||||
</span>
|
|
||||||
<span className="text-light-900 dark:text-dark-900">
|
|
||||||
{memberCount} / {seatLimit} {t`seats`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : !unlimitedSeats ? (
|
|
||||||
hasTeamSubscription || hasProSubscription ? (
|
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-emerald-500 dark:text-emerald-400">
|
<span className="font-medium text-emerald-500 dark:text-emerald-400">
|
||||||
{hasTeamSubscription ? t`Team Plan` : t`Pro Plan ∞`}
|
{hasTeamSubscription ? t`Team Plan` : t`Pro Plan ∞`}
|
||||||
</span>
|
</span>
|
||||||
<p className="mt-1">
|
{!isPartnerTier && (
|
||||||
{unlimitedSeats
|
<p className="mt-1">
|
||||||
? t`You have unlimited seats with your Pro Plan. There is no additional charge for new members!`
|
{unlimitedSeats
|
||||||
: t`Adding a new member will cost an additional ${price} (${billingType}) per seat.`}
|
? t`You have unlimited seats with your Pro Plan. There is no additional charge for new members!`
|
||||||
</p>
|
: t`Adding a new member will cost an additional ${price} (${billingType}) per seat.`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -333,10 +310,9 @@ export function InviteMemberForm({
|
|||||||
{t`Inviting members requires a Team or Pro plan. You'll be redirected to upgrade your workspace.`}
|
{t`Inviting members requires a Team or Pro plan. You'll be redirected to upgrade your workspace.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
<div className="mt-12 flex items-center justify-end space-x-4 border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||||
|
|||||||
@@ -11,11 +11,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import type { Subscription } from "@kan/shared/utils";
|
import type { Subscription } from "@kan/shared/utils";
|
||||||
import { authClient } from "@kan/auth/client";
|
import { authClient } from "@kan/auth/client";
|
||||||
import {
|
import { getSubscriptionByPlan, hasUnlimitedSeats } from "@kan/shared/utils";
|
||||||
getSeatLimit,
|
|
||||||
getSubscriptionByPlan,
|
|
||||||
hasUnlimitedSeats,
|
|
||||||
} from "@kan/shared/utils";
|
|
||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
@@ -88,7 +84,6 @@ export default function MembersPage() {
|
|||||||
const isPaidPlan = isProPlan || isTeamPlan;
|
const isPaidPlan = isProPlan || isTeamPlan;
|
||||||
|
|
||||||
const activeMembers = data?.members.length ?? 0;
|
const activeMembers = data?.members.length ?? 0;
|
||||||
const seatLimit = getSeatLimit(subscriptions);
|
|
||||||
const totalSeats =
|
const totalSeats =
|
||||||
teamSubscription?.seats ??
|
teamSubscription?.seats ??
|
||||||
proSubscription?.seats ??
|
proSubscription?.seats ??
|
||||||
@@ -407,8 +402,6 @@ export default function MembersPage() {
|
|||||||
<InviteMemberForm
|
<InviteMemberForm
|
||||||
subscriptions={subscriptions}
|
subscriptions={subscriptions}
|
||||||
unlimitedSeats={unlimitedSeats}
|
unlimitedSeats={unlimitedSeats}
|
||||||
memberCount={activeMembers}
|
|
||||||
seatLimit={seatLimit}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,13 @@ import * as userRepo from "@kan/db/repository/user.repo";
|
|||||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
import {
|
import {
|
||||||
generateUID,
|
generateUID,
|
||||||
getSeatLimit,
|
|
||||||
getSubscriptionByPlan,
|
getSubscriptionByPlan,
|
||||||
hasUnlimitedSeats,
|
hasUnlimitedSeats,
|
||||||
} from "@kan/shared";
|
} from "@kan/shared";
|
||||||
import { updateSubscriptionSeats } from "@kan/stripe";
|
import { updateSubscriptionSeats } from "@kan/stripe";
|
||||||
|
|
||||||
import { memberInviteResponseSchema } from "../schemas";
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
import { memberInviteResponseSchema } from "../schemas";
|
||||||
import {
|
import {
|
||||||
assertCanManageMember,
|
assertCanManageMember,
|
||||||
assertCanManageRole,
|
assertCanManageRole,
|
||||||
@@ -115,20 +114,6 @@ export const memberRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seatLimit = getSeatLimit(subscriptions);
|
|
||||||
if (seatLimit !== null) {
|
|
||||||
const memberCount = await memberRepo.getCountByWorkspaceId(
|
|
||||||
ctx.db,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
if (memberCount >= seatLimit) {
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `SEAT_LIMIT_REACHED`,
|
|
||||||
code: "FORBIDDEN",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await userRepo.getByEmail(ctx.db, input.email);
|
const existingUser = await userRepo.getByEmail(ctx.db, input.email);
|
||||||
@@ -659,20 +644,6 @@ export const memberRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seatLimit = getSeatLimit(subscriptions);
|
|
||||||
if (seatLimit !== null) {
|
|
||||||
const memberCount = await memberRepo.getCountByWorkspaceId(
|
|
||||||
ctx.db,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
if (memberCount >= seatLimit) {
|
|
||||||
throw new TRPCError({
|
|
||||||
message: `SEAT_LIMIT_REACHED`,
|
|
||||||
code: "FORBIDDEN",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the workspace role to set roleId
|
// Get the workspace role to set roleId
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, count, eq, isNull, or } from "drizzle-orm";
|
import { and, count, eq, isNull } from "drizzle-orm";
|
||||||
|
|
||||||
import type { dbClient } from "@kan/db/client";
|
import type { dbClient } from "@kan/db/client";
|
||||||
import type { MemberRole, MemberStatus } from "@kan/db/schema";
|
import type { MemberRole, MemberStatus } from "@kan/db/schema";
|
||||||
@@ -19,27 +19,6 @@ export const getActiveCount = async (db: dbClient) => {
|
|||||||
return result[0]?.count ?? 0;
|
return result[0]?.count ?? 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCountByWorkspaceId = async (
|
|
||||||
db: dbClient,
|
|
||||||
workspaceId: number,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.select({ count: count() })
|
|
||||||
.from(workspaceMembers)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(workspaceMembers.workspaceId, workspaceId),
|
|
||||||
isNull(workspaceMembers.deletedAt),
|
|
||||||
or(
|
|
||||||
eq(workspaceMembers.status, "active"),
|
|
||||||
eq(workspaceMembers.status, "invited"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return result[0]?.count ?? 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const create = async (
|
export const create = async (
|
||||||
db: dbClient,
|
db: dbClient,
|
||||||
memberInput: {
|
memberInput: {
|
||||||
|
|||||||
@@ -54,14 +54,3 @@ export const hasUnlimitedSeats = (
|
|||||||
const activeSubscriptions = getActiveSubscriptions(subscriptions);
|
const activeSubscriptions = getActiveSubscriptions(subscriptions);
|
||||||
return activeSubscriptions.some((sub) => sub.unlimitedSeats);
|
return activeSubscriptions.some((sub) => sub.unlimitedSeats);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSeatLimit = (
|
|
||||||
subscriptions: Subscription[] | undefined,
|
|
||||||
): number | null => {
|
|
||||||
const activeSubscriptions = getActiveSubscriptions(subscriptions);
|
|
||||||
const partnerSub = activeSubscriptions.find(
|
|
||||||
(sub) =>
|
|
||||||
sub.partnerTier !== null && !sub.unlimitedSeats && sub.seats !== null,
|
|
||||||
);
|
|
||||||
return partnerSub?.seats ?? null;
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user