diff --git a/README.md b/README.md index 37295b4a..11730fef 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Else, refer to the following sections. | ------------------------------ | ------------------------------ | | `dfx deploy frontend` | Deploy to a local DFX replica | | `pnpm turbo start -F frontend` | Run a local development server | +| `pnpm turbo build -F frontend` | Create a prod build | | `pnpm turbo test -F frontend` | Run unit tests | ### Docs diff --git a/src/frontend/src/app/core/api/index.ts b/src/frontend/src/app/core/api/index.ts index 80a451f2..515d09ce 100644 --- a/src/frontend/src/app/core/api/index.ts +++ b/src/frontend/src/app/core/api/index.ts @@ -1,3 +1,4 @@ export * from './commit-review'; +export * from './profile'; export * from './proposal'; export * from './review'; diff --git a/src/frontend/src/app/core/api/profile/index.ts b/src/frontend/src/app/core/api/profile/index.ts new file mode 100644 index 00000000..181fd12a --- /dev/null +++ b/src/frontend/src/app/core/api/profile/index.ts @@ -0,0 +1,2 @@ +export * from './profile-api.model'; +export * from './profile-api.service'; diff --git a/src/frontend/src/app/core/api/profile/profile-api.mapper.ts b/src/frontend/src/app/core/api/profile/profile-api.mapper.ts new file mode 100644 index 00000000..cdc710e4 --- /dev/null +++ b/src/frontend/src/app/core/api/profile/profile-api.mapper.ts @@ -0,0 +1,178 @@ +import { Ok, toCandidOpt } from '../../utils'; +import { + GetMyUserProfileResponse as GetMyUserProfileApiResponse, + UpdateMyUserProfileRequest as UpdateMyUserProfileApiRequest, + SocialLink as ApiSocialLink, + SocialLinkPlatform as ApiSocialLinkPlatform, +} from '@cg/backend'; +import { + GetMyUserProfileResponse, + SocialMediaLink, + SocialMediaLinkType, + UpdateMyUserProfileRequest, + UserRole, +} from './profile-api.model'; + +export function mapGetMyUserProfileResponse( + res: Ok, +): GetMyUserProfileResponse { + if ('reviewer' in res.config) { + const config = res.config.reviewer; + + return { + role: UserRole.Reviewer, + id: res.id, + username: res.username, + bio: config.bio, + neuronId: config.neuron_id, + walletAddress: config.wallet_address, + socialMedia: mapSocialLinksResponse(config.social_links), + }; + } else if ('admin' in res.config) { + return { + role: UserRole.Admin, + id: res.id, + username: res.username, + bio: res.config.admin.bio, + }; + } else { + return { + role: UserRole.Anonymous, + id: res.id, + username: res.username, + }; + } +} + +function mapSocialLinksResponse( + socialLinks: ApiSocialLink[], +): SocialMediaLink[] { + return socialLinks.map(link => ({ + type: mapSocialLinkPlatformResponse(link.platform), + username: link.username, + })); +} + +function mapSocialLinkPlatformResponse( + platform: ApiSocialLinkPlatform, +): SocialMediaLinkType { + if ('dscvr' in platform) { + return SocialMediaLinkType.DSCVR; + } + + if ('openchat' in platform) { + return SocialMediaLinkType.OpenChat; + } + + if ('taggr' in platform) { + return SocialMediaLinkType.Taggr; + } + + if ('x' in platform) { + return SocialMediaLinkType.X; + } + + if ('github' in platform) { + return SocialMediaLinkType.Github; + } + + if ('dfinityforum' in platform) { + return SocialMediaLinkType.DfinityForum; + } + + if ('discord' in platform) { + return SocialMediaLinkType.Discord; + } + + if ('website' in platform) { + return SocialMediaLinkType.Website; + } + + throw new Error(`Unknown social link platform: ${JSON.stringify(platform)}`); +} + +export function mapUpdateMyUserProfileRequest( + req: UpdateMyUserProfileRequest, +): UpdateMyUserProfileApiRequest { + switch (req.role) { + case UserRole.Anonymous: { + return { + username: toCandidOpt(req.username), + config: toCandidOpt(), + }; + } + + case UserRole.Reviewer: { + return { + username: toCandidOpt(req.username), + config: toCandidOpt( + req.bio || req.socialMedia || req.walletAddress + ? { + reviewer: { + bio: toCandidOpt(req.bio), + social_links: toCandidOpt( + mapSocialLinksRequest(req.socialMedia), + ), + wallet_address: toCandidOpt(req.walletAddress), + }, + } + : undefined, + ), + }; + } + + case UserRole.Admin: { + return { + username: toCandidOpt(req.username), + config: toCandidOpt( + req.bio + ? { + admin: { + bio: toCandidOpt(req.bio), + }, + } + : undefined, + ), + }; + } + } +} + +function mapSocialLinksRequest( + links?: SocialMediaLink[], +): ApiSocialLink[] | undefined { + return links?.map(link => ({ + platform: mapSocialLinkPlatformRequest(link.type), + username: link.username, + })); +} + +function mapSocialLinkPlatformRequest( + platform: SocialMediaLinkType, +): ApiSocialLinkPlatform { + switch (platform) { + case SocialMediaLinkType.DSCVR: + return { dscvr: null }; + + case SocialMediaLinkType.OpenChat: + return { openchat: null }; + + case SocialMediaLinkType.Taggr: + return { taggr: null }; + + case SocialMediaLinkType.X: + return { x: null }; + + case SocialMediaLinkType.Github: + return { github: null }; + + case SocialMediaLinkType.DfinityForum: + return { dfinityforum: null }; + + case SocialMediaLinkType.Discord: + return { discord: null }; + + case SocialMediaLinkType.Website: + return { website: null }; + } +} diff --git a/src/frontend/src/app/core/api/profile/profile-api.model.ts b/src/frontend/src/app/core/api/profile/profile-api.model.ts new file mode 100644 index 00000000..b3f126d3 --- /dev/null +++ b/src/frontend/src/app/core/api/profile/profile-api.model.ts @@ -0,0 +1,85 @@ +/** + * Generic types + */ + +export enum UserRole { + Anonymous = 'Anonymous', + Reviewer = 'Reviewer', + Admin = 'Admin', +} + +export enum SocialMediaLinkType { + DSCVR = 'DSCVR', + OpenChat = 'OpenChat', + Taggr = 'Taggr', + X = 'X', + Github = 'Github', + DfinityForum = 'DfinityForum', + Discord = 'Discord', + Website = 'Website', +} + +export interface SocialMediaLink { + type: SocialMediaLinkType; + username: string; +} + +/** + * GetMyUserProfile types + */ + +export interface BaseGetMyUserProfileResponse { + role: T; + id: string; + username: string; +} + +export type AnonymousGetMyUserProfileResponse = + BaseGetMyUserProfileResponse; + +export interface ReviewerGetMyUserProfileResponse + extends BaseGetMyUserProfileResponse { + neuronId: bigint; + walletAddress: string; + bio: string; + socialMedia: SocialMediaLink[]; +} + +export interface AdminGetMyUserProfileResponse + extends BaseGetMyUserProfileResponse { + bio: string; +} + +export type GetMyUserProfileResponse = + | AnonymousGetMyUserProfileResponse + | ReviewerGetMyUserProfileResponse + | AdminGetMyUserProfileResponse; + +/** + * UpdateMyUserProfile types + */ + +export interface BaseUpdateMyUserProfileRequest { + role: T; + username?: string; +} + +export type AnonymousUpdateMyUserProfileRequest = + BaseUpdateMyUserProfileRequest; + +export interface ReviewerUpdateMyUserProfileRequest + extends BaseUpdateMyUserProfileRequest { + bio?: string; + socialMedia?: SocialMediaLink[]; + walletAddress?: string; +} + +export interface AdminUpdateMyUserProfileRequest + extends BaseUpdateMyUserProfileRequest { + bio?: string; +} + +export type UpdateMyUserProfileRequest = + | AnonymousUpdateMyUserProfileRequest + | ReviewerUpdateMyUserProfileRequest + | AdminUpdateMyUserProfileRequest; diff --git a/src/frontend/src/app/core/api/profile/profile-api.service.mock.ts b/src/frontend/src/app/core/api/profile/profile-api.service.mock.ts new file mode 100644 index 00000000..f58e988f --- /dev/null +++ b/src/frontend/src/app/core/api/profile/profile-api.service.mock.ts @@ -0,0 +1,11 @@ +import { ProfileApiService } from './profile-api.service'; + +export type ProfileApiServiceMock = jasmine.SpyObj; + +export function profileApiServiceMockFactory(): ProfileApiServiceMock { + return jasmine.createSpyObj('ProfileApiService', [ + 'getMyUserProfile', + 'updateMyUserProfile', + 'createMyUserProfile', + ]); +} diff --git a/src/frontend/src/app/core/api/profile/profile-api.service.spec.ts b/src/frontend/src/app/core/api/profile/profile-api.service.spec.ts new file mode 100644 index 00000000..cb1947be --- /dev/null +++ b/src/frontend/src/app/core/api/profile/profile-api.service.spec.ts @@ -0,0 +1,410 @@ +import { TestBed } from '@angular/core/testing'; + +import { BackendActorService } from '../../services'; +import { + GetMyUserProfileResponse as GetMyUserProfileApiResponse, + UpdateMyUserProfileRequest as UpdateMyUserProfileApiRequest, + SocialLink as ApiSocialLink, +} from '@cg/backend'; +import { + BackendActorServiceMock, + backendActorServiceMockFactory, +} from '~core/services/backend-actor-service-mock'; +import { + AdminGetMyUserProfileResponse, + AnonymousGetMyUserProfileResponse, + ReviewerGetMyUserProfileResponse, + SocialMediaLink, + SocialMediaLinkType, + UpdateMyUserProfileRequest, + UserRole, +} from './profile-api.model'; +import { ProfileApiService } from './profile-api.service'; + +describe('ProfileApiService', () => { + let service: ProfileApiService; + let backendActorServiceMock: BackendActorServiceMock; + + const commonSocialMediaLinks: SocialMediaLink[] = [ + { type: SocialMediaLinkType.DSCVR, username: 'dscvr_username' }, + { + type: SocialMediaLinkType.DfinityForum, + username: 'forum_username', + }, + { + type: SocialMediaLinkType.Discord, + username: 'discord_username', + }, + { + type: SocialMediaLinkType.Github, + username: 'github_username', + }, + { + type: SocialMediaLinkType.OpenChat, + username: 'oc_username', + }, + { + type: SocialMediaLinkType.Taggr, + username: 'taggr_username', + }, + { + type: SocialMediaLinkType.Website, + username: 'website_username', + }, + { + type: SocialMediaLinkType.X, + username: 'x_username', + }, + ]; + const commonApiSocialMediaLinks: ApiSocialLink[] = [ + { platform: { dscvr: null }, username: 'dscvr_username' }, + { + platform: { dfinityforum: null }, + username: 'forum_username', + }, + { platform: { discord: null }, username: 'discord_username' }, + { platform: { github: null }, username: 'github_username' }, + { platform: { openchat: null }, username: 'oc_username' }, + { platform: { taggr: null }, username: 'taggr_username' }, + { platform: { website: null }, username: 'website_username' }, + { platform: { x: null }, username: 'x_username' }, + ]; + + beforeEach(() => { + backendActorServiceMock = backendActorServiceMockFactory(); + + TestBed.configureTestingModule({ + providers: [ + { provide: BackendActorService, useValue: backendActorServiceMock }, + ], + }); + + service = TestBed.inject(ProfileApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getMyUserProfile()', () => { + it('should return an anonymous user profile', async () => { + const res: AnonymousGetMyUserProfileResponse = { + id: 'id', + role: UserRole.Anonymous, + username: 'username', + }; + const apiRes: GetMyUserProfileApiResponse = { + ok: { + id: 'id', + username: 'username', + config: { + anonymous: null, + }, + }, + }; + + backendActorServiceMock.get_my_user_profile.and.resolveTo(apiRes); + + const result = await service.getMyUserProfile(); + expect(result).toEqual(res); + }); + + it('should return an admin user profile', async () => { + const res: AdminGetMyUserProfileResponse = { + id: 'id', + role: UserRole.Admin, + username: 'username', + bio: 'bio', + }; + const apiRes: GetMyUserProfileApiResponse = { + ok: { + id: 'id', + username: 'username', + config: { + admin: { + bio: 'bio', + }, + }, + }, + }; + + backendActorServiceMock.get_my_user_profile.and.resolveTo(apiRes); + + const result = await service.getMyUserProfile(); + expect(result).toEqual(res); + }); + + it('should return a reviewer user profile', async () => { + const res: ReviewerGetMyUserProfileResponse = { + id: 'id', + role: UserRole.Reviewer, + username: 'username', + bio: 'bio', + neuronId: 1n, + walletAddress: 'walletAddress', + socialMedia: commonSocialMediaLinks, + }; + const apiRes: GetMyUserProfileApiResponse = { + ok: { + id: 'id', + username: 'username', + config: { + reviewer: { + bio: 'bio', + neuron_id: 1n, + wallet_address: 'walletAddress', + social_links: commonApiSocialMediaLinks, + }, + }, + }, + }; + + backendActorServiceMock.get_my_user_profile.and.resolveTo(apiRes); + + const result = await service.getMyUserProfile(); + expect(result).toEqual(res); + }); + }); + + describe('updateMyUserProfile()', () => { + type TestCase = [ + string, + UpdateMyUserProfileRequest, + UpdateMyUserProfileApiRequest, + ]; + + const testCases: TestCase[] = [ + [ + 'anonymous', + { + role: UserRole.Anonymous, + username: 'username', + }, + { + username: ['username'], + config: [], + }, + ], + [ + 'anonymous without username', + { + role: UserRole.Anonymous, + }, + { + username: [], + config: [], + }, + ], + [ + 'admin', + { + role: UserRole.Admin, + username: 'username', + bio: 'bio', + }, + { + username: ['username'], + config: [ + { + admin: { + bio: ['bio'], + }, + }, + ], + }, + ], + [ + 'admin without username', + { + role: UserRole.Admin, + bio: 'bio', + }, + { + username: [], + config: [ + { + admin: { + bio: ['bio'], + }, + }, + ], + }, + ], + [ + 'admin without bio', + { + role: UserRole.Admin, + username: 'username', + }, + { + username: ['username'], + config: [], + }, + ], + [ + 'admin without bio or username', + { + role: UserRole.Admin, + }, + { + username: [], + config: [], + }, + ], + [ + 'reviewer', + { + role: UserRole.Reviewer, + bio: 'bio', + username: 'username', + walletAddress: 'walletAddress', + socialMedia: commonSocialMediaLinks, + }, + { + username: ['username'], + config: [ + { + reviewer: { + bio: ['bio'], + wallet_address: ['walletAddress'], + social_links: [commonApiSocialMediaLinks], + }, + }, + ], + }, + ], + [ + 'reviewer without bio', + { + role: UserRole.Reviewer, + username: 'username', + walletAddress: 'walletAddress', + socialMedia: commonSocialMediaLinks, + }, + { + username: ['username'], + config: [ + { + reviewer: { + bio: [], + wallet_address: ['walletAddress'], + social_links: [commonApiSocialMediaLinks], + }, + }, + ], + }, + ], + [ + 'reviewer without username', + { + role: UserRole.Reviewer, + bio: 'bio', + walletAddress: 'walletAddress', + socialMedia: commonSocialMediaLinks, + }, + { + username: [], + config: [ + { + reviewer: { + bio: ['bio'], + wallet_address: ['walletAddress'], + social_links: [commonApiSocialMediaLinks], + }, + }, + ], + }, + ], + [ + 'reviewer without wallet address', + { + role: UserRole.Reviewer, + bio: 'bio', + username: 'username', + socialMedia: commonSocialMediaLinks, + }, + { + username: ['username'], + config: [ + { + reviewer: { + bio: ['bio'], + wallet_address: [], + social_links: [commonApiSocialMediaLinks], + }, + }, + ], + }, + ], + [ + 'reviewer without social links', + { + role: UserRole.Reviewer, + bio: 'bio', + username: 'username', + walletAddress: 'walletAddress', + }, + { + username: ['username'], + config: [ + { + reviewer: { + bio: ['bio'], + wallet_address: ['walletAddress'], + social_links: [], + }, + }, + ], + }, + ], + [ + 'reviewer without bio, username, wallet address, or social links', + { + role: UserRole.Reviewer, + }, + { + username: [], + config: [], + }, + ], + ]; + + testCases.forEach(([description, req, apiReq]) => { + it(`should update a user profile: ${description}`, async () => { + backendActorServiceMock.update_my_user_profile.and.resolveTo({ + ok: null, + }); + + const result = await service.updateMyUserProfile(req); + expect(result).toBeNull(); + expect( + backendActorServiceMock.update_my_user_profile, + ).toHaveBeenCalledWith(apiReq); + }); + }); + }); + + describe('createMyUserProfile()', () => { + it('should create an anonymous user profile', async () => { + const res: AnonymousGetMyUserProfileResponse = { + id: 'id', + role: UserRole.Anonymous, + username: 'username', + }; + const apiRes: GetMyUserProfileApiResponse = { + ok: { + id: 'id', + username: 'username', + config: { + anonymous: null, + }, + }, + }; + + backendActorServiceMock.create_my_user_profile.and.resolveTo(apiRes); + + const result = await service.createMyUserProfile(); + expect(result).toEqual(res); + }); + }); +}); diff --git a/src/frontend/src/app/core/api/profile/profile-api.service.ts b/src/frontend/src/app/core/api/profile/profile-api.service.ts new file mode 100644 index 00000000..5f14a5f0 --- /dev/null +++ b/src/frontend/src/app/core/api/profile/profile-api.service.ts @@ -0,0 +1,44 @@ +import { inject, Injectable } from '@angular/core'; + +import { BackendActorService } from '../../services'; +import { handleErr } from '../../utils'; +import { + mapGetMyUserProfileResponse, + mapUpdateMyUserProfileRequest, +} from './profile-api.mapper'; +import { + GetMyUserProfileResponse, + UpdateMyUserProfileRequest, +} from './profile-api.model'; + +@Injectable({ + providedIn: 'root', +}) +export class ProfileApiService { + private readonly actorService = inject(BackendActorService); + + public async getMyUserProfile(): Promise { + const res = await this.actorService.get_my_user_profile(); + const okRes = handleErr(res); + + return mapGetMyUserProfileResponse(okRes); + } + + public async updateMyUserProfile( + req: UpdateMyUserProfileRequest, + ): Promise { + const apiReq = mapUpdateMyUserProfileRequest(req); + + const res = await this.actorService.update_my_user_profile(apiReq); + const okRes = handleErr(res); + + return okRes; + } + + public async createMyUserProfile(): Promise { + const res = await this.actorService.create_my_user_profile(); + const okRes = handleErr(res); + + return mapGetMyUserProfileResponse(okRes); + } +} diff --git a/src/frontend/src/app/core/api/review/review-api.service.ts b/src/frontend/src/app/core/api/review/review-api.service.ts index 7f463050..c51812a0 100644 --- a/src/frontend/src/app/core/api/review/review-api.service.ts +++ b/src/frontend/src/app/core/api/review/review-api.service.ts @@ -86,7 +86,7 @@ export class ReviewApiService { try { return await this.getMyProposalReview(req); } catch (error) { - if (error instanceof ApiError && error.err.code === 404) { + if (error instanceof ApiError && error.code === 404) { return await this.createProposalReview(req); } diff --git a/src/frontend/src/app/core/state/profile/index.ts b/src/frontend/src/app/core/state/profile/index.ts index 2232fb36..7fd606d3 100644 --- a/src/frontend/src/app/core/state/profile/index.ts +++ b/src/frontend/src/app/core/state/profile/index.ts @@ -1,2 +1 @@ export * from './profile.service'; -export * from './profile.model'; diff --git a/src/frontend/src/app/core/state/profile/profile.mapper.ts b/src/frontend/src/app/core/state/profile/profile.mapper.ts deleted file mode 100644 index 6d83e84b..00000000 --- a/src/frontend/src/app/core/state/profile/profile.mapper.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { - CreateMyUserProfileResponse, - GetMyUserProfileResponse, - UpdateMyUserProfileRequest, - SocialLink as ApiSocialLink, - SocialLinkPlatform, -} from '@cg/backend'; -import { Ok, toCandidOpt } from '~core/utils'; -import { - Profile, - ProfileUpdate, - SocialLink, - SocialMediaType, - UserRole, -} from './profile.model'; - -export function mapProfileResponse( - apiResponse: Ok | Ok, -): Profile { - if ('reviewer' in apiResponse.config) { - const config = apiResponse.config.reviewer; - - return { - role: UserRole.Reviewer, - id: apiResponse.id, - username: apiResponse.username, - bio: config.bio, - neuronId: config.neuron_id, - walletAddress: config.wallet_address, - socialMedia: mapSocialLinksResponse(config.social_links), - }; - } else if ('admin' in apiResponse.config) { - return { - role: UserRole.Admin, - id: apiResponse.id, - username: apiResponse.username, - bio: apiResponse.config.admin.bio, - }; - } else { - return { - role: UserRole.Anonymous, - id: apiResponse.id, - username: apiResponse.username, - }; - } -} - -export function mapSocialLinksResponse( - apiResponse: ApiSocialLink[], -): SocialLink[] { - return apiResponse.map(link => ({ - type: mapSocialLinkPlatform(link.platform), - username: link.username, - })); -} - -export function mapSocialLinkPlatform( - platform: SocialLinkPlatform, -): SocialMediaType { - if ('dscvr' in platform) { - return SocialMediaType.DSCVR; - } - - if ('openchat' in platform) { - return SocialMediaType.OpenChat; - } - - if ('taggr' in platform) { - return SocialMediaType.Taggr; - } - - if ('x' in platform) { - return SocialMediaType.X; - } - - if ('github' in platform) { - return SocialMediaType.Github; - } - - if ('dfinityforum' in platform) { - return SocialMediaType.DfinityForum; - } - - if ('discord' in platform) { - return SocialMediaType.Discord; - } - - if ('website' in platform) { - return SocialMediaType.Website; - } - - throw new Error(`Unknown social link platform: ${JSON.stringify(platform)}`); -} - -export function mapUpdateProfileRequest( - profile: ProfileUpdate, -): UpdateMyUserProfileRequest { - switch (profile.role) { - case UserRole.Anonymous: - default: { - return { - username: toCandidOpt(profile.username), - config: [], - }; - } - - case UserRole.Reviewer: { - return { - username: toCandidOpt(profile.username), - config: toCandidOpt( - profile.bio || profile.walletAddress || profile.socialMedia - ? { - reviewer: { - bio: toCandidOpt(profile.bio), - wallet_address: toCandidOpt(profile.walletAddress), - social_links: toCandidOpt( - mapProfileUpdateSocialLinksRequest(profile.socialMedia), - ), - }, - } - : null, - ), - }; - } - - case UserRole.Admin: { - return { - username: toCandidOpt(profile.username), - config: toCandidOpt( - profile.bio - ? { - admin: { - bio: toCandidOpt(profile.bio), - }, - } - : null, - ), - }; - } - } -} - -export function mapProfileUpdateSocialLinksRequest( - socialLinks: SocialLink[] | undefined, -): ApiSocialLink[] { - if (!socialLinks) { - return []; - } - - return socialLinks.map(link => ({ - platform: mapSocialLinkType(link.type), - username: link.username, - })); -} - -export function mapSocialLinkType(type: SocialMediaType): SocialLinkPlatform { - switch (type) { - case SocialMediaType.DSCVR: - return { dscvr: null }; - case SocialMediaType.OpenChat: - return { openchat: null }; - case SocialMediaType.Taggr: - return { taggr: null }; - case SocialMediaType.X: - return { x: null }; - case SocialMediaType.Github: - return { github: null }; - case SocialMediaType.DfinityForum: - return { dfinityforum: null }; - case SocialMediaType.Discord: - return { discord: null }; - case SocialMediaType.Website: - return { website: null }; - default: - throw new Error(`Unknown social link type: ${type}`); - } -} - -export function mergeProfileUpdate( - profile: Profile, - profileUpdate: ProfileUpdate, -): Profile { - // create a new object reference so Angular will detect the changes - profile = { ...profile }; - - if ( - profile.role === UserRole.Anonymous && - profileUpdate.role === UserRole.Anonymous - ) { - if (profileUpdate.username) { - profile.username = profileUpdate.username; - } - } else if ( - profile.role === UserRole.Reviewer && - profileUpdate.role === UserRole.Reviewer - ) { - if (profileUpdate.username) { - profile.username = profileUpdate.username; - } - if (profileUpdate.bio) { - profile.bio = profileUpdate.bio; - } - if (profileUpdate.walletAddress) { - profile.walletAddress = profileUpdate.walletAddress; - } - if (profileUpdate.socialMedia) { - profile.socialMedia = profileUpdate.socialMedia; - } - } else if ( - profile.role === UserRole.Admin && - profileUpdate.role === UserRole.Admin - ) { - if (profileUpdate.username) { - profile.username = profileUpdate.username; - } - if (profileUpdate.bio) { - profile.bio = profileUpdate.bio; - } - } else { - throw new Error('Users cannot change their own role'); - } - - return profile; -} diff --git a/src/frontend/src/app/core/state/profile/profile.model.ts b/src/frontend/src/app/core/state/profile/profile.model.ts deleted file mode 100644 index 2e4af67d..00000000 --- a/src/frontend/src/app/core/state/profile/profile.model.ts +++ /dev/null @@ -1,65 +0,0 @@ -export enum UserRole { - Anonymous = 'Anonymous', - Reviewer = 'Reviewer', - Admin = 'Admin', -} - -export interface BaseProfile { - role: T; - id: string; - username: string; -} - -export type AnonymousProfile = BaseProfile; - -export interface ReviewerProfile extends BaseProfile { - neuronId: bigint; - walletAddress: string; - bio: string; - socialMedia: SocialLink[]; -} - -export interface AdminProfile extends BaseProfile { - bio: string; -} - -export type Profile = AnonymousProfile | ReviewerProfile | AdminProfile; - -export interface BaseProfileUpdate { - role: T; - username?: string; -} - -export type AnonymousProfileUpdate = BaseProfileUpdate; - -export interface ReviewerProfileUpdate - extends BaseProfileUpdate { - bio?: string; - socialMedia?: SocialLink[]; - walletAddress?: string; -} - -export interface AdminProfileUpdate extends BaseProfileUpdate { - bio?: string; -} - -export type ProfileUpdate = - | AnonymousProfileUpdate - | ReviewerProfileUpdate - | AdminProfileUpdate; - -export interface SocialLink { - type: SocialMediaType; - username: string; -} - -export enum SocialMediaType { - DSCVR = 'DSCVR', - OpenChat = 'OpenChat', - Taggr = 'Taggr', - X = 'X', - Github = 'Github', - DfinityForum = 'DfinityForum', - Discord = 'Discord', - Website = 'Website', -} diff --git a/src/frontend/src/app/core/state/profile/profile.service.spec.ts b/src/frontend/src/app/core/state/profile/profile.service.spec.ts index ef1ad408..9cb346f8 100644 --- a/src/frontend/src/app/core/state/profile/profile.service.spec.ts +++ b/src/frontend/src/app/core/state/profile/profile.service.spec.ts @@ -1,19 +1,41 @@ -import { backendActorServiceMockFactory } from '~core/services/backend-actor-service-mock'; -import { dialogMockFactory, routerMockFactory } from '~testing'; +import { Dialog } from '@angular/cdk/dialog'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { ProfileApiService } from '../../api'; +import { + ProfileApiServiceMock, + profileApiServiceMockFactory, +} from '~core/api/profile/profile-api.service.mock'; +import { + DialogMock, + dialogMockFactory, + RouterMock, + routerMockFactory, +} from '~testing'; import { ProfileService } from './profile.service'; describe('ProfileService', () => { let service: ProfileService; - const backendActorServiceMock = backendActorServiceMockFactory(); - const routerMock = routerMockFactory(); - const dialogMock = dialogMockFactory(); + + let profileApiServiceMock: ProfileApiServiceMock; + let routerMock: RouterMock; + let dialogMock: DialogMock; beforeEach(() => { - service = new ProfileService( - backendActorServiceMock, - routerMock, - dialogMock, - ); + profileApiServiceMock = profileApiServiceMockFactory(); + routerMock = routerMockFactory(); + dialogMock = dialogMockFactory(); + + TestBed.configureTestingModule({ + providers: [ + { provide: ProfileApiService, useValue: profileApiServiceMock }, + { provide: Router, useValue: routerMock }, + { provide: Dialog, useValue: dialogMock }, + ], + }); + + service = TestBed.inject(ProfileService); }); it('should be created', () => { diff --git a/src/frontend/src/app/core/state/profile/profile.service.ts b/src/frontend/src/app/core/state/profile/profile.service.ts index 6ec219a4..40857e23 100644 --- a/src/frontend/src/app/core/state/profile/profile.service.ts +++ b/src/frontend/src/app/core/state/profile/profile.service.ts @@ -1,27 +1,31 @@ import { Dialog } from '@angular/cdk/dialog'; -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, map } from 'rxjs'; -import { BackendActorService } from '~core/services'; +import { + GetMyUserProfileResponse, + ProfileApiService, + UpdateMyUserProfileRequest, + UserRole, +} from '~core/api'; import { LoadingDialogComponent, LoadingDialogInput, getLoadingDialogConfig, } from '~core/ui'; -import { isErr, isNil, isNotNil, isOk } from '~core/utils'; -import { - mapProfileResponse, - mapUpdateProfileRequest, - mergeProfileUpdate, -} from './profile.mapper'; -import { Profile, ProfileUpdate, UserRole } from './profile.model'; +import { ApiError, isNil, isNotNil } from '~core/utils'; @Injectable({ providedIn: 'root', }) export class ProfileService { - private userProfileSubject = new BehaviorSubject(null); + private readonly profileApiService = inject(ProfileApiService); + private readonly router = inject(Router); + private readonly dialog = inject(Dialog); + + private readonly userProfileSubject = + new BehaviorSubject(null); public readonly userProfile$ = this.userProfileSubject.asObservable(); public readonly userRole$ = this.userProfile$.pipe( @@ -40,58 +44,45 @@ export class ProfileService { message: 'Creating new profile...', }; - constructor( - private readonly actorService: BackendActorService, - private readonly router: Router, - private readonly dialog: Dialog, - ) {} - public async loadProfile(): Promise { const currentProfile = this.userProfileSubject.getValue(); if (isNotNil(currentProfile)) { return; } - const getResponse = await this.actorService.get_my_user_profile(); - - if (isOk(getResponse)) { - this.userProfileSubject.next(mapProfileResponse(getResponse.ok)); - return; - } - - if (getResponse.err.code !== 404) { - throw new Error(`${getResponse.err.code}: ${getResponse.err.message}`); - } - - const loadingDialog = this.dialog.open( - LoadingDialogComponent, - getLoadingDialogConfig(this.createProfileMessage), - ); - try { - await this.createProfile(); - } finally { - loadingDialog.close(); + const res = await this.profileApiService.getMyUserProfile(); + this.userProfileSubject.next(res); + } catch (error) { + if (error instanceof ApiError && error.code === 404) { + const loadingDialog = this.dialog.open( + LoadingDialogComponent, + getLoadingDialogConfig(this.createProfileMessage), + ); + + try { + await this.createProfile(); + this.router.navigate(['profile/edit']); + } finally { + loadingDialog.close(); + } + + return; + } + + throw error; } - - this.router.navigate(['profile/edit']); } - public async saveProfile(profileUpdate: ProfileUpdate): Promise { + public async saveProfile( + profileUpdate: UpdateMyUserProfileRequest, + ): Promise { const currentProfile = this.userProfileSubject.getValue(); if (isNil(currentProfile)) { throw new Error('User profile not loaded yet'); } - const updateResponse = await this.actorService.update_my_user_profile( - mapUpdateProfileRequest(profileUpdate), - ); - - if (isErr(updateResponse)) { - throw new Error( - `${updateResponse.err.code}: ${updateResponse.err.message}`, - ); - } + await this.profileApiService.updateMyUserProfile(profileUpdate); this.userProfileSubject.next( mergeProfileUpdate(currentProfile, profileUpdate), @@ -99,13 +90,55 @@ export class ProfileService { } private async createProfile(): Promise { - const createResponse = await this.actorService.create_my_user_profile(); - if (isErr(createResponse)) { - throw new Error( - `${createResponse.err.code}: ${createResponse.err.message}`, - ); - } + const res = await this.profileApiService.createMyUserProfile(); - this.userProfileSubject.next(mapProfileResponse(createResponse.ok)); + this.userProfileSubject.next(res); + } +} + +function mergeProfileUpdate( + profile: GetMyUserProfileResponse, + profileUpdate: UpdateMyUserProfileRequest, +): GetMyUserProfileResponse { + // create a new object reference so Angular will detect the changes + profile = { ...profile }; + + if ( + profile.role === UserRole.Anonymous && + profileUpdate.role === UserRole.Anonymous + ) { + if (profileUpdate.username) { + profile.username = profileUpdate.username; + } + } else if ( + profile.role === UserRole.Reviewer && + profileUpdate.role === UserRole.Reviewer + ) { + if (profileUpdate.username) { + profile.username = profileUpdate.username; + } + if (profileUpdate.bio) { + profile.bio = profileUpdate.bio; + } + if (profileUpdate.walletAddress) { + profile.walletAddress = profileUpdate.walletAddress; + } + if (profileUpdate.socialMedia) { + profile.socialMedia = profileUpdate.socialMedia; + } + } else if ( + profile.role === UserRole.Admin && + profileUpdate.role === UserRole.Admin + ) { + if (profileUpdate.username) { + profile.username = profileUpdate.username; + } + if (profileUpdate.bio) { + profile.bio = profileUpdate.bio; + } + } else { + throw new Error('Users cannot change their own role'); } + + return profile; } diff --git a/src/frontend/src/app/core/utils/api-response.ts b/src/frontend/src/app/core/utils/api-response.ts index 32197263..9a565826 100644 --- a/src/frontend/src/app/core/utils/api-response.ts +++ b/src/frontend/src/app/core/utils/api-response.ts @@ -29,8 +29,14 @@ export function extractOkResponse(res: ApiResponse): T { } export class ApiError extends Error { - constructor(public readonly err: Err) { + public readonly code: number; + public readonly message: string; + + constructor(err: Err) { super(`${err.code}: ${err.message}`); + + this.code = err.code; + this.message = err.message; } } diff --git a/src/frontend/src/app/core/utils/candid.ts b/src/frontend/src/app/core/utils/candid.ts index e98dcc7f..350d3232 100644 --- a/src/frontend/src/app/core/utils/candid.ts +++ b/src/frontend/src/app/core/utils/candid.ts @@ -1,7 +1,7 @@ -import { isNil } from './nil'; +import { isNil, isNotNil } from './nil'; export function toCandidOpt(value?: T | undefined | null): [] | [T] { - return value ? [value] : []; + return isNotNil(value) ? [value] : []; } export function fromCandidOpt(value: [] | [T] | undefined | null): T | null { diff --git a/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.spec.ts b/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.spec.ts index c7190bf6..ffe2024b 100644 --- a/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ProfileService, UserRole } from '~core/state'; +import { UserRole } from '~core/api'; +import { ProfileService } from '~core/state'; import { ProfileServiceMock } from '~core/state/profile/profile.service.mock'; import { AdminPersonalInfoFormComponent } from './admin-personal-info-form.component'; diff --git a/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.ts b/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.ts index 2594c7f5..11502072 100644 --- a/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.ts +++ b/src/frontend/src/app/pages/profile-edit/admin-personal-info-form/admin-personal-info-form.component.ts @@ -15,11 +15,11 @@ import { } from '@angular/forms'; import { - AdminProfile, - AdminProfileUpdate, - ProfileService, + AdminGetMyUserProfileResponse, + AdminUpdateMyUserProfileRequest, UserRole, -} from '~core/state'; +} from '~core/api'; +import { ProfileService } from '~core/state'; import { FormFieldComponent, FormValidationInfoComponent, @@ -121,7 +121,7 @@ interface AdminProfileForm { `, }) export class AdminPersonalInfoFormComponent { - public readonly userProfile = input.required(); + public readonly userProfile = input.required(); public readonly formClose = output(); @@ -156,7 +156,7 @@ export class AdminPersonalInfoFormComponent { const formValues = this.profileForm().value; - const profileUpdate: AdminProfileUpdate = { + const profileUpdate: AdminUpdateMyUserProfileRequest = { role: UserRole.Admin, username: formValues.username, bio: formValues.bio, diff --git a/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.spec.ts b/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.spec.ts index 7c1d4f43..ad612dac 100644 --- a/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserRole } from '~core/state'; +import { UserRole } from '~core/api'; import { AdminPersonalInfoComponent } from './admin-personal-info.component'; describe('AdminPersonalInfoComponent', () => { diff --git a/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.ts b/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.ts index ed1571ce..a42d4db5 100644 --- a/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.ts +++ b/src/frontend/src/app/pages/profile-edit/admin-personal-info/admin-personal-info.component.ts @@ -5,12 +5,12 @@ import { output, } from '@angular/core'; +import { AdminGetMyUserProfileResponse } from '~core/api'; import { KeyColComponent, KeyValueGridComponent, ValueColComponent, -} from '../../../core/ui'; -import { AdminProfile } from '~core/state'; +} from '~core/ui'; @Component({ selector: 'app-admin-personal-info', @@ -36,7 +36,7 @@ import { AdminProfile } from '~core/state'; `, }) export class AdminPersonalInfoComponent { - public readonly userProfile = input.required(); + public readonly userProfile = input.required(); public readonly edit = output(); diff --git a/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.spec.ts b/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.spec.ts index 973c2b37..6d4df1d7 100644 --- a/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.spec.ts @@ -1,7 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterModule } from '@angular/router'; -import { ProfileService, UserRole } from '~core/state'; +import { UserRole } from '~core/api'; +import { ProfileService } from '~core/state'; import { ProfileServiceMock, profileServiceMockFactory, diff --git a/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.ts b/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.ts index 9a6628fd..f64d1bf7 100644 --- a/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.ts +++ b/src/frontend/src/app/pages/profile-edit/admin-profile/admin-profile.component.ts @@ -9,8 +9,8 @@ import { import { AdminPersonalInfoComponent } from '../admin-personal-info'; import { AdminPersonalInfoFormComponent } from '../admin-personal-info-form'; import { CardComponent } from '@cg/angular-ui'; +import { AdminGetMyUserProfileResponse } from '~core/api'; import { InfoIconComponent } from '~core/icons'; -import { AdminProfile } from '~core/state'; import { KeyColComponent, KeyValueGridComponent, @@ -94,7 +94,7 @@ import { `, }) export class AdminProfileComponent { - public readonly userProfile = input.required(); + public readonly userProfile = input.required(); public readonly adminInfo = signal( 'Use DFX command to change this property.', diff --git a/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.spec.ts b/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.spec.ts index 4baf0d2b..65b58de8 100644 --- a/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserRole } from '~core/state'; +import { UserRole } from '~core/api'; import { AnonymousProfileComponent } from './anonymous-profile.component'; describe('AnonymousProfileComponent', () => { diff --git a/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.ts b/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.ts index a554a76a..3e9a6aea 100644 --- a/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.ts +++ b/src/frontend/src/app/pages/profile-edit/anonymous-profile/anonymous-profile.component.ts @@ -7,7 +7,7 @@ import { import { GLOBAL_CONFIG } from '../../../../global-config'; import { CardComponent, CopyToClipboardComponent } from '@cg/angular-ui'; -import { AnonymousProfile } from '~core/state'; +import { AnonymousGetMyUserProfileResponse } from '~core/api'; @Component({ selector: 'app-anonymous-profile', @@ -38,7 +38,8 @@ import { AnonymousProfile } from '~core/state'; `, }) export class AnonymousProfileComponent { - public readonly userProfile = input.required(); + public readonly userProfile = + input.required(); public readonly applyLink = signal(GLOBAL_CONFIG.applyLink); } diff --git a/src/frontend/src/app/pages/profile-edit/profile-edit.component.ts b/src/frontend/src/app/pages/profile-edit/profile-edit.component.ts index e0b6c09d..a6437ca1 100644 --- a/src/frontend/src/app/pages/profile-edit/profile-edit.component.ts +++ b/src/frontend/src/app/pages/profile-edit/profile-edit.component.ts @@ -4,9 +4,8 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; -import { InfoIconComponent } from '~core/icons'; -import { ProfileService, UserRole } from '~core/state'; -import { FormFieldComponent, InputDirective } from '~core/ui'; +import { UserRole } from '~core/api'; +import { ProfileService } from '~core/state'; import { AdminProfileComponent } from './admin-profile'; import { AnonymousProfileComponent } from './anonymous-profile'; import { ReviewerProfileComponent } from './reviewer-profile'; @@ -18,9 +17,6 @@ import { ReviewerProfileComponent } from './reviewer-profile'; ReactiveFormsModule, CommonModule, RouterModule, - InfoIconComponent, - FormFieldComponent, - InputDirective, AnonymousProfileComponent, ReviewerProfileComponent, AdminProfileComponent, diff --git a/src/frontend/src/app/pages/profile-edit/profile.model.ts b/src/frontend/src/app/pages/profile-edit/profile.model.ts index 2b330920..f6a62105 100644 --- a/src/frontend/src/app/pages/profile-edit/profile.model.ts +++ b/src/frontend/src/app/pages/profile-edit/profile.model.ts @@ -1,7 +1,7 @@ -import { SocialMediaType } from '~core/state'; +import { SocialMediaLinkType } from '~core/api'; export type SocialMediaInputs = { - [K in SocialMediaType]: SocialMediaInputProps; + [K in SocialMediaLinkType]: SocialMediaInputProps; }; export const SOCIAL_MEDIA_INPUTS: SocialMediaInputs = { diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.spec.ts b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.spec.ts index f5cd29b7..aa43fdad 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ProfileService, UserRole, SocialMediaType } from '~core/state'; +import { UserRole, SocialMediaLinkType } from '~core/api'; +import { ProfileService } from '~core/state'; import { ProfileServiceMock } from '~core/state/profile/profile.service.mock'; import { ReviewerPersonalInfoFormComponent } from './reviewer-personal-info-form.component'; @@ -24,7 +25,7 @@ describe('ReviewerPersonalInfoFormComponent', () => { neuronId: 10685924793606457081n, walletAddress: '123213123sdfsdfs', bio: 'bio', - socialMedia: [{ type: SocialMediaType.DSCVR, username: 'testLink' }], + socialMedia: [{ type: SocialMediaLinkType.DSCVR, username: 'testLink' }], }); fixture.detectChanges(); }); diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.ts b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.ts index 16b17594..f4fed4e2 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info-form/reviewer-personal-info-form.component.ts @@ -16,11 +16,11 @@ import { } from '@angular/forms'; import { - ProfileService, - ReviewerProfile, - ReviewerProfileUpdate, + ReviewerGetMyUserProfileResponse, + UpdateMyUserProfileRequest, UserRole, -} from '~core/state'; +} from '~core/api'; +import { ProfileService } from '~core/state'; import { FormFieldComponent, FormValidationInfoComponent, @@ -169,7 +169,8 @@ export interface ReviewerProfileForm { `, }) export class ReviewerPersonalInfoFormComponent { - public readonly userProfile = input.required(); + public readonly userProfile = + input.required(); public readonly formClose = output(); @@ -219,7 +220,7 @@ export class ReviewerPersonalInfoFormComponent { const profileFormValues = this.profileForm().value; - const profileUpdate: ReviewerProfileUpdate = { + const profileUpdate: UpdateMyUserProfileRequest = { role: UserRole.Reviewer, username: profileFormValues.username, bio: profileFormValues.bio, diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.spec.ts b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.spec.ts index 1546cd39..c1bbde13 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SocialMediaType, UserRole } from '~core/state'; +import { UserRole, SocialMediaLinkType } from '~core/api'; import { ReviewerPersonalInfoComponent } from './reviewer-personal-info.component'; describe('ReviewerPersonalInfoComponent', () => { @@ -21,7 +21,7 @@ describe('ReviewerPersonalInfoComponent', () => { neuronId: 10685924793606457081n, walletAddress: '123213123sdfsdfs', bio: 'bio', - socialMedia: [{ type: SocialMediaType.DSCVR, username: 'testLink' }], + socialMedia: [{ type: SocialMediaLinkType.DSCVR, username: 'testLink' }], }); fixture.detectChanges(); }); diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.ts b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.ts index ed9ee528..7dec06e8 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-personal-info/reviewer-personal-info.component.ts @@ -5,7 +5,7 @@ import { output, } from '@angular/core'; -import { ReviewerProfile } from '~core/state'; +import { ReviewerGetMyUserProfileResponse } from '~core/api'; import { KeyColComponent, KeyValueGridComponent, @@ -62,7 +62,8 @@ import { `, }) export class ReviewerPersonalInfoComponent { - public readonly userProfile = input.required(); + public readonly userProfile = + input.required(); public readonly edit = output(); diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.spec.ts b/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.spec.ts index 873eec4e..8ff01370 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserRole } from '~core/state'; +import { UserRole } from '~core/api'; import { ReviewerProfileComponent } from './reviewer-profile.component'; describe('ReviewerProfileComponent', () => { diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.ts b/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.ts index 7460a34b..f74bc26a 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-profile/reviewer-profile.component.ts @@ -11,8 +11,8 @@ import { ReviewerPersonalInfoFormComponent } from '../reviewer-personal-info-for import { ReviewerSocialMediaComponent } from '../reviewer-social-media'; import { ReviewerSocialMediaFormComponent } from '../reviewer-social-media-form'; import { CardComponent } from '@cg/angular-ui'; +import { ReviewerGetMyUserProfileResponse } from '~core/api'; import { InfoIconComponent } from '~core/icons'; -import { ReviewerProfile } from '~core/state'; import { KeyColComponent, KeyValueGridComponent, @@ -142,7 +142,8 @@ import { `, }) export class ReviewerProfileComponent { - public readonly userProfile = input.required(); + public readonly userProfile = + input.required(); public readonly isPersonalInfoEditable = signal(false); public readonly isSocialMediaEditable = signal(false); diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.spec.ts b/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.spec.ts index 2ff76713..984b2faf 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ProfileService, SocialMediaType, UserRole } from '~core/state'; +import { UserRole, SocialMediaLinkType } from '~core/api'; +import { ProfileService } from '~core/state'; import { ProfileServiceMock } from '~core/state/profile/profile.service.mock'; import { ReviewerSocialMediaFormComponent } from './reviewer-social-media-form.component'; @@ -24,7 +25,7 @@ describe('ReviewerSocialMediaFormComponent', () => { neuronId: 10685924793606457081n, walletAddress: '123213123sdfsdfs', bio: 'bio', - socialMedia: [{ type: SocialMediaType.DSCVR, username: 'testLink' }], + socialMedia: [{ type: SocialMediaLinkType.DSCVR, username: 'testLink' }], }); fixture.detectChanges(); }); diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.ts b/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.ts index fb0e416b..5f7e2b2f 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-social-media-form/reviewer-social-media-form.component.ts @@ -17,13 +17,13 @@ import { import { SOCIAL_MEDIA_INPUTS, SocialMediaInputs } from '../profile.model'; import { - ProfileService, - ReviewerProfile, - ReviewerProfileUpdate, - SocialLink, - SocialMediaType, + ReviewerGetMyUserProfileResponse, + SocialMediaLink, + UpdateMyUserProfileRequest, UserRole, -} from '~core/state'; + SocialMediaLinkType, +} from '~core/api'; +import { ProfileService } from '~core/state'; import { FormFieldComponent, FormValidationInfoComponent, @@ -38,7 +38,7 @@ import { import { keysOf } from '~core/utils'; export type SocialMediaForm = { - [K in SocialMediaType]: FormControl; + [K in SocialMediaLinkType]: FormControl; }; @Component({ @@ -112,7 +112,8 @@ export type SocialMediaForm = { `, }) export class ReviewerSocialMediaFormComponent { - public readonly userProfile = input.required(); + public readonly userProfile = + input.required(); public readonly formClose = output(); public readonly formSaving = output(); @@ -155,12 +156,12 @@ export class ReviewerSocialMediaFormComponent { const socialMedia = Object.entries( socialMediaFormValues ?? {}, - ).map(([key, value]) => ({ - type: key as SocialMediaType, + ).map(([key, value]) => ({ + type: key as SocialMediaLinkType, username: value, })); - const profileUpdate: ReviewerProfileUpdate = { + const profileUpdate: UpdateMyUserProfileRequest = { role: UserRole.Reviewer, socialMedia: socialMedia, }; diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.spec.ts b/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.spec.ts index 144cee40..bdc2023e 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.spec.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserRole, SocialMediaType } from '~core/state'; +import { UserRole, SocialMediaLinkType } from '~core/api'; import { ReviewerSocialMediaComponent } from './reviewer-social-media.component'; describe('ReviewerSocialMediaComponent', () => { @@ -21,7 +21,7 @@ describe('ReviewerSocialMediaComponent', () => { neuronId: 10685924793606457081n, walletAddress: '123213123sdfsdfs', bio: 'bio', - socialMedia: [{ type: SocialMediaType.DSCVR, username: 'testLink' }], + socialMedia: [{ type: SocialMediaLinkType.DSCVR, username: 'testLink' }], }); fixture.detectChanges(); }); diff --git a/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.ts b/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.ts index 060598f4..b3a01845 100644 --- a/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.ts +++ b/src/frontend/src/app/pages/profile-edit/reviewer-social-media/reviewer-social-media.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { SOCIAL_MEDIA_INPUTS } from '../profile.model'; -import { ReviewerProfile } from '~core/state'; +import { ReviewerGetMyUserProfileResponse } from '~core/api'; import { KeyColComponent, KeyValueGridComponent, @@ -47,7 +47,8 @@ import { keysOf } from '~core/utils'; `, }) export class ReviewerSocialMediaComponent { - public readonly userProfile = input.required(); + public readonly userProfile = + input.required(); public readonly edit = output(); diff --git a/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts b/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts index dbd1d3ae..4689559d 100644 --- a/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts +++ b/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts @@ -14,6 +14,7 @@ import { } from '@angular/forms'; import { Subscription } from 'rxjs'; +import { CardComponent, RadioInputComponent } from '@cg/angular-ui'; import { FormFieldComponent, InputDirective, @@ -22,8 +23,7 @@ import { KeyColComponent, KeyValueGridComponent, ValueColComponent, -} from '../../../core/ui'; -import { CardComponent, RadioInputComponent } from '@cg/angular-ui'; +} from '~core/ui'; @Component({ selector: 'app-review-commits-form', diff --git a/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts b/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts index a43e2b6a..a25c3499 100644 --- a/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts +++ b/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts @@ -2,18 +2,18 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { + ImageSet, + ImageUploaderBtnComponent, + RadioInputComponent, +} from '@cg/angular-ui'; import { FormFieldComponent, InputDirective, KeyColComponent, KeyValueGridComponent, ValueColComponent, -} from '../../../core/ui'; -import { - ImageSet, - ImageUploaderBtnComponent, - RadioInputComponent, -} from '@cg/angular-ui'; +} from '~core/ui'; @Component({ selector: 'app-review-details-form',