6a1b28bc12
## What & why A single, consistent **workspace-creation step** for both multi-workspace and single-workspace self-host — collecting **name + logo** (and the **subdomain** in multi-workspace) — which **removes the duplicate name/logo prompt** that previously reappeared on the workspace subdomain (reported after #21641). ## Changes **One creation form for both modes** - With 0 workspaces, both multi-workspace and single-workspace route to the shared `SignInUpWorkspaceCreationForm`; `SignInUp` renders it for the `WorkspaceCreation` step regardless of domain/scope. - The subdomain field shows only in multi-workspace; single-workspace keeps its fixed address. **Logo on the creation step** - New scoped `uploadNewWorkspaceLogo(workspaceId, file)` mutation: the creator sets a logo on their just-created `PENDING_CREATION` workspace via the workspace-agnostic token (membership enforced — only the creator is a member at that point), reusing `uploadWorkspacePicture`. Upload size is capped via `settings.storage.maxFileSize` (also applied to the existing logo / profile-picture uploads). - The picked file is held locally (object-URL preview, revoked on unmount) and uploaded right after creation (non-fatal on failure). **Onboarding step → pure activation loader** - The old "Create your workspace" form (name + logo) is removed. The onboarding step now activates the pending workspace on mount and shows the loader, with a **Retry** action on failure. ## Testing - typecheck (front + server) ✅; oxlint + oxfmt clean on changed files ✅ - Unit tests: `auth.resolver.spec`, `useWorkspaceSubdomainField`, `SignInUpWorkspaceCreationForm` (multi + single-workspace), `useAuth` ✅ - Metadata GraphQL + `twenty-client-sdk` schema regenerated. Follow-up to #21641. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Xw37hR5seiCyWnppG9z4op --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
137 lines
4.8 KiB
TypeScript
137 lines
4.8 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { Repository } from 'typeorm';
|
|
|
|
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
|
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
|
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
|
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
|
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
|
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
|
import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants';
|
|
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
|
|
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
|
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant';
|
|
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
|
|
|
|
@Injectable()
|
|
export class WorkspaceManagerService {
|
|
private readonly logger = new Logger(WorkspaceManagerService.name);
|
|
|
|
constructor(
|
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
@InjectRepository(UserWorkspaceEntity)
|
|
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
|
private readonly roleService: RoleService,
|
|
private readonly userRoleService: UserRoleService,
|
|
private readonly twentyStandardApplicationService: TwentyStandardApplicationService,
|
|
@InjectRepository(WorkspaceEntity)
|
|
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
|
@InjectWorkspaceScopedRepository(RoleEntity)
|
|
private readonly roleRepository: WorkspaceScopedRepository<RoleEntity>,
|
|
private readonly applicationService: ApplicationService,
|
|
) {}
|
|
|
|
public async init({
|
|
workspace,
|
|
userId,
|
|
}: {
|
|
workspace: WorkspaceEntity;
|
|
userId: string;
|
|
}): Promise<void> {
|
|
const workspaceId = workspace.id;
|
|
const schemaCreationStart = performance.now();
|
|
const schemaName =
|
|
await this.workspaceDataSourceService.createWorkspaceDBSchema(
|
|
workspaceId,
|
|
);
|
|
|
|
const schemaCreationEnd = performance.now();
|
|
|
|
this.logger.log(
|
|
`Schema creation took ${schemaCreationEnd - schemaCreationStart}ms`,
|
|
);
|
|
|
|
const dataSourceMetadataCreationStart = performance.now();
|
|
|
|
await this.workspaceRepository.update(workspaceId, {
|
|
databaseSchema: schemaName,
|
|
});
|
|
|
|
await this.applicationService.createTwentyStandardApplication({
|
|
workspaceId,
|
|
});
|
|
|
|
await this.twentyStandardApplicationService.synchronizeTwentyStandardApplicationOrThrow(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
const dataSourceMetadataCreationEnd = performance.now();
|
|
|
|
this.logger.log(
|
|
`Metadata creation took ${dataSourceMetadataCreationEnd - dataSourceMetadataCreationStart}ms`,
|
|
);
|
|
|
|
const { workspaceCustomFlatApplication } =
|
|
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
await this.setupDefaultRoles({
|
|
workspaceId,
|
|
userId,
|
|
workspaceCustomFlatApplication,
|
|
});
|
|
}
|
|
|
|
private async setupDefaultRoles({
|
|
userId,
|
|
workspaceId,
|
|
workspaceCustomFlatApplication,
|
|
}: {
|
|
workspaceId: string;
|
|
userId: string;
|
|
workspaceCustomFlatApplication: FlatApplication;
|
|
}): Promise<void> {
|
|
const adminRole = await this.roleRepository.findOne(workspaceId, {
|
|
where: {
|
|
universalIdentifier: STANDARD_ROLE.admin.universalIdentifier,
|
|
},
|
|
});
|
|
|
|
if (adminRole) {
|
|
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
|
|
where: { workspaceId, userId },
|
|
});
|
|
|
|
await this.userRoleService.assignRoleToManyUserWorkspace({
|
|
workspaceId,
|
|
userWorkspaceIds: [userWorkspace.id],
|
|
roleId: adminRole.id,
|
|
});
|
|
}
|
|
|
|
const existingMemberRole = await this.roleRepository.findOne(workspaceId, {
|
|
where: { label: MEMBER_ROLE_LABEL },
|
|
});
|
|
|
|
const memberRole =
|
|
existingMemberRole ??
|
|
(await this.roleService.createMemberRole({
|
|
workspaceId,
|
|
ownerFlatApplication: workspaceCustomFlatApplication,
|
|
}));
|
|
|
|
await this.workspaceRepository.update(workspaceId, {
|
|
defaultRoleId: memberRole.id,
|
|
});
|
|
}
|
|
}
|