[improvement] Default workspace (#12834)

* Added set-default API

* Setting default workspace for super-admin onboarding

* Seperated the migrations

* Added nestjs init

* removed nestjs init

* Added: default workspace case to signup

* Fixed: instance signup

* Fixed: existed non-active user instance signup

* Added: SSO default workspace support

* Added: Default workspace chooser

* Moved some scss changes to ee folder

* Added: disable workspace default organization check

* updated the migration

* Fixing .env issue

* Removed the logs

* Remove personal workspace check from enable signup

* Fixing sign-in cases

* Fixing workspace invited user's instance signup cases

* Fixing sso workspace invited user's instance signup cases

* fixing the workspace signup issue

* Adding ee server and frontend file

* Adding ee server and frontend file

* Adding active check

* Added query fix for the migration

* Added migration logic fix

* Removed/Commented the ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS env support from EE and CE

* Adding server and frontend files

* Added frontend file

* Bump version
This commit is contained in:
Muhsin Shah C P 2025-05-14 16:06:52 +05:30 committed by GitHub
parent c0009b2c3a
commit 958cbd1d02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 425 additions and 137 deletions

View file

@ -1 +1 @@
3.12.0
3.12.1

View file

@ -1 +1 @@
3.12.0
3.12.1

@ -1 +1 @@
Subproject commit 518f3334b12a83785fd37dd53b0245d72848211a
Subproject commit dd5796edccc8d5eda524d804a222845791d733fb

View file

@ -13,6 +13,7 @@ export const organizationService = {
getWorkspacesLimit,
checkWorkspaceUniqueness,
updateOrganization,
setDefaultWorkspace,
};
function getUsersByValue(searchInput) {
@ -100,3 +101,8 @@ function checkWorkspaceUniqueness(name, slug) {
const query = queryString.stringify({ name, slug });
return fetch(`${config.apiUrl}/organizations/is-unique?${query}`, requestOptions).then(handleResponse);
}
function setDefaultWorkspace(workspaceId) {
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/organizations/${workspaceId}/default`, requestOptions).then(handleResponse);
}

View file

@ -10029,92 +10029,6 @@ tbody {
}
}
}
.manage-ws-table-body {
width: 100%;
.workspace-table-row {
border-bottom: 1px solid var(--slate5);
height: 64px;
width: 100%;
.ws-name {
padding-left: 8px;
.current-workspace-tag {
font-weight: 500;
color: var(--indigo9);
font-size: 12px;
display: flex;
height: 21px;
width: 130px;
align-items: center;
margin-left: 20px;
padding: 4px 8px 5px 8px;
border: 1px solid var(--indigo7);
background-color: var(--indigo3);
border-radius: 100px;
}
}
.open-button-cont {
width: 44px;
padding: 0px 8px 0px 8px;
.workspace-open-btn {
width: 28px;
height: 28px;
background-color: var(--slate1);
border: 1px solid var(--slate7);
box-shadow: none;
&:hover {
background-color: var(--slate4);
}
}
}
.archive-btn-cont {
width: 103px;
padding-right: 8px;
.workspace-archive-btn {
width: 95px;
height: 28px;
background-color: var(--slate1);
box-shadow: none;
border: 1px solid var(--tomato7);
color: var(--tomato9);
&:hover {
background-color: var(--tomato3);
}
&:disabled {
border: 1px solid var(--slate7);
}
}
.workspace-active-btn {
width: 95px;
height: 28px;
background-color: var(--slate1);
box-shadow: none;
border: 1px solid var(--slate7);
color: var(--slate12);
&:hover {
background-color: var(--slate7);
}
}
}
}
}
}
.manage-workspace-table-wrap.dark-mode {

View file

@ -1 +1 @@
3.12.0
3.12.1

View file

@ -0,0 +1,66 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { TOOLJET_EDITIONS } from '@modules/app/constants';
import { getCustomEnvVars, getTooljetEdition } from '@helpers/utils.helper';
import { Organization } from '@entities/organization.entity';
import { WORKSPACE_STATUS } from '@modules/users/constants/lifecycle';
export class SetDefaultWorkspace1740401100000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
console.log('Skipping migration as it is not EE edition');
return;
}
// Check if default workspace URL is configured
const defaultWorkspaceUrl = getCustomEnvVars('TOOLJET_DEFAULT_WORKSPACE_URL');
if (defaultWorkspaceUrl) {
try {
const url = new URL(defaultWorkspaceUrl);
const pathParts = url.pathname.split('/');
const workspaceSlug = pathParts[pathParts.length - 1];
if (workspaceSlug) {
const organization = await queryRunner.manager.findOne(Organization, {
where: { slug: workspaceSlug, status: WORKSPACE_STATUS.ACTIVE },
select: ['id'],
});
if (organization){
await queryRunner.query(`
UPDATE organizations
SET is_default = true
WHERE slug = $1
`, [workspaceSlug]);
return;
}
console.log(`No active organization found with slug: ${workspaceSlug}`);
}
} catch (err) {
console.log('Invalid TOOLJET_DEFAULT_WORKSPACE_URL format');
}
}
// Set the first created organization as default
await queryRunner.query(`
UPDATE organizations
SET is_default = true
WHERE id = (
SELECT id
FROM organizations
WHERE status = '${WORKSPACE_STATUS.ACTIVE}'
ORDER BY created_at ASC
LIMIT 1
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
return;
}
// Unset all default workspaces
await queryRunner.query(`
UPDATE organizations
SET is_default = false;
`);
}
}

@ -1 +1 @@
Subproject commit b9e73f87b9062e06c49c2c73add6b82ba21dcacf
Subproject commit 7848948f90077fa3fa02e43fc577a62d00f9a4da

View file

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddIsDefaultToOrganizations1740401000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Add is_default column
await queryRunner.addColumn(
'organizations',
new TableColumn({
name: 'is_default',
type: 'boolean',
default: false,
isNullable: false,
})
);
// Create a partial unique index to ensure only one default workspace
await queryRunner.query(`
CREATE UNIQUE INDEX idx_organizations_single_default
ON organizations (is_default)
WHERE is_default = true;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop the unique index first
await queryRunner.query(`DROP INDEX IF EXISTS idx_organizations_single_default;`);
// Then drop the column
await queryRunner.dropColumn('organizations', 'is_default');
}
}

View file

@ -35,6 +35,9 @@ export class Organization extends BaseEntity {
@Column({ name: 'domain' })
domain: string;
@Column({ name: 'is_default', default: false })
isDefault: boolean;
@Column({ name: 'enable_sign_up' })
enableSignUp: boolean;

View file

@ -5,6 +5,8 @@ import { isEmpty } from 'lodash';
import { USER_TYPE } from '@modules/users/constants/lifecycle';
import { ConflictException } from '@nestjs/common';
import { DataBaseConstraints } from './db_constraints.constants';
import { getEnvVars } from 'scripts/database-config-utils';
const semver = require('semver');
@ -449,5 +451,11 @@ export const getSubpath = () => {
};
export function getTooljetEdition(): string {
return process.env.TOOLJET_EDITION?.toLowerCase() || 'ce';
const envVars = getEnvVars();
return envVars['TOOLJET_EDITION']?.toLowerCase() || 'ce';
}
export function getCustomEnvVars(name: string) {
const envVars = getEnvVars();
return envVars[name] || '';
}

View file

@ -174,7 +174,7 @@ export class OauthService implements IOAuthService {
// Not logging in to specific organization, creating new
const { name, slug } = generateNextNameAndSlug('My workspace');
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
userDetails = await this.userRepository.createOrUpdate(
{
@ -221,7 +221,7 @@ export class OauthService implements IOAuthService {
if (!isInviteRedirect) {
// no SSO login enabled organization available for user - creating new one
const { name, slug } = generateNextNameAndSlug('My workspace');
organizationDetails = await this.setupOrganizationsUtilService.create(name, slug, userDetails, manager);
organizationDetails = await this.setupOrganizationsUtilService.create({ name, slug }, userDetails, manager);
await this.userRepository.updateOne(
userDetails.id,
{ defaultOrganizationId: organizationDetails.id },

View file

@ -85,7 +85,7 @@ export class AuthService implements IAuthService {
} else if (allowPersonalWorkspace && !isInviteRedirect) {
// no form login enabled organization available for user - creating new one
const { name, slug } = generateNextNameAndSlug('My workspace');
organization = await this.setupOrganizationsUtilService.create(name, slug, user, manager);
organization = await this.setupOrganizationsUtilService.create({ name, slug }, user, manager);
} else {
if (!isInviteRedirect) throw new UnauthorizedException('User is not assigned to any workspaces');
}

View file

@ -149,7 +149,7 @@ export class AuthUtilService implements IAuthUtilService {
if (!user && allowPersonalWorkspace) {
const { name, slug } = generateNextNameAndSlug('My workspace');
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
}
const { source, status } = getUserStatusAndSource(lifecycleEvents.USER_SSO_ACTIVATE, sso);

View file

@ -26,7 +26,7 @@ export class LoginConfigsService implements ILoginConfigsService {
throw new NotFoundException();
}
if (!organizationId) {
const result = this.loginConfigsUtilService.constructSSOConfigs();
const result = await this.loginConfigsUtilService.constructSSOConfigs();
return result;
}

View file

@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController {
@InitFeature(FEATURE_KEY.SIGNUP)
@UseGuards(
SignupDisableGuard,
AllowPersonalWorkspaceGuard,
UserCountGuard,
EditorUserCountGuard,
FirstUserSignupDisableGuard,

View file

@ -26,6 +26,7 @@ export interface IOnboardingUtilService {
signingUpOrganization: Organization,
userParams: { firstName: string; lastName: string; password: string },
redirectTo?: string,
defaultWorkspace?: Organization,
manager?: EntityManager
): Promise<void>;
processOrganizationSignup(
@ -40,4 +41,10 @@ export interface IOnboardingUtilService {
organizationInviteUrl: string;
}>;
splitName(name: string): { firstName: string; lastName: string };
updateExistingUserDefaultWorkspace(
userParams: { password: string; firstName: string; lastName: string },
existingUser: User,
defaultWorkspace: Organization,
manager?: EntityManager
)
}

View file

@ -119,6 +119,9 @@ export class OnboardingService implements IOnboardingService {
const { firstName, lastName } = names;
const userParams = { email, password, firstName, lastName };
// Find the default workspace
const defaultWorkspace = await this.organizationRepository. getDefaultWorkspaceOfInstance();
if (existingUser) {
// Handling instance and workspace level signup for existing user
return await this.onboardingUtilService.whatIfTheSignUpIsAtTheWorkspaceLevel(
@ -126,9 +129,18 @@ export class OnboardingService implements IOnboardingService {
signingUpOrganization,
userParams,
redirectTo,
defaultWorkspace,
manager
);
} else {
if(defaultWorkspace && !signingUpOrganization) {
return await this.onboardingUtilService.createUserInDefaultWorkspace(
userParams,
defaultWorkspace,
redirectTo,
manager
);
}
return await this.onboardingUtilService.createUserOrPersonalWorkspace(
userParams,
existingUser,
@ -149,8 +161,7 @@ export class OnboardingService implements IOnboardingService {
const result = await dbTransactionWrap(async (manager: EntityManager) => {
// Create first organization
const organization = await this.organizationRepository.createOne(
workspace || 'My workspace',
'my-workspace',
{ name: workspace || 'My workspace', slug: 'my-workspace' },
manager
);
@ -226,7 +237,8 @@ export class OnboardingService implements IOnboardingService {
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
'true';
if (!(allowPersonalWorkspace || organizationToken)) {
const defaultWorkspace = await this.organizationRepository.getDefaultWorkspaceOfInstance();
if (!(defaultWorkspace || allowPersonalWorkspace || organizationToken)) {
throw new BadRequestException('Invalid invitation link');
}
if (organizationToken) {
@ -251,7 +263,8 @@ export class OnboardingService implements IOnboardingService {
throw new BadRequestException('Please enter password');
}
if (allowPersonalWorkspace) {
const activateDefaultWorkspace = (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
if (activateDefaultWorkspace) {
// Getting default workspace
const defaultOrganizationUser: OrganizationUser = user.organizationUsers.find(
(ou) => ou.organizationId === user.defaultOrganizationId
@ -264,6 +277,14 @@ export class OnboardingService implements IOnboardingService {
// Activate default workspace
await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager);
if(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId){
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(user.id);
for(const personalWorkspace of personalWorkspaces){
// if any personal workspace left. activate those
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
}
}
if (workspaceName) {
const { slug } = generateNextNameAndSlug('My workspace');
await this.organizationRepository.updateOne(
@ -449,10 +470,10 @@ export class OnboardingService implements IOnboardingService {
onboarding_details: {
status: user.onboardingStatus,
password: isPasswordMandatory(user.source), // Should accept password if user is setting up first time
questions:
(this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
!organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
(await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
// questions:
// (this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
// !organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
// (await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
},
};
}
@ -686,8 +707,7 @@ export class OnboardingService implements IOnboardingService {
// Create first organization
const workspaceSlug = generateWorkspaceSlug(workspaceName || 'My workspace');
const organization = await this.setupOrganizationsUtilService.create(
workspaceName || 'My workspace',
workspaceSlug,
{ name: workspaceName || 'My workspace', slug: workspaceSlug },
null,
manager
);

View file

@ -151,6 +151,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
signingUpOrganization: Organization,
userParams: { firstName: string; lastName: string; password: string },
redirectTo?: string,
defaultWorkspace?: Organization,
manager?: EntityManager
) => {
return dbTransactionWrap(async (manager: EntityManager) => {
@ -251,19 +252,28 @@ export class OnboardingUtilService implements IOnboardingUtilService {
case hasWorkspaceInviteButUserWantsInstanceSignup: {
const firstTimeSignup = ![SOURCE.SIGNUP, SOURCE.WORKSPACE_SIGNUP].includes(existingUser.source as SOURCE);
if (firstTimeSignup) {
if(defaultWorkspace) {
return this.updateExistingUserDefaultWorkspace({
password,
firstName,
lastName
},existingUser, defaultWorkspace, manager);
}
/* Invite user doing instance signup. So reset name fields and set password */
let defaultOrganizationId = existingUser.defaultOrganizationId;
const isPersonalWorkspaceAllowed =
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
'true';
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(existingUser.id);
if (personalWorkspaces.length) {
defaultOrganizationId = personalWorkspaces[0].organizationId;
} else {
/* Create a personal workspace for the user */
const { name, slug } = generateNextNameAndSlug('My workspace');
const defaultOrganization = await this.organizationRepository.createOne(name, slug, manager);
const defaultOrganization = await this.organizationRepository.createOne({ name, slug }, manager);
defaultOrganizationId = defaultOrganization.id;
await this.organizationUserRepository.createOne(existingUser, defaultOrganization, true, manager);
}
@ -272,7 +282,6 @@ export class OnboardingUtilService implements IOnboardingUtilService {
userId: existingUser.id,
});
}
await this.userRepository.updateOne(
existingUser.id,
{
@ -398,7 +407,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
let personalWorkspace: Organization;
if (isPersonalWorkspaceEnabled) {
const { name, slug } = generateNextNameAndSlug('My workspace');
personalWorkspace = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
personalWorkspace = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
}
const organizationRole = personalWorkspace ? USER_ROLE.ADMIN : USER_ROLE.END_USER;
@ -604,4 +613,130 @@ export class OnboardingUtilService implements IOnboardingUtilService {
manager
);
}
createUserInDefaultWorkspace = async (
userParams: { email: string; password: string; firstName: string; lastName: string },
defaultWorkspace: Organization,
redirectTo?: string,
manager?: EntityManager
) => {
return await dbTransactionWrap(async (manager: EntityManager) => {
const { email, password, firstName, lastName } = userParams;
if (!defaultWorkspace) {
throw new Error('No default workspace found in the instance');
}
// Create user with end-user role in default workspace
const lifeCycleParms = getUserStatusAndSource(lifecycleEvents.USER_SIGN_UP);
const user = await this.create(
{
email,
password,
...(firstName && { firstName }),
...(lastName && { lastName }),
...lifeCycleParms,
},
defaultWorkspace.id,
USER_ROLE.END_USER,
null,
true,
null,
manager,
false
);
// Create organization user entry
await this.organizationUserRepository.createOne(
user,
defaultWorkspace,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
// Validate license
await this.licenseUserService.validateUser(manager);
// Send welcome email
this.eventEmitter.emit('emailEvent', {
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
payload: {
to: user.email,
name: user.firstName,
invitationtoken: user.invitationToken,
},
});
return {};
}, manager);
};
updateExistingUserDefaultWorkspace = async (
userParams: { password: string; firstName: string; lastName: string },
existingUser: User,
defaultWorkspace: Organization,
manager?: EntityManager
) => {
return await dbTransactionWrap(async (manager: EntityManager) => {
const { password, firstName, lastName } = userParams;
// Create organization user entry if not exists
const existingOrgUser = await this.organizationUserRepository.findOne({
where: {
userId: existingUser.id,
organizationId: defaultWorkspace.id,
}
});
if(existingOrgUser){
throw new NotAcceptableException(
'The user is already registered. Please check your inbox for the activation link'
);
}
// Update user's default organization ID
await this.userRepository.updateOne(
existingUser.id,
{
password,
firstName,
lastName,
source: SOURCE.SIGNUP,
defaultOrganizationId: defaultWorkspace.id,
},
manager
);
await this.organizationUserRepository.createOne(
existingUser,
defaultWorkspace,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
// Add end-user role in default workspace if not already present
await this.rolesUtilService.addUserRole(
defaultWorkspace.id,
{ role: USER_ROLE.END_USER, userId: existingUser.id },
manager
);
// Validate license
await this.licenseUserService.validateUser(manager);
// send welcome email
this.eventEmitter.emit('emailEvent', {
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
payload: {
to: existingUser.email,
name: existingUser.firstName,
invitationtoken: existingUser.invitationToken,
},
});
return {};
}, manager);
};
}

View file

@ -7,6 +7,7 @@ import {
lifecycleEvents,
USER_STATUS,
USER_TYPE,
WORKSPACE_USER_SOURCE,
WORKSPACE_USER_STATUS,
} from '@modules/users/constants/lifecycle';
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
@ -212,7 +213,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
async createDefaultOrganization(manager: EntityManager) {
const { name, slug } = generateNextNameAndSlug('My workspace');
return await this.setupOrganizationsUtilService.create(name, slug, null, manager);
return await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
}
addUserAsAdmin(userId: string, organizationId: string, manager: EntityManager) {
@ -343,7 +344,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
async personalWorkspaces(userId: string): Promise<OrganizationUser[]> {
const personalWorkspaces: Partial<OrganizationUser[]> = await this.organizationUsersRepository.find({
select: ['organizationId', 'invitationToken'],
select: ['organizationId', 'invitationToken', 'id'],
where: { userId },
});
const personalWorkspaceArray: OrganizationUser[] = [];
@ -578,4 +579,41 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
user.organizationUserSource = organizationUser.source;
return user;
}
addUserToWorkspace = async (
user: User,
workspace: Organization,
manager?: EntityManager
) => {
return await dbTransactionWrap(async (manager: EntityManager) => {
// Create organization user entry if not exists
let existingOrgUser = await this.organizationUsersRepository.findOne({
where: {
userId: user.id,
organizationId: workspace.id,
}
});
if(existingOrgUser){
return existingOrgUser;
}
const organizationUser = await this.organizationUsersRepository.createOne(
user,
workspace,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
// Add end-user role in default workspace if not already present
await this.rolesUtilService.addUserRole(
workspace.id,
{ role: USER_ROLE.END_USER, userId: user.id },
manager
);
return organizationUser;
}, manager);
};
}

View file

@ -44,7 +44,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
can([FEATURE_KEY.UPDATE, FEATURE_KEY.GET, FEATURE_KEY.CHECK_UNIQUE], Organization);
}
if (superAdmin) {
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE], Organization);
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE, FEATURE_KEY.SET_DEFAULT], Organization);
}
}
}

View file

@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = {
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: {
isPublic: true,
},
[FEATURE_KEY.SET_DEFAULT]: {},
},
};

View file

@ -26,4 +26,5 @@ export enum FEATURE_KEY {
CHECK_UNIQUE = 'check_unique',
CREATE = 'create',
CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding',
SET_DEFAULT = 'set_default',
}

View file

@ -1,4 +1,4 @@
import { Body, Controller, Get, Patch, UseGuards, Query, Param } from '@nestjs/common';
import { Body, Controller, Get, Patch, UseGuards, Query, Param, NotImplementedException } from '@nestjs/common';
import { OrganizationsService } from '@modules/organizations/service';
import { decamelizeKeys } from 'humps';
import { User } from '@modules/app/decorators/user.decorator';
@ -17,7 +17,7 @@ import { OrganizationAuthGuard } from '@modules/session/guards/organization-auth
@Controller('organizations')
@InitModule(MODULES.ORGANIZATIONS)
export class OrganizationsController implements IOrganizationsController {
constructor(private organizationsService: OrganizationsService) {}
constructor(protected organizationsService: OrganizationsService) {}
@InitFeature(FEATURE_KEY.GET)
// TODO: Change to jwt auth guard - check why we need OrganizationAuthGuard here
@ -41,6 +41,15 @@ export class OrganizationsController implements IOrganizationsController {
await this.organizationsService.updateOrganizationNameAndSlug(user.organizationId, organizationUpdateDto);
return;
}
@InitFeature(FEATURE_KEY.SET_DEFAULT)
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
@Patch(':id/set-default')
async setDefaultWorkspace(@Param('id') id: string) {
await this.organizationsService.setDefaultWorkspace(id);
return;
}
// Note : This endpoint is used for archive/unarchive workspaces.
@InitFeature(FEATURE_KEY.WORKSPACE_STATUS_UPDATE)
@UseGuards(JwtAuthGuard)

View file

@ -11,4 +11,6 @@ export interface IOrganizationsController {
checkWorkspaceUnique(name: string, slug: string): Promise<void>;
checkUniqueWorkspaceName(name: string): Promise<void>;
setDefaultWorkspace(id: string): Promise<void>;
}

View file

@ -1,5 +1,6 @@
import { Organization } from 'src/entities/organization.entity';
import { OrganizationUpdateDto, OrganizationStatusUpdateDto } from '@modules/organizations/dto';
import { EntityManager } from 'typeorm';
export interface IOrganizationsService {
fetchOrganizations(
@ -15,4 +16,8 @@ export interface IOrganizationsService {
updateOrganizationStatus(organizationId: string, updatableData: OrganizationStatusUpdateDto): Promise<Organization>;
checkWorkspaceUniqueness(name: string, slug: string): Promise<void>;
checkWorkspaceNameUniqueness(name: string): Promise<void>;
setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void>;
}

View file

@ -7,6 +7,7 @@ import { catchDbException, isSuperAdmin } from '@helpers/utils.helper';
import { ConfigScope, SSOType } from '@entities/sso_config.entity';
import { WORKSPACE_STATUS, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle';
import { CONSTRAINTS } from './constants';
import { OrganizationInputs } from '@modules/setup-organization/types/organization-inputs';
@Injectable()
export class OrganizationRepository extends Repository<Organization> {
@ -106,7 +107,8 @@ export class OrganizationRepository extends Repository<Organization> {
}, manager);
}
createOne(name: string, slug: string, manager?: EntityManager): Promise<any> {
createOne(organizationInputs: OrganizationInputs, manager?: EntityManager): Promise<any> {
const { name, slug, isDefault } = organizationInputs;
return dbTransactionWrap((manager: EntityManager) => {
return catchDbException(() => {
return manager.save(
@ -120,6 +122,7 @@ export class OrganizationRepository extends Repository<Organization> {
],
name,
slug,
isDefault,
createdAt: new Date(),
updatedAt: new Date(),
})
@ -201,4 +204,27 @@ export class OrganizationRepository extends Repository<Organization> {
});
});
}
async getDefaultWorkspaceOfInstance(): Promise<Organization>{
return dbTransactionWrap(async (manager: EntityManager) => {
try {
return await manager.findOneOrFail(Organization, {
where: { isDefault: true },
});
} catch (error) {
console.error('No default workspace in this instance');
return null;
}
});
}
async changeDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
return await dbTransactionWrap(async (manager: EntityManager) => {
// First, unset any existing default workspace
await manager.update(Organization, { isDefault: true }, { isDefault: false });
// Then set the new default workspace
await manager.update(Organization, { id: organizationId }, { isDefault: true });
}, manager || this.manager);
}
}

View file

@ -1,4 +1,4 @@
import { ConflictException, Injectable, NotAcceptableException } from '@nestjs/common';
import { ConflictException, Injectable, NotAcceptableException, NotImplementedException } from '@nestjs/common';
import { Organization } from 'src/entities/organization.entity';
import { isSuperAdmin } from 'src/helpers/utils.helper';
import { dbTransactionWrap } from 'src/helpers/database.helper';
@ -51,6 +51,11 @@ export class OrganizationsService implements IOrganizationsService {
updatableData: OrganizationStatusUpdateDto
): Promise<Organization> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const organization = await this.organizationRepository.findOne({ where: { id: organizationId } });
if (organization.isDefault) {
throw new NotAcceptableException('Default workspace cannot be archived');
}
await this.organizationRepository.updateOne(organizationId, updatableData, manager);
if (updatableData.status === WORKSPACE_STATUS.ACTIVE) {
await this.licenseOrganizationService.validateOrganization(manager); //Check for only unarchiving
@ -85,4 +90,8 @@ export class OrganizationsService implements IOrganizationsService {
if (result) throw new ConflictException('Workspace name must be unique');
return;
}
async setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
throw new NotImplementedException('This feature is only available in Enterprise Edition');
}
}

View file

@ -9,6 +9,7 @@ interface Features {
[FEATURE_KEY.CREATE]: FeatureConfig;
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: FeatureConfig;
[FEATURE_KEY.WORKSPACE_STATUS_UPDATE]: FeatureConfig;
[FEATURE_KEY.SET_DEFAULT]: FeatureConfig;
}
export interface FeaturesConfig {

View file

@ -368,8 +368,8 @@ export class SessionUtilService {
async #onboardingFlags(user: User) {
let isFirstUserOnboardingCompleted = true;
let isOnboardingCompleted = true;
const isOnboardingQuestionsEnabled =
this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
// const isOnboardingQuestionsEnabled =
// this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
const instanceUsersCount = await this.userRepository.count({
where: { status: USER_STATUS.ACTIVE },
@ -383,14 +383,14 @@ export class SessionUtilService {
}
/* Signed up user check */
if (
instanceUsersCount > 1 &&
isOnboardingQuestionsEnabled &&
user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
) {
/* Signed up user went through onboarding flow, didn't complete */
isOnboardingCompleted = false;
}
// if (
// instanceUsersCount > 1 &&
// isOnboardingQuestionsEnabled &&
// user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
// ) {
// /* Signed up user went through onboarding flow, didn't complete */
// isOnboardingCompleted = false;
// }
return { isFirstUserOnboardingCompleted, isOnboardingCompleted };
}

View file

@ -29,8 +29,7 @@ export class SetupOrganizationsController implements ISetupOrganizationsControll
@Res({ passthrough: true }) response: Response
) {
const result = await this.setupOrganizationsService.create(
organizationCreateDto.name,
organizationCreateDto.slug,
{ name: organizationCreateDto.name, slug: organizationCreateDto.slug },
user
);

View file

@ -1,7 +1,8 @@
import { User } from 'src/entities/user.entity';
import { Organization } from 'src/entities/organization.entity';
import { EntityManager } from 'typeorm';
import { OrganizationInputs } from '../types/organization-inputs';
export interface ISetupOrganizationsService {
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
}

View file

@ -1,7 +1,8 @@
import { User } from 'src/entities/user.entity';
import { EntityManager } from 'typeorm';
import { Organization } from '@entities/organization.entity';
import { OrganizationInputs } from '../types/organization-inputs';
export interface ISetupOrganizationsUtilService {
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
}

View file

@ -4,12 +4,13 @@ import { User } from 'src/entities/user.entity';
import { EntityManager } from 'typeorm';
import { SetupOrganizationsUtilService } from './util.service';
import { ISetupOrganizationsService } from './interfaces/IService';
import { OrganizationInputs } from './types/organization-inputs';
@Injectable()
export class SetupOrganizationsService implements ISetupOrganizationsService {
constructor(protected readonly setupOrganizationsUtilService: SetupOrganizationsUtilService) {}
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
return this.setupOrganizationsUtilService.create(name, slug, user, manager);
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
return this.setupOrganizationsUtilService.create(organizationInputs, user, manager);
}
}

View file

@ -0,0 +1,5 @@
export interface OrganizationInputs {
name: string;
slug: string;
isDefault?: boolean;
}

View file

@ -15,6 +15,7 @@ import { OrganizationUsersRepository } from '@modules/organization-users/reposit
import { SampleDataSourceService } from '@modules/data-sources/services/sample-ds.service';
import { ISetupOrganizationsUtilService } from './interfaces/IUtilService';
import { TooljetDbTableOperationsService } from '@modules/tooljet-db/services/tooljet-db-table-operations.service';
import { OrganizationInputs } from './types/organization-inputs';
@Injectable()
export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilService {
@ -31,9 +32,9 @@ export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilSer
protected readonly organizationUserRepository: OrganizationUsersRepository
) {}
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const organization = await this.organizationRepository.createOne(name, slug, manager);
const organization = await this.organizationRepository.createOne(organizationInputs, manager);
await this.appEnvironmentUtilService.createDefaultEnvironments(organization.id, manager);
await this.groupPermissionUtilService.createDefaultGroups(organization.id, manager);