diff --git a/apps/admin/app/(all)/(dashboard)/workspace/page.tsx b/apps/admin/app/(all)/(dashboard)/workspace/page.tsx index 5816ac9599d..820f3e88e54 100644 --- a/apps/admin/app/(all)/(dashboard)/workspace/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/workspace/page.tsx @@ -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"; @@ -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 @@ -83,27 +84,50 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props >
{formattedConfig ? ( -
-
-
-
Prevent anyone else from creating a workspace.
-
- Toggling this on will let only you create workspaces. You will have to invite users to new workspaces. +
+
+
+
+
Prevent anyone else from creating a workspace.
+
+ Toggling this on will let only you create workspaces. You will have to invite users to new workspaces. +
+
+
+
+
+ { + if (Boolean(parseInt(disableWorkspaceCreation)) === true) { + updateConfig("DISABLE_WORKSPACE_CREATION", "0"); + } else { + updateConfig("DISABLE_WORKSPACE_CREATION", "1"); + } + }} + size="sm" + disabled={isSubmitting} + />
-
-
- { - if (Boolean(parseInt(disableWorkspaceCreation)) === true) { - updateConfig("DISABLE_WORKSPACE_CREATION", "0"); - } else { - updateConfig("DISABLE_WORKSPACE_CREATION", "1"); - } - }} - size="sm" +
+
+
+
Default workspaces for new users.
+
+ Comma-separated workspace slugs (e.g. my-org,my-org-dev) or * for all + workspaces. New users are automatically added as Members and skip the onboarding flow. +
+
+
+
+ updateConfig("DEFAULT_WORKSPACE_SLUGS", e.target.value)} + placeholder="* or workspace-slug, another-slug" + className="w-64" disabled={isSubmitting} />
diff --git a/apps/api/plane/authentication/utils/user_auth_workflow.py b/apps/api/plane/authentication/utils/user_auth_workflow.py index 4641f332c5a..8af984a74fb 100644 --- a/apps/api/plane/authentication/utils/user_auth_workflow.py +++ b/apps/api/plane/authentication/utils/user_auth_workflow.py @@ -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) diff --git a/apps/api/plane/authentication/utils/workspace_project_join.py b/apps/api/plane/authentication/utils/workspace_project_join.py index 9222791a845..15a486520c3 100644 --- a/apps/api/plane/authentication/utils/workspace_project_join.py +++ b/apps/api/plane/authentication/utils/workspace_project_join.py @@ -2,6 +2,9 @@ # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. +# Python imports +import os + # Django imports from django.utils import timezone @@ -9,6 +12,7 @@ from plane.db.models import ( ProjectMember, ProjectMemberInvite, + Workspace, WorkspaceMember, WorkspaceMemberInvite, ) @@ -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, + }, + ) diff --git a/apps/api/plane/utils/instance_config_variables/core.py b/apps/api/plane/utils/instance_config_variables/core.py index 274c6539af9..ceab6e00a74 100644 --- a/apps/api/plane/utils/instance_config_variables/core.py +++ b/apps/api/plane/utils/instance_config_variables/core.py @@ -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 = [ diff --git a/packages/types/src/instance/workspace.ts b/packages/types/src/instance/workspace.ts index 3f4f4853b58..ebcdd5ff4ad 100644 --- a/packages/types/src/instance/workspace.ts +++ b/packages/types/src/instance/workspace.ts @@ -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";