mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge pull request #2930 from Harikrishnan1367709/Add-the-ability-to-mark-an-organization-as--default--or-remember-last-used-organization-#1991
feat: Add ability to mark organization as default (#1991)
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
|||||||
PieChart,
|
PieChart,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Star,
|
||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
@@ -497,7 +498,6 @@ function SidebarLogo() {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.get.useQuery();
|
const { data: user } = api.user.get.useQuery();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: organizations,
|
data: organizations,
|
||||||
refetch,
|
refetch,
|
||||||
@@ -505,6 +505,8 @@ function SidebarLogo() {
|
|||||||
} = api.organization.all.useQuery();
|
} = api.organization.all.useQuery();
|
||||||
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||||
api.organization.delete.useMutation();
|
api.organization.delete.useMutation();
|
||||||
|
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
|
||||||
|
api.organization.setDefault.useMutation();
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
const _utils = api.useUtils();
|
const _utils = api.useUtils();
|
||||||
@@ -594,66 +596,127 @@ function SidebarLogo() {
|
|||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
Organizations
|
Organizations
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{organizations?.map((org) => (
|
{organizations?.map((org) => {
|
||||||
<div className="flex flex-row justify-between" key={org.name}>
|
const isDefault = org.members?.[0]?.isDefault ?? false;
|
||||||
<DropdownMenuItem
|
return (
|
||||||
onClick={async () => {
|
<div
|
||||||
await authClient.organization.setActive({
|
className="flex flex-row justify-between"
|
||||||
organizationId: org.id,
|
key={org.name}
|
||||||
});
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
className="w-full gap-2 p-2"
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">{org.name}</div>
|
<DropdownMenuItem
|
||||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
onClick={async () => {
|
||||||
<Logo
|
await authClient.organization.setActive({
|
||||||
className={cn(
|
organizationId: org.id,
|
||||||
"transition-all",
|
});
|
||||||
state === "collapsed" ? "size-6" : "size-10",
|
window.location.reload();
|
||||||
)}
|
}}
|
||||||
logoUrl={org.logo ?? undefined}
|
className="w-full gap-2 p-2"
|
||||||
/>
|
>
|
||||||
</div>
|
<div className="flex flex-col gap-1">
|
||||||
</DropdownMenuItem>
|
<div className="flex items-center gap-2">
|
||||||
{org.ownerId === session?.user?.id && (
|
{org.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
logoUrl={org.logo ?? undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AddOrganization organizationId={org.id} />
|
<Button
|
||||||
<DialogAction
|
variant="ghost"
|
||||||
title="Delete Organization"
|
size="icon"
|
||||||
description="Are you sure you want to delete this organization?"
|
className={cn(
|
||||||
type="destructive"
|
"group",
|
||||||
onClick={async () => {
|
isDefault
|
||||||
await deleteOrganization({
|
? "hover:bg-yellow-500/10"
|
||||||
|
: "hover:bg-blue-500/10",
|
||||||
|
)}
|
||||||
|
isLoading={isSettingDefault && !isDefault}
|
||||||
|
disabled={isDefault}
|
||||||
|
onClick={async (e) => {
|
||||||
|
if (isDefault) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
await setDefaultOrganization({
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
toast.success(
|
toast.success("Default organization updated");
|
||||||
"Organization deleted successfully",
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
error?.message ||
|
error?.message ||
|
||||||
"Error deleting organization",
|
"Error setting default organization",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
title={
|
||||||
|
isDefault
|
||||||
|
? "Default organization"
|
||||||
|
: "Set as default"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
{isDefault ? (
|
||||||
variant="ghost"
|
<Star
|
||||||
size="icon"
|
fill="#eab308"
|
||||||
className="group hover:bg-red-500/10"
|
stroke="#eab308"
|
||||||
isLoading={isRemoving}
|
className="size-4 text-yellow-500"
|
||||||
>
|
/>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
) : (
|
||||||
</Button>
|
<Star
|
||||||
</DialogAction>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-4 text-gray-400 group-hover:text-blue-500 transition-colors"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{org.ownerId === session?.user?.id && (
|
||||||
|
<>
|
||||||
|
<AddOrganization organizationId={org.id} />
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Organization"
|
||||||
|
description="Are you sure you want to delete this organization?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteOrganization({
|
||||||
|
organizationId: org.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
"Organization deleted successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error?.message ||
|
||||||
|
"Error deleting organization",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
{(user?.role === "owner" || isCloud) && (
|
{(user?.role === "owner" || isCloud) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0119_bouncy_morbius.sql
Normal file
1
apps/dokploy/drizzle/0119_bouncy_morbius.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "is_default" boolean DEFAULT false NOT NULL;
|
||||||
6686
apps/dokploy/drizzle/meta/0119_snapshot.json
Normal file
6686
apps/dokploy/drizzle/meta/0119_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -800,12 +800,12 @@
|
|||||||
"tag": "0113_complete_rafael_vega",
|
"tag": "0113_complete_rafael_vega",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 114,
|
"idx": 114,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1759643172958,
|
"when": 1759643172958,
|
||||||
"tag": "0114_dry_black_tom",
|
"tag": "0114_dry_black_tom",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 115,
|
"idx": 115,
|
||||||
@@ -834,6 +834,13 @@
|
|||||||
"when": 1761415824484,
|
"when": 1761415824484,
|
||||||
"tag": "0118_loose_anita_blake",
|
"tag": "0118_loose_anita_blake",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 119,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1762142756443,
|
||||||
|
"tag": "0119_bouncy_morbius",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,11 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is the user's first organization
|
||||||
|
const existingMemberships = await db.query.member.findMany({
|
||||||
|
where: eq(member.userId, ctx.user.id),
|
||||||
|
});
|
||||||
|
|
||||||
await db.insert(member).values({
|
await db.insert(member).values({
|
||||||
organizationId: result.id,
|
organizationId: result.id,
|
||||||
role: "owner",
|
role: "owner",
|
||||||
@@ -63,6 +68,11 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
with: {
|
||||||
|
members: {
|
||||||
|
where: eq(member.userId, ctx.user.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return memberResult;
|
return memberResult;
|
||||||
}),
|
}),
|
||||||
@@ -184,4 +194,45 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
.delete(invitation)
|
.delete(invitation)
|
||||||
.where(eq(invitation.id, input.invitationId));
|
.where(eq(invitation.id, input.invitationId));
|
||||||
}),
|
}),
|
||||||
|
setDefault: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
organizationId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
// Verify user is a member of this organization
|
||||||
|
const userMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.organizationId, input.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You are not a member of this organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, unset all defaults for this user
|
||||||
|
await db
|
||||||
|
.update(member)
|
||||||
|
.set({ isDefault: false })
|
||||||
|
.where(eq(member.userId, ctx.user.id));
|
||||||
|
|
||||||
|
// Then set this organization as default
|
||||||
|
await db
|
||||||
|
.update(member)
|
||||||
|
.set({ isDefault: true })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(member.organizationId, input.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const member = pgTable("member", {
|
|||||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||||
createdAt: timestamp("created_at").notNull(),
|
createdAt: timestamp("created_at").notNull(),
|
||||||
teamId: text("team_id"),
|
teamId: text("team_id"),
|
||||||
|
isDefault: boolean("is_default").notNull().default(false),
|
||||||
// Permissions
|
// Permissions
|
||||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||||
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ const { handler, api } = betterAuth({
|
|||||||
organizationId: organization?.id || "",
|
organizationId: organization?.id || "",
|
||||||
role: "owner",
|
role: "owner",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
isDefault: true, // Mark first organization as default
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -174,9 +175,14 @@ const { handler, api } = betterAuth({
|
|||||||
session: {
|
session: {
|
||||||
create: {
|
create: {
|
||||||
before: async (session) => {
|
before: async (session) => {
|
||||||
|
// Find the default organization for this user
|
||||||
|
// Priority: 1) isDefault=true, 2) most recently created
|
||||||
const member = await db.query.member.findFirst({
|
const member = await db.query.member.findFirst({
|
||||||
where: eq(schema.member.userId, session.userId),
|
where: eq(schema.member.userId, session.userId),
|
||||||
orderBy: desc(schema.member.createdAt),
|
orderBy: [
|
||||||
|
desc(schema.member.isDefault),
|
||||||
|
desc(schema.member.createdAt),
|
||||||
|
],
|
||||||
with: {
|
with: {
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user