feat: multi-workspace (frontend) (#4232)

* select workspace component

* generateJWT mutation

* workspaces state and hooks

* requested changes

* mutation fix

* requested changes

* user workpsace delete call

* migration to drop and createt user workspace

* revert select props

* add DropdownMenu

* seperate multi-workspace dropdown as component

* Signup button displayed accurately

* update seed data for multi-workspace

* lint fix

* lint fix

* css fix

* lint fix

* state fix

* isDefined check

* refactor

* add default workspace constants for logo and name

* update migration

* lint fix

* isInviteMode check on sign-in/up

* removeWorkspaceMember mutation

* import fixes

* prop name fix

* backfill migration

* handle edge cases

* refactor

* remove migration query

* delete user on no-workspace found condition

* emit workspaceMember.deleted

* Fix event class and unrelated fix linked to a previously missing dependency

* Edit migration (I did it in prod manually)

* Revert changes

* Fix tests

* Fix conflicts

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Aditya Pimpalkar
2024-03-20 13:43:41 +00:00
committed by GitHub
parent 352192a63f
commit da12710fe9
29 changed files with 726 additions and 134 deletions

View File

@ -20,7 +20,9 @@ describe('FieldUtils', () => {
checkFields(objectMetadataItemMock, ['fieldNumber']),
).not.toThrow();
expect(() => checkFields(objectMetadataItemMock, ['wrongField'])).toThrow();
expect(() =>
checkFields(objectMetadataItemMock, ['wrongField']),
).toThrow();
expect(() =>
checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']),

View File

@ -4,6 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from 'src/engine/modules/user/user.entity';
import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity';
import { UserService } from './user.service';
@ -18,6 +19,10 @@ describe('UserService', () => {
provide: getRepositoryToken(User, 'core'),
useValue: {},
},
{
provide: getRepositoryToken(UserWorkspace, 'core'),
useValue: {},
},
{
provide: DataSourceService,
useValue: {},

View File

@ -8,12 +8,15 @@ import { User } from 'src/engine/modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/modules/user/dtos/workspace-member.dto';
import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity';
import { DataSourceEntity } from 'src/engine-metadata/data-source/data-source.entity';
export class UserService extends TypeOrmQueryService<User> {
constructor(
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
) {
@ -104,6 +107,8 @@ export class UserService extends TypeOrmQueryService<User> {
`DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`,
);
await this.userWorkspaceRepository.delete({ userId });
await this.userRepository.delete(user.id);
return user;

View File

@ -2,11 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Workspace } from 'src/engine/modules/workspace/workspace.entity';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/modules/user/user.entity';
import { BillingService } from 'src/engine/modules/billing/billing.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service';
import { BillingService } from 'src/engine/modules/billing/billing.service';
import { WorkspaceService } from './workspace.service';

View File

@ -6,13 +6,13 @@ import assert from 'assert';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { Workspace } from 'src/engine/modules/workspace/workspace.entity';
import { User } from 'src/engine/modules/user/user.entity';
import { ActivateWorkspaceInput } from 'src/engine/modules/workspace/dtos/activate-workspace-input';
import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/modules/user/user.entity';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service';
import { BillingService } from 'src/engine/modules/billing/billing.service';
import { ActivateWorkspaceInput } from 'src/engine/modules/workspace/dtos/activate-workspace-input';
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
@ -70,4 +70,118 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
.find()
.then((workspaces) => workspaces.map((workspace) => workspace.id));
}
private async reassignDefaultWorkspace(
currentWorkspaceId: string,
user: User,
worskpaces: UserWorkspace[],
) {
// We'll filter all user workspaces without the one which its getting removed from
const filteredUserWorkspaces = worskpaces.filter(
(workspace) => workspace.workspaceId !== currentWorkspaceId,
);
// Loop over each workspace in the filteredUserWorkspaces array and check if it currently exists in
// the database
for (let index = 0; index < filteredUserWorkspaces.length; index++) {
const userWorkspace = filteredUserWorkspaces[index];
const nextWorkspace = await this.workspaceRepository.findOneBy({
id: userWorkspace.workspaceId,
});
if (nextWorkspace) {
await this.userRepository.save({
id: user.id,
defaultWorkspace: nextWorkspace,
updatedAt: new Date().toISOString(),
});
break;
}
// if no workspaces are valid then we delete the user
if (index === filteredUserWorkspaces.length - 1) {
await this.userRepository.delete({ id: user.id });
}
}
}
/*
async removeWorkspaceMember(workspaceId: string, memberId: string) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
// using "SELECT *" here because we will need the corresponding members userId later
const [workspaceMember] = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`,
);
if (!workspaceMember) {
throw new NotFoundException('Member not found.');
}
await workspaceDataSource?.query(
`DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`,
);
const workspaceMemberUser = await this.userRepository.findOne({
where: {
id: workspaceMember.userId,
},
relations: ['defaultWorkspace'],
});
if (!workspaceMemberUser) {
throw new NotFoundException('User not found');
}
const userWorkspaces = await this.userWorkspaceRepository.find({
where: { userId: workspaceMemberUser.id },
relations: ['workspace'],
});
// We want to check if we the user has signed up to more than one workspace
if (userWorkspaces.length > 1) {
// We neeed to check if the workspace that its getting removed from is its default workspace, if it is then
// change the default workspace to point to the next workspace available.
if (workspaceMemberUser.defaultWorkspace.id === workspaceId) {
await this.reassignDefaultWorkspace(
workspaceId,
workspaceMemberUser,
userWorkspaces,
);
}
// if its not the default workspace then simply delete the user-workspace mapping
await this.userWorkspaceRepository.delete({
userId: workspaceMemberUser.id,
workspaceId,
});
} else {
await this.userWorkspaceRepository.delete({
userId: workspaceMemberUser.id,
});
// After deleting the user-workspace mapping, we have a condition where we have the users default workspace points to a
// workspace which it doesnt have access to. So we delete the user.
await this.userRepository.delete({ id: workspaceMemberUser.id });
}
const payload =
new ObjectRecordDeleteEvent<WorkspaceMemberObjectMetadata>();
payload.workspaceId = workspaceId;
payload.details = {
before: workspaceMember,
};
this.eventEmitter.emit('workspaceMember.deleted', payload);
return memberId;
}
*/
}

View File

@ -6,15 +6,16 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { WorkspaceResolver } from 'src/engine/modules/workspace/workspace.resolver';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity';
import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/modules/user/user.entity';
import { UserWorkspaceModule } from 'src/engine/modules/user-workspace/user-workspace.module';
import { BillingModule } from 'src/engine/modules/billing/billing.module';
import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity';
import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity';
import { UserWorkspaceModule } from 'src/engine/modules/user-workspace/user-workspace.module';
import { User } from 'src/engine/modules/user/user.entity';
import { DataSourceModule } from 'src/engine-metadata/data-source/data-source.module';
import { FileUploadModule } from 'src/engine/modules/file/file-upload/file-upload.module';
import { Workspace } from './workspace.entity';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { Workspace } from './workspace.entity';
import { WorkspaceService } from './services/workspace.service';
@ -31,6 +32,8 @@ import { WorkspaceService } from './services/workspace.service';
),
UserWorkspaceModule,
WorkspaceManagerModule,
DataSourceModule,
TypeORMModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,