Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 43 additions & 19 deletions apps/admin/app/(all)/(dashboard)/workspace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Loader as LoaderIcon } from "lucide-react";
import { Button, getButtonStyling } from "@plane/propel/button";
import { setPromiseToast } from "@plane/propel/toast";
import type { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui";
import { Input, Loader, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
Expand All @@ -37,6 +37,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
} = useWorkspace();
// derived values
const disableWorkspaceCreation = formattedConfig?.DISABLE_WORKSPACE_CREATION ?? "";
const defaultWorkspaceSlugs = formattedConfig?.DEFAULT_WORKSPACE_SLUGS ?? "";
const hasNextPage = paginationInfo?.next_page_results && paginationInfo?.next_cursor !== undefined;

// fetch data
Expand Down Expand Up @@ -83,27 +84,50 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
>
<div className="space-y-3">
{formattedConfig ? (
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="pb-1 text-16 font-medium">Prevent anyone else from creating a workspace.</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
<div className="space-y-6">
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="pb-1 text-16 font-medium">Prevent anyone else from creating a workspace.</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(disableWorkspaceCreation))}
onChange={() => {
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
} else {
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
}
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(disableWorkspaceCreation))}
onChange={() => {
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
} else {
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
}
}}
size="sm"
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="pb-1 text-16 font-medium">Default workspaces for new users.</div>
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
Comma-separated workspace slugs (e.g. <code>my-org,my-org-dev</code>) or <code>*</code> for all
workspaces. New users are automatically added as Members and skip the onboarding flow.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<Input
type="text"
value={defaultWorkspaceSlugs}
onChange={(e) => updateConfig("DEFAULT_WORKSPACE_SLUGS", e.target.value)}
placeholder="* or workspace-slug, another-slug"
className="w-64"
disabled={isSubmitting}
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/api/plane/authentication/utils/user_auth_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

from .workspace_project_join import process_workspace_project_invitations
from .workspace_project_join import auto_join_default_workspaces, process_workspace_project_invitations


def post_user_auth_workflow(user, is_signup, request):
process_workspace_project_invitations(user=user)
auto_join_default_workspaces(user=user)
66 changes: 66 additions & 0 deletions apps/api/plane/authentication/utils/workspace_project_join.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

# Python imports
import os

# Django imports
from django.utils import timezone

# Module imports
from plane.db.models import (
ProjectMember,
ProjectMemberInvite,
Workspace,
WorkspaceMember,
WorkspaceMemberInvite,
)
Expand Down Expand Up @@ -89,3 +93,65 @@ def process_workspace_project_invitations(user):
# Delete all the invites
workspace_member_invites.delete()
project_member_invites.delete()


def auto_join_default_workspaces(user):
"""
If DEFAULT_WORKSPACE_SLUGS is configured and the user has no workspace memberships,
automatically add them as a Member to all listed workspaces and mark onboarding
complete so they land directly in the first workspace without the onboarding flow.

DEFAULT_WORKSPACE_SLUGS accepts:
- A comma-separated list of workspace slugs: "my-org,my-org-dev"
- A wildcard "*" to auto-join all workspaces on the instance

The first slug (or oldest workspace for "*") becomes the landing workspace.
"""
from plane.license.utils.instance_value import get_configuration_value

(slugs_raw,) = get_configuration_value(
[{"key": "DEFAULT_WORKSPACE_SLUGS", "default": os.environ.get("DEFAULT_WORKSPACE_SLUGS", "")}]
)
if not slugs_raw:
return

slugs_raw = slugs_raw.strip()

# Only auto-join users who have no workspace memberships yet
if WorkspaceMember.objects.filter(member=user, is_active=True).exists():
return

if slugs_raw == "*":
workspaces = list(Workspace.objects.order_by("created_at"))
slug_order = {} # not used for wildcard; primary = oldest workspace
else:
slugs = [s.strip() for s in slugs_raw.split(",") if s.strip()]
if not slugs:
return
workspaces = list(Workspace.objects.filter(slug__in=slugs))
slug_order = {s: i for i, s in enumerate(slugs)}

if not workspaces:
return

WorkspaceMember.objects.bulk_create(
[WorkspaceMember(workspace=w, member=user, role=15, is_active=True) for w in workspaces],
ignore_conflicts=True,
)

# Primary (landing) workspace: first by slug order, or oldest for wildcard
primary = workspaces[0] if slugs_raw == "*" else min(workspaces, key=lambda w: slug_order.get(w.slug, 999))

# Mark onboarding complete so the user lands directly in the workspace
from plane.db.models.user import Profile

Profile.objects.filter(user=user).update(
is_onboarded=True,
last_workspace_id=primary.id,
onboarding_step={
"profile_complete": True,
"workspace_create": True,
"workspace_invite": True,
"workspace_join": True,
},
)
6 changes: 6 additions & 0 deletions apps/api/plane/utils/instance_config_variables/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"category": "WORKSPACE_MANAGEMENT",
"is_encrypted": False,
},
{
"key": "DEFAULT_WORKSPACE_SLUGS",
"value": os.environ.get("DEFAULT_WORKSPACE_SLUGS", ""),
"category": "WORKSPACE_MANAGEMENT",
"is_encrypted": False,
},
]

google_config_variables = [
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/instance/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
* See the LICENSE file for details.
*/

export type TInstanceWorkspaceConfigurationKeys = "DISABLE_WORKSPACE_CREATION";
export type TInstanceWorkspaceConfigurationKeys = "DISABLE_WORKSPACE_CREATION" | "DEFAULT_WORKSPACE_SLUGS";