Add admin avatars and app logos (#20001)
## Summary - Add user avatars and workspace logos to the admin general table and workspace member detail view - Show app icons in the admin app registrations table and reuse the shared application display component - Expose the needed avatar and logo fields through admin GraphQL queries and backend lookup/statistics services - Keep workspace fallback behavior consistent when no logo is set and clean up a few local table styling duplicates ## Testing - `./node_modules/.bin/tsc -p packages/twenty-front/tsconfig.json --noEmit --pretty false` - Manual UI verification of the admin general, workspace detail, and apps tables --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
499067ae14
commit
fb8b9cb86c
@@ -33,6 +33,7 @@ import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-clie
|
||||
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
|
||||
import { UsageModule } from 'src/engine/core-modules/usage/usage.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@@ -71,6 +72,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
UsageModule,
|
||||
KeyValuePairModule,
|
||||
UserVarsModule,
|
||||
UserModule,
|
||||
],
|
||||
providers: [
|
||||
AdminPanelResolver,
|
||||
|
||||
+3
@@ -10,4 +10,7 @@ export class AdminPanelRecentUserDTO extends UserInfoDTO {
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
workspaceId?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
workspaceLogo?: string | null;
|
||||
}
|
||||
|
||||
+3
-3
@@ -7,6 +7,9 @@ export class AdminPanelTopWorkspaceDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logoUrl?: string | null;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@@ -15,7 +18,4 @@ export class AdminPanelTopWorkspaceDTO {
|
||||
|
||||
@Field(() => String)
|
||||
subdomain: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logo: string | null;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ export class UserInfoDTO {
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
+161
-70
@@ -1,16 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Brackets, ILike, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { type AdminPanelRecentUserDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-recent-user.dto';
|
||||
import { type AdminPanelTopWorkspaceDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-top-workspace.dto';
|
||||
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
const RECENT_USERS_LIMIT = 10;
|
||||
const TOP_WORKSPACES_LIMIT = 10;
|
||||
|
||||
@Injectable()
|
||||
export class AdminPanelStatisticsService {
|
||||
constructor(
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly userService: UserService,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepository: Repository<UserEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
@@ -20,90 +28,173 @@ export class AdminPanelStatisticsService {
|
||||
async getRecentUsers(
|
||||
searchTerm?: string,
|
||||
): Promise<AdminPanelRecentUserDTO[]> {
|
||||
let whereClause = 'u."deletedAt" IS NULL';
|
||||
const params: unknown[] = [];
|
||||
const trimmedSearch = searchTerm?.trim();
|
||||
|
||||
if (searchTerm && searchTerm.trim().length > 0) {
|
||||
const term = `%${searchTerm.trim()}%`;
|
||||
const queryBuilder = this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.leftJoinAndSelect(
|
||||
'user.userWorkspaces',
|
||||
'userWorkspace',
|
||||
'"userWorkspace"."deletedAt" IS NULL',
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
'userWorkspace.workspace',
|
||||
'workspace',
|
||||
'"workspace"."deletedAt" IS NULL',
|
||||
)
|
||||
.where({ deletedAt: IsNull() })
|
||||
.orderBy('user.createdAt', 'DESC')
|
||||
.addOrderBy('userWorkspace.createdAt', 'DESC')
|
||||
.take(RECENT_USERS_LIMIT);
|
||||
|
||||
whereClause += ` AND (u.email ILIKE $1 OR CONCAT(u."firstName", ' ', u."lastName") ILIKE $1 OR u.id::text ILIKE $1)`;
|
||||
params.push(term);
|
||||
if (trimmedSearch && trimmedSearch.length > 0) {
|
||||
const like = `%${trimmedSearch}%`;
|
||||
|
||||
queryBuilder.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where({ email: ILike(like) })
|
||||
.orWhere(
|
||||
`CONCAT("user"."firstName", ' ', "user"."lastName") ILIKE :like`,
|
||||
{ like },
|
||||
)
|
||||
.orWhere('"user"."id"::text ILIKE :like', { like });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const results = await this.userRepository.manager.query(
|
||||
`SELECT * FROM (
|
||||
SELECT DISTINCT ON (u.id) u.id, u.email, u."firstName", u."lastName", u."createdAt",
|
||||
w."displayName" AS "workspaceName", w.id AS "workspaceId"
|
||||
FROM core."user" u
|
||||
LEFT JOIN core."userWorkspace" uw ON uw."userId" = u.id AND uw."deletedAt" IS NULL
|
||||
LEFT JOIN core.workspace w ON w.id = uw."workspaceId" AND w."deletedAt" IS NULL
|
||||
WHERE ${whereClause}
|
||||
ORDER BY u.id, u."createdAt" DESC
|
||||
) sub
|
||||
ORDER BY sub."createdAt" DESC
|
||||
LIMIT 10`,
|
||||
params,
|
||||
);
|
||||
const users = await queryBuilder.getMany();
|
||||
|
||||
return results.map(
|
||||
(row: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
createdAt: Date;
|
||||
workspaceName: string | null;
|
||||
workspaceId: string | null;
|
||||
}) => ({
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
firstName: row.firstName || null,
|
||||
lastName: row.lastName || null,
|
||||
createdAt: row.createdAt,
|
||||
workspaceName: row.workspaceName ?? null,
|
||||
workspaceId: row.workspaceId ?? null,
|
||||
}),
|
||||
);
|
||||
const signedAvatarUrlByUserId =
|
||||
await this.buildSignedAvatarUrlByUserId(users);
|
||||
|
||||
return users.map((user) => {
|
||||
const displayWorkspace = user.userWorkspaces[0]?.workspace;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName ?? undefined,
|
||||
lastName: user.lastName ?? undefined,
|
||||
createdAt: user.createdAt,
|
||||
avatarUrl: signedAvatarUrlByUserId.get(user.id) ?? null,
|
||||
workspaceName: displayWorkspace?.displayName ?? null,
|
||||
workspaceId: displayWorkspace?.id ?? null,
|
||||
workspaceLogo: displayWorkspace
|
||||
? this.fileUrlService.signWorkspaceLogoUrl(displayWorkspace)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getTopWorkspaces(
|
||||
searchTerm?: string,
|
||||
): Promise<AdminPanelTopWorkspaceDTO[]> {
|
||||
let whereClause = 'w."deletedAt" IS NULL';
|
||||
const params: unknown[] = [];
|
||||
const trimmedSearch = searchTerm?.trim();
|
||||
|
||||
if (searchTerm && searchTerm.trim().length > 0) {
|
||||
const term = `%${searchTerm.trim()}%`;
|
||||
const queryBuilder = this.workspaceRepository
|
||||
.createQueryBuilder('workspace')
|
||||
.leftJoin(
|
||||
'workspace.workspaceUsers',
|
||||
'userWorkspace',
|
||||
'"userWorkspace"."deletedAt" IS NULL',
|
||||
)
|
||||
.select('workspace.id', 'id')
|
||||
.addSelect('workspace.displayName', 'name')
|
||||
.addSelect('workspace.subdomain', 'subdomain')
|
||||
.addSelect('workspace.logoFileId', 'logoFileId')
|
||||
.addSelect('COUNT("userWorkspace"."id")::int', 'totalUsers')
|
||||
.where({ deletedAt: IsNull() })
|
||||
.groupBy('workspace.id')
|
||||
.orderBy('"totalUsers"', 'DESC')
|
||||
.limit(TOP_WORKSPACES_LIMIT);
|
||||
|
||||
whereClause += ` AND (w."displayName" ILIKE $1 OR w.subdomain ILIKE $1 OR w.id::text ILIKE $1)`;
|
||||
params.push(term);
|
||||
if (trimmedSearch && trimmedSearch.length > 0) {
|
||||
const like = `%${trimmedSearch}%`;
|
||||
|
||||
queryBuilder.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('"workspace"."displayName" ILIKE :like', { like })
|
||||
.orWhere('"workspace"."subdomain" ILIKE :like', { like })
|
||||
.orWhere('"workspace"."id"::text ILIKE :like', { like });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const results = await this.workspaceRepository.manager.query(
|
||||
`SELECT w.id, w."displayName" AS name, w.subdomain, w.logo, COUNT(uw.id)::int AS "totalUsers"
|
||||
FROM core.workspace w
|
||||
LEFT JOIN core."userWorkspace" uw ON uw."workspaceId" = w.id AND uw."deletedAt" IS NULL
|
||||
WHERE ${whereClause}
|
||||
GROUP BY w.id
|
||||
ORDER BY "totalUsers" DESC
|
||||
LIMIT 10`,
|
||||
params,
|
||||
const rows: Array<{
|
||||
id: string;
|
||||
name: string | null;
|
||||
subdomain: string | null;
|
||||
logoFileId: string | null;
|
||||
totalUsers: number;
|
||||
}> = await queryBuilder.getRawMany();
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
logoUrl: this.fileUrlService.signWorkspaceLogoUrl({
|
||||
id: row.id,
|
||||
logoFileId: row.logoFileId,
|
||||
}),
|
||||
name: row.name ?? '',
|
||||
subdomain: row.subdomain ?? '',
|
||||
totalUsers: row.totalUsers,
|
||||
}));
|
||||
}
|
||||
|
||||
private async buildSignedAvatarUrlByUserId(
|
||||
users: UserEntity[],
|
||||
): Promise<Map<string, string | null>> {
|
||||
const signedAvatarUrlByUserId = new Map<string, string | null>();
|
||||
const contextsByWorkspaceId = new Map<
|
||||
string,
|
||||
{
|
||||
workspace: WorkspaceEntity;
|
||||
fallbackAvatarUrlsByUserId: Map<string, string | null>;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const user of users) {
|
||||
signedAvatarUrlByUserId.set(user.id, user.defaultAvatarUrl ?? null);
|
||||
|
||||
for (const userWorkspace of user.userWorkspaces) {
|
||||
const workspace = userWorkspace.workspace;
|
||||
|
||||
if (!workspace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = contextsByWorkspaceId.get(workspace.id) ?? {
|
||||
workspace,
|
||||
fallbackAvatarUrlsByUserId: new Map(),
|
||||
};
|
||||
|
||||
entry.fallbackAvatarUrlsByUserId.set(
|
||||
user.id,
|
||||
userWorkspace.defaultAvatarUrl ?? user.defaultAvatarUrl ?? null,
|
||||
);
|
||||
contextsByWorkspaceId.set(workspace.id, entry);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Array.from(contextsByWorkspaceId.values()).map(
|
||||
async ({ workspace, fallbackAvatarUrlsByUserId }) => {
|
||||
const perWorkspaceSigned =
|
||||
await this.userService.loadSignedAvatarUrlsByUserId({
|
||||
workspace,
|
||||
fallbackAvatarUrlsByUserId,
|
||||
});
|
||||
|
||||
for (const [userId, signedUrl] of perWorkspaceSigned.entries()) {
|
||||
const existing = signedAvatarUrlByUserId.get(userId);
|
||||
|
||||
if (!isNonEmptyString(existing) && isNonEmptyString(signedUrl)) {
|
||||
signedAvatarUrlByUserId.set(userId, signedUrl);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return results.map(
|
||||
(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
subdomain: string;
|
||||
logo: string | null;
|
||||
totalUsers: number;
|
||||
}) => ({
|
||||
id: row.id,
|
||||
name: row.name ?? '',
|
||||
subdomain: row.subdomain ?? '',
|
||||
logo: row.logo ?? null,
|
||||
totalUsers: row.totalUsers,
|
||||
}),
|
||||
);
|
||||
return signedAvatarUrlByUserId;
|
||||
}
|
||||
}
|
||||
|
||||
+88
-53
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FeatureFlagKey, FileFolder } from 'twenty-shared/types';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@@ -16,6 +16,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@@ -24,6 +25,7 @@ export class AdminPanelUserLookupService {
|
||||
constructor(
|
||||
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly userService: UserService,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userRepository: Repository<UserEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
@@ -34,6 +36,21 @@ export class AdminPanelUserLookupService {
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
) {}
|
||||
|
||||
private buildFallbackAvatarUrlsByUserId(
|
||||
workspaceUsers: UserWorkspaceEntity[],
|
||||
): Map<string, string | null> {
|
||||
return new Map(
|
||||
workspaceUsers
|
||||
.filter((workspaceUser) => isDefined(workspaceUser.user))
|
||||
.map((workspaceUser) => [
|
||||
workspaceUser.user.id,
|
||||
workspaceUser.defaultAvatarUrl ??
|
||||
workspaceUser.user.defaultAvatarUrl ??
|
||||
null,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
async userLookup(userIdentifier: string): Promise<UserLookup> {
|
||||
const isEmail = userIdentifier.includes('@');
|
||||
const normalizedIdentifier = isEmail
|
||||
@@ -65,6 +82,53 @@ export class AdminPanelUserLookupService {
|
||||
|
||||
const allFeatureFlagKeys = Object.values(FeatureFlagKey);
|
||||
|
||||
const workspaces = await Promise.all(
|
||||
targetUser.userWorkspaces.map(async (userWorkspace) => {
|
||||
const workspaceUsers = userWorkspace.workspace.workspaceUsers.filter(
|
||||
(workspaceUser) => isDefined(workspaceUser.user),
|
||||
);
|
||||
const avatarUrlsByUserId =
|
||||
await this.userService.loadSignedAvatarUrlsByUserId({
|
||||
workspace: userWorkspace.workspace,
|
||||
fallbackAvatarUrlsByUserId:
|
||||
this.buildFallbackAvatarUrlsByUserId(workspaceUsers),
|
||||
});
|
||||
|
||||
return {
|
||||
id: userWorkspace.workspace.id,
|
||||
name: userWorkspace.workspace.displayName ?? '',
|
||||
totalUsers: workspaceUsers.length,
|
||||
activationStatus: userWorkspace.workspace.activationStatus,
|
||||
createdAt: userWorkspace.workspace.createdAt,
|
||||
logo:
|
||||
this.fileUrlService.signWorkspaceLogoUrl(userWorkspace.workspace) ??
|
||||
undefined,
|
||||
allowImpersonation: userWorkspace.workspace.allowImpersonation,
|
||||
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({
|
||||
subdomain: userWorkspace.workspace.subdomain,
|
||||
customDomain: userWorkspace.workspace.customDomain,
|
||||
isCustomDomainEnabled:
|
||||
userWorkspace.workspace.isCustomDomainEnabled,
|
||||
}),
|
||||
users: workspaceUsers.map((workspaceUser) => ({
|
||||
id: workspaceUser.user.id,
|
||||
email: workspaceUser.user.email,
|
||||
firstName: workspaceUser.user.firstName,
|
||||
lastName: workspaceUser.user.lastName,
|
||||
avatarUrl: avatarUrlsByUserId.get(workspaceUser.user.id) ?? null,
|
||||
createdAt: workspaceUser.user.createdAt,
|
||||
})),
|
||||
featureFlags: allFeatureFlagKeys.map((key) => ({
|
||||
key,
|
||||
value:
|
||||
userWorkspace.workspace.featureFlags?.find(
|
||||
(flag) => flag.key === key,
|
||||
)?.value ?? false,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: targetUser.id,
|
||||
@@ -73,42 +137,7 @@ export class AdminPanelUserLookupService {
|
||||
lastName: targetUser.lastName,
|
||||
createdAt: targetUser.createdAt,
|
||||
},
|
||||
workspaces: targetUser.userWorkspaces.map((userWorkspace) => ({
|
||||
id: userWorkspace.workspace.id,
|
||||
name: userWorkspace.workspace.displayName ?? '',
|
||||
totalUsers: userWorkspace.workspace.workspaceUsers.length,
|
||||
activationStatus: userWorkspace.workspace.activationStatus,
|
||||
createdAt: userWorkspace.workspace.createdAt,
|
||||
logo: isDefined(userWorkspace.workspace.logoFileId)
|
||||
? this.fileUrlService.signFileByIdUrl({
|
||||
fileId: userWorkspace.workspace.logoFileId,
|
||||
workspaceId: userWorkspace.workspace.id,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
})
|
||||
: undefined,
|
||||
allowImpersonation: userWorkspace.workspace.allowImpersonation,
|
||||
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({
|
||||
subdomain: userWorkspace.workspace.subdomain,
|
||||
customDomain: userWorkspace.workspace.customDomain,
|
||||
isCustomDomainEnabled: userWorkspace.workspace.isCustomDomainEnabled,
|
||||
}),
|
||||
users: userWorkspace.workspace.workspaceUsers
|
||||
.filter((workspaceUser) => isDefined(workspaceUser.user))
|
||||
.map((workspaceUser) => ({
|
||||
id: workspaceUser.user.id,
|
||||
email: workspaceUser.user.email,
|
||||
firstName: workspaceUser.user.firstName,
|
||||
lastName: workspaceUser.user.lastName,
|
||||
createdAt: workspaceUser.user.createdAt,
|
||||
})),
|
||||
featureFlags: allFeatureFlagKeys.map((key) => ({
|
||||
key,
|
||||
value:
|
||||
userWorkspace.workspace.featureFlags?.find(
|
||||
(flag) => flag.key === key,
|
||||
)?.value ?? false,
|
||||
})),
|
||||
})),
|
||||
workspaces,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,6 +167,16 @@ export class AdminPanelUserLookupService {
|
||||
]);
|
||||
|
||||
const allFeatureFlagKeys = Object.values(FeatureFlagKey);
|
||||
const definedWorkspaceUsers = workspaceUsers.filter((wu) =>
|
||||
isDefined(wu.user),
|
||||
);
|
||||
const avatarUrlsByUserId =
|
||||
await this.userService.loadSignedAvatarUrlsByUserId({
|
||||
workspace,
|
||||
fallbackAvatarUrlsByUserId: this.buildFallbackAvatarUrlsByUserId(
|
||||
definedWorkspaceUsers,
|
||||
),
|
||||
});
|
||||
|
||||
const workspaceInfo = {
|
||||
id: workspace.id,
|
||||
@@ -145,28 +184,21 @@ export class AdminPanelUserLookupService {
|
||||
totalUsers: workspaceUsers.length,
|
||||
activationStatus: workspace.activationStatus,
|
||||
createdAt: workspace.createdAt,
|
||||
logo: isDefined(workspace.logoFileId)
|
||||
? this.fileUrlService.signFileByIdUrl({
|
||||
fileId: workspace.logoFileId,
|
||||
workspaceId: workspace.id,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
})
|
||||
: undefined,
|
||||
logo: this.fileUrlService.signWorkspaceLogoUrl(workspace) ?? undefined,
|
||||
allowImpersonation: workspace.allowImpersonation,
|
||||
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({
|
||||
subdomain: workspace.subdomain,
|
||||
customDomain: workspace.customDomain,
|
||||
isCustomDomainEnabled: workspace.isCustomDomainEnabled,
|
||||
}),
|
||||
users: workspaceUsers
|
||||
.filter((wu) => isDefined(wu.user))
|
||||
.map((wu) => ({
|
||||
id: wu.user.id,
|
||||
email: wu.user.email,
|
||||
firstName: wu.user.firstName,
|
||||
lastName: wu.user.lastName,
|
||||
createdAt: wu.user.createdAt,
|
||||
})),
|
||||
users: definedWorkspaceUsers.map((wu) => ({
|
||||
id: wu.user.id,
|
||||
email: wu.user.email,
|
||||
firstName: wu.user.firstName,
|
||||
lastName: wu.user.lastName,
|
||||
avatarUrl: avatarUrlsByUserId.get(wu.user.id) ?? null,
|
||||
createdAt: wu.user.createdAt,
|
||||
})),
|
||||
featureFlags: allFeatureFlagKeys.map((key) => ({
|
||||
key,
|
||||
value: featureFlags.find((flag) => flag.key === key)?.value ?? false,
|
||||
@@ -181,6 +213,9 @@ export class AdminPanelUserLookupService {
|
||||
email: firstUser?.email ?? '',
|
||||
firstName: firstUser?.firstName,
|
||||
lastName: firstUser?.lastName,
|
||||
avatarUrl: firstUser
|
||||
? (avatarUrlsByUserId.get(firstUser.id) ?? null)
|
||||
: null,
|
||||
createdAt: firstUser?.createdAt ?? new Date(),
|
||||
},
|
||||
workspaces: [workspaceInfo],
|
||||
|
||||
Reference in New Issue
Block a user