From 95a8969099292ccb22f7517cfa797408da5f6328 Mon Sep 17 00:00:00 2001 From: Fedor Date: Mon, 31 Aug 2026 11:01:29 +0300 Subject: [PATCH] Polish vacancy catalog and profile UI --- .../detail/profile-detail-info.service.ts | 24 +--- .../ui/profile-detail-ui-info.service.ts | 27 ++-- .../ui/projects-detail-ui.service.spec.ts | 15 +++ .../detail/ui/projects-detail-ui.service.ts | 2 +- .../vacancy-detail-info.service.spec.ts | 26 +++- .../facades/vacancy-detail-info.service.ts | 5 + .../stage-two/stage-two.component.html | 3 +- .../stage-two/stage-two.component.scss | 1 + .../profile-mid-side.component.html | 12 +- .../profile-mid-side.component.scss | 29 +---- .../profile-mid-side.component.spec.ts | 115 +++++++++++++++++ .../profile-mid-side.component.ts | 9 -- .../profile-right-side.component.html | 6 +- .../profile-right-side.component.scss | 20 +++ .../profile-right-side.component.spec.ts | 51 ++++++++ .../profile/detail/main/main.component.ts | 19 +-- .../ui/pages/profile/edit/edit.component.html | 14 +- .../ui/pages/profile/edit/edit.component.scss | 12 ++ .../pages/profile/edit/edit.component.spec.ts | 42 +++++- .../ui/pages/profile/edit/edit.component.ts | 8 +- .../projects-mid-side.component.html | 13 +- .../projects-mid-side.component.scss | 18 +-- .../projects-mid-side.component.spec.ts | 79 ++++++++++++ .../projects-mid-side.component.ts | 50 +++++-- .../projects-right-side.component.html | 2 +- .../projects-right-side.component.scss | 7 + .../task/detail/task-detail.component.html | 10 +- .../task/detail/task-detail.component.scss | 22 ++-- .../task/detail/task-detail.component.ts | 4 + .../project-vacancy-step.component.html | 12 +- .../project-vacancy-step.component.scss | 13 ++ .../project-vacancy-step.component.ts | 4 + .../vacancies-right-side.component.html | 12 +- .../vacancies-right-side.component.scss | 7 + .../vacancies-right-side.component.ts | 13 ++ .../project-vacancy-card.component.html | 29 +++-- .../project-vacancy-card.component.scss | 35 +++++ .../project-vacancy-card.component.spec.ts | 122 +++++++++++++++++- .../project-vacancy-card.component.ts | 3 +- .../skills-group/skills-group.component.html | 3 +- .../skills-group/skills-group.component.scss | 15 ++- .../skills-group.component.spec.ts | 83 +++++++++++- .../skills-group/skills-group.component.ts | 10 +- .../specializations-group.component.html | 15 ++- .../specializations-group.component.scss | 15 ++- .../specializations-group.component.spec.ts | 12 +- .../vacancy-card/vacancy-card.component.html | 2 +- .../vacancy-card/vacancy-card.component.scss | 14 +- .../vacancy-card.component.spec.ts | 8 ++ .../vacancy-card/vacancy-card.component.ts | 3 +- 50 files changed, 888 insertions(+), 187 deletions(-) create mode 100644 projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/projects/detail/info/components/projects-mid-side/projects-mid-side.component.spec.ts diff --git a/projects/social_platform/src/app/api/profile/facades/detail/profile-detail-info.service.ts b/projects/social_platform/src/app/api/profile/facades/detail/profile-detail-info.service.ts index 8f96d7770..795cce82b 100644 --- a/projects/social_platform/src/app/api/profile/facades/detail/profile-detail-info.service.ts +++ b/projects/social_platform/src/app/api/profile/facades/detail/profile-detail-info.service.ts @@ -1,10 +1,9 @@ /** @format */ -import { DestroyRef, ElementRef, inject, Injectable } from "@angular/core"; -import { concatMap, filter, map, tap } from "rxjs"; +import { DestroyRef, inject, Injectable } from "@angular/core"; +import { filter, map, switchMap, tap } from "rxjs"; import { ProfileNews } from "@domain/profile/profile-news.model"; import { ActivatedRoute } from "@angular/router"; -import { ExpandService } from "../../../expand/expand.service"; import { calculateProfileProgress } from "@utils/calculateProgress"; import { ProfileDetailUIInfoService } from "./ui/profile-detail-ui-info.service"; import { NewsInfoService } from "../../../news/news-info.service"; @@ -23,7 +22,6 @@ export class ProfileDetailInfoService { private readonly route = inject(ActivatedRoute); private readonly destroyRef = inject(DestroyRef); - private readonly expandService = inject(ExpandService); private readonly profileDetailUIInfoService = inject(ProfileDetailUIInfoService); private readonly profileInfoService = inject(ProfileInfoService); private readonly newsInfoService = inject(NewsInfoService); @@ -69,16 +67,10 @@ export class ProfileDetailInfoService { }, }); - this.initializationProfileVields(); + this.initializationProfileFields(); this.initializationProfileNews(); } - initCheckDescription(descEl?: ElementRef): void { - setTimeout(() => { - this.expandService.checkExpandable("description", !!this.profile()?.personal.aboutMe, descEl); - }, 150); - } - onAddNews(news: { text: string; files: string[] }) { return this.addProfileNewsUseCase.execute(this.route.snapshot.params["id"], news).pipe( tap(result => { @@ -136,16 +128,16 @@ export class ProfileDetailInfoService { .subscribe(); } - private initializationProfileVields(): void { + private initializationProfileFields(): void { this.profileDetailUIInfoService.applySetLoggedUserId("logged", this.profile()!.id); - this.profileDetailUIInfoService.applyProfileEmpty(); } - private initializationProfileNews(descEl?: ElementRef): void { + private initializationProfileNews(): void { this.route.params .pipe( map(r => r["id"]), - concatMap(userId => this.fetchProfileNewsUseCase.execute(Number(userId))), + tap(() => this.newsInfoService.applySetNews({ results: [], count: 0 })), + switchMap(userId => this.fetchProfileNewsUseCase.execute(Number(userId))), takeUntilDestroyed(this.destroyRef), ) .subscribe(result => { @@ -157,8 +149,6 @@ export class ProfileDetailInfoService { this.setupNewsObserver(); }, 100); }); - - this.initCheckDescription(descEl); } private setupNewsObserver(): void { diff --git a/projects/social_platform/src/app/api/profile/facades/detail/ui/profile-detail-ui-info.service.ts b/projects/social_platform/src/app/api/profile/facades/detail/ui/profile-detail-ui-info.service.ts index b925f5829..aa76aaf11 100644 --- a/projects/social_platform/src/app/api/profile/facades/detail/ui/profile-detail-ui-info.service.ts +++ b/projects/social_platform/src/app/api/profile/facades/detail/ui/profile-detail-ui-info.service.ts @@ -1,6 +1,6 @@ /** @format */ -import { Injectable, signal } from "@angular/core"; +import { computed, Injectable, signal } from "@angular/core"; import { DirectionItem, directionItemBuilder } from "@utils/directionItemBuilder"; import { User } from "@domain/auth/user.model"; @@ -12,7 +12,18 @@ export class ProfileDetailUIInfoService { readonly loggedUserId = signal(0); readonly profileId = signal(0); // ID текущего пользователя. - readonly isProfileEmpty = signal(undefined); + readonly isProfileEmpty = computed(() => { + const user = this.user(); + if (!user) return undefined; + + return !( + user.firstName && + user.lastName && + user.email && + user.personal.avatar && + user.personal.birthday + ); + }); readonly isProfileFill = signal(false); readonly directions = signal([]); @@ -35,18 +46,6 @@ export class ProfileDetailUIInfoService { } } - applyProfileEmpty(): void { - this.isProfileEmpty.set( - !( - this.user()?.firstName && - this.user()?.lastName && - this.user()?.email && - this.user()?.personal.avatar && - this.user()?.personal.birthday - ), - ); - } - applySetLoggedUserId(type: "logged" | "profile", profileId: number): void { type === "logged" ? this.loggedUserId.set(profileId) : this.profileId.set(profileId); } diff --git a/projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.spec.ts b/projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.spec.ts new file mode 100644 index 000000000..bf2095e75 --- /dev/null +++ b/projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.spec.ts @@ -0,0 +1,15 @@ +/** @format */ + +import { Project } from "@domain/project/project.model"; +import { ProjectsDetailUIInfoService } from "./projects-detail-ui.service"; + +describe("ProjectsDetailUIInfoService", () => { + it("показывает полное название блока актуальности", () => { + const service = new ProjectsDetailUIInfoService(); + service.applySetProject(Project.default()); + + service.applyDirectionItems(); + + expect(service.directions()[2].direction).toBe("актуальность"); + }); +}); diff --git a/projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.ts b/projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.ts index c5bca10f1..d52b14808 100644 --- a/projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.ts +++ b/projects/social_platform/src/app/api/project/facades/detail/ui/projects-detail-ui.service.ts @@ -26,7 +26,7 @@ export class ProjectsDetailUIInfoService { this.directions.set( directionItemBuilder( 5, - ["проблема", "целевая аудитория", "актуаль-сть", "цели", "партнеры"], + ["проблема", "целевая аудитория", "актуальность", "цели", "партнеры"], ["key", "smile", "graph", "goal", "team"], [ this.project()!.problem, diff --git a/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.spec.ts b/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.spec.ts index 4c887330e..edc450b68 100644 --- a/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.spec.ts +++ b/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.spec.ts @@ -29,6 +29,7 @@ describe("VacancyDetailInfoService", () => { let downloadCvUseCase: any; let fileService: any; let snackbar: any; + let queryParams$: Subject>; beforeEach(() => { sendUseCase = { execute: vi.fn() }; @@ -38,6 +39,7 @@ describe("VacancyDetailInfoService", () => { downloadCvUseCase = { execute: vi.fn() }; fileService = { uploadFile: vi.fn() }; snackbar = { success: vi.fn(), error: vi.fn() }; + queryParams$ = new Subject>(); TestBed.configureTestingModule({ providers: [ @@ -48,7 +50,7 @@ describe("VacancyDetailInfoService", () => { provide: ActivatedRoute, useValue: { data: of({}), - queryParams: of({}), + queryParams: queryParams$, snapshot: { paramMap: { get: () => "10" } }, }, }, @@ -110,6 +112,28 @@ describe("VacancyDetailInfoService", () => { expect(ui.openModal()).toBe(false); }); + it("opens manager responses from a guarded query parameter", () => { + ui.vacancy.update(vacancy => + vacancy ? Object.assign(new Vacancy(), vacancy, { canManageResponses: true }) : vacancy, + ); + getResponsesUseCase.execute.mockReturnValue(of(ok([]))); + service.initializeDetailInfoQueryParams(); + + queryParams$.next({ manageResponses: "true" }); + + expect(ui.responsesModal()).toBe(true); + expect(getResponsesUseCase.execute).toHaveBeenCalledExactlyOnceWith(10); + }); + + it("does not open or load manager responses without backend permission", () => { + service.initializeDetailInfoQueryParams(); + + queryParams$.next({ manageResponses: "true" }); + + expect(ui.responsesModal()).toBe(false); + expect(getResponsesUseCase.execute).not.toHaveBeenCalled(); + }); + it.each([ [["Вы уже откликнулись на эту вакансию."], "Вы уже откликнулись на эту вакансию"], [ diff --git a/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.ts b/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.ts index e7375c937..66055c9b0 100644 --- a/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.ts +++ b/projects/social_platform/src/app/api/vacancy/facades/vacancy-detail-info.service.ts @@ -57,6 +57,11 @@ export class VacancyDetailInfoService { initializeDetailInfoQueryParams(): void { this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: r => { + if (r["manageResponses"] === true || r["manageResponses"] === "true") { + this.openVacancyResponses(); + return; + } + this.vacancyDetailUIInfoService.applyNoResponseOpenModal(r); }, }); diff --git a/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.html b/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.html index 7e70ab5cb..3271d9fd4 100644 --- a/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.html +++ b/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.html @@ -83,7 +83,8 @@

библиотека

[title]="skillGroup.name" [options]="skillGroup.skills" [selected]="stageForm.getRawValue().skills" - [disabled]="isSkillGroupDisabled(skillGroup.name)" + [hasOpenGroups]="hasOpenSkillsGroups()" + [isOpen]="openSkillGroup() === skillGroup.name" (groupToggled)="onSkillGroupToggled($event, skillGroup.name)" (optionToggled)="onOptionToggled($event)" > diff --git a/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.scss b/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.scss index 570802ea9..66887b7d2 100644 --- a/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.scss +++ b/projects/social_platform/src/app/ui/pages/onboarding/stage-two/stage-two.component.scss @@ -137,6 +137,7 @@ } &__skills-groups { + position: relative; height: 100%; overflow: auto; scrollbar-width: thin; diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.html b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.html index 22e26bff2..cf5fad45e 100644 --- a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.html +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.html @@ -10,15 +10,7 @@

обо мне

-

- @if (descriptionExpandable()) { -
- {{ readFullDescription() ? "скрыть" : "подробнее" }} -
- } +

} @@ -37,7 +29,7 @@

обо мне

} @if (isProfileEmpty()) { @if (loggedUserId() === user()!.id) { -
+

заполните профиль и начните пользоваться PROCOLLAB diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.scss b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.scss index 191b34566..1d913f7d4 100644 --- a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.scss +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.scss @@ -32,6 +32,10 @@ flex-direction: column; gap: 24px; align-items: center; + justify-content: center; + min-height: min(480px, calc(100vh - 240px)); + padding: 24px; + text-align: center; &--text { color: var(--grey-for-text); @@ -64,21 +68,11 @@ margin-bottom: 8px; color: var(--accent); } - /* stylelint-disable value-no-vendor-prefix */ + &__text { p { - display: -webkit-box; - overflow: hidden; color: var(--black); - text-overflow: ellipsis; word-break: break-word; - transition: all 0.7s ease-in-out; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - - &.expanded { - -webkit-line-clamp: unset; - } } ::ng-deep a { @@ -94,8 +88,6 @@ } } - /* stylelint-enable value-no-vendor-prefix */ - &__read-full { margin-top: 2px; color: var(--accent); @@ -114,14 +106,3 @@ margin-top: 20px; } } - -.read-more { - margin-top: 12px; - color: var(--accent); - cursor: pointer; - transition: color 0.2s; - - &:hover { - color: var(--accent-dark); - } -} diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.spec.ts b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.spec.ts new file mode 100644 index 000000000..b18f99d2b --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.spec.ts @@ -0,0 +1,115 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideRouter } from "@angular/router"; +import { FileService, ValidationService } from "@corelib"; +import { NewsInfoService } from "@api/news/news-info.service"; +import { ProfileDetailInfoService } from "@api/profile/facades/detail/profile-detail-info.service"; +import { ProfileDetailUIInfoService } from "@api/profile/facades/detail/ui/profile-detail-ui-info.service"; +import { User } from "@domain/auth/user.model"; +import { ProfileMidSideComponent } from "./profile-mid-side.component"; + +describe("ProfileMidSideComponent", () => { + let fixture: ComponentFixture; + let ui: ProfileDetailUIInfoService; + + const createUser = (id: number, filled: boolean, aboutMe = ""): User => + ({ + id, + firstName: filled ? "Анна" : "", + lastName: filled ? "Иванова" : "", + email: filled ? "anna@example.test" : "", + personal: { + aboutMe, + avatar: filled ? "https://example.test/avatar.png" : "", + birthday: filled ? "2000-01-01" : null, + }, + relations: { + progress: filled ? 100 : 0, + profileFillPromptAcknowledgedAt: null, + skills: [], + achievements: [], + }, + }) as unknown as User; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfileMidSideComponent], + providers: [ + provideRouter([]), + { + provide: ProfileDetailInfoService, + useValue: { + onAddNews: vi.fn(), + onDeleteNews: vi.fn(), + onLike: vi.fn(), + onEditNews: vi.fn(), + onNewsInView: vi.fn(), + }, + }, + { provide: NewsInfoService, useValue: { news: signal([]) } }, + ProfileDetailUIInfoService, + { provide: ValidationService, useValue: { getFormValidation: vi.fn() } }, + { provide: FileService, useValue: {} }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ProfileMidSideComponent); + ui = TestBed.inject(ProfileDetailUIInfoService); + const user = createUser(7, false); + ui.applySetLoggedUserId("logged", 7); + ui.applyInitProfile({ data: { user } }, 7); + fixture.componentRef.setInput("user", user); + fixture.detectChanges(); + }); + + it("показывает центрируемый empty state владельцу пустого профиля", () => { + const emptyState = fixture.nativeElement.querySelector( + '[data-testid="empty-profile-state"]', + ) as HTMLElement; + + expect(emptyState).toBeTruthy(); + expect(emptyState.textContent).toContain("заполните профиль и начните пользоваться PROCOLLAB"); + }); + + it("does not leak empty state across foreign and own profile navigation", () => { + const foreignEmpty = createUser(20, false); + const ownFilled = createUser(10, true, "Заполненный профиль"); + ui.applySetLoggedUserId("logged", 10); + + ui.applyInitProfile({ data: { user: foreignEmpty } }, 10); + fixture.componentRef.setInput("user", foreignEmpty); + fixture.detectChanges(); + expect(ui.isProfileEmpty()).toBe(true); + expect(fixture.nativeElement.querySelector('[data-testid="empty-profile-state"]')).toBeFalsy(); + + ui.applyInitProfile({ data: { user: ownFilled } }, 10); + fixture.componentRef.setInput("user", ownFilled); + fixture.detectChanges(); + expect(ui.isProfileEmpty()).toBe(false); + expect(fixture.nativeElement.querySelector('[data-testid="empty-profile-state"]')).toBeFalsy(); + expect(fixture.nativeElement.querySelector("app-news-form")).toBeTruthy(); + + ui.applyInitProfile({ data: { user: foreignEmpty } }, 10); + fixture.componentRef.setInput("user", foreignEmpty); + fixture.detectChanges(); + ui.applyInitProfile({ data: { user: ownFilled } }, 10); + fixture.componentRef.setInput("user", ownFilled); + fixture.detectChanges(); + + expect(ui.isProfileEmpty()).toBe(false); + expect(fixture.nativeElement.querySelector("app-news-form")).toBeTruthy(); + }); + + it("shows the full 300-character about me text without expansion control", () => { + const aboutMe = "Я".repeat(300); + const user = createUser(7, true, aboutMe); + ui.applyInitProfile({ data: { user } }, 7); + fixture.componentRef.setInput("user", user); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector(".about__text p").textContent).toBe(aboutMe); + expect(fixture.nativeElement.querySelector(".about .read-more")).toBeFalsy(); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.ts b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.ts index 3155ad594..433173061 100644 --- a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.ts +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-mid-side/profile-mid-side.component.ts @@ -18,7 +18,6 @@ import { IconComponent, ButtonComponent } from "@ui/primitives"; import { NewsCardComponent } from "@ui/widgets/news-card/news-card.component"; import { NewsFormComponent } from "@ui/widgets/news-form/news-form.component"; import { ProjectDirectionCard } from "@ui/widgets/project-direction-card/project-direction-card.component"; -import { ExpandService } from "@api/expand/expand.service"; import { NewsInfoService } from "@api/news/news-info.service"; import { ProfileDetailInfoService } from "@api/profile/facades/detail/profile-detail-info.service"; import { ProfileDetailUIInfoService } from "@api/profile/facades/detail/ui/profile-detail-ui-info.service"; @@ -53,7 +52,6 @@ export class ProfileMidSideComponent { private readonly profileDetailInfoService = inject(ProfileDetailInfoService); private readonly profileDetailUIInfoService = inject(ProfileDetailUIInfoService); private readonly newsInfoService = inject(NewsInfoService); - private readonly expandService = inject(ExpandService); protected readonly loggedUserId = this.profileDetailUIInfoService.loggedUserId; protected readonly isProfileEmpty = this.profileDetailUIInfoService.isProfileEmpty; @@ -63,9 +61,6 @@ export class ProfileMidSideComponent { protected readonly newsPending = signal(false); - protected readonly descriptionExpandable = this.expandService.descriptionExpandable; - protected readonly readFullDescription = this.expandService.readFullDescription; - private readonly destroyRef$ = inject(DestroyRef); protected readonly AppRoutes = AppRoutes; @@ -103,8 +98,4 @@ export class ProfileMidSideComponent { onNewsInView(entries: IntersectionObserverEntry[]): void { this.profileDetailInfoService.onNewsInView(entries); } - - onExpandDescription(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void { - this.expandService.onExpand("description", elem, expandedClass, isExpanded); - } } diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.html b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.html index 302f1b1ee..12930f5bb 100644 --- a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.html +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.html @@ -31,7 +31,7 @@

контакты

@if (link | userLinks; as l) { - {{ l.tag | truncate: 30 }} + {{ l.tag }} } @@ -217,8 +217,8 @@

проекты

height="30" />
-

- {{ project.name | truncate: 30 }} +

+ {{ project.name }}

{{ project.collaborator?.role }}
diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.scss b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.scss index 3f4226437..8167e755c 100644 --- a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.scss +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.scss @@ -31,6 +31,7 @@ &__right { display: flex; flex-direction: column; + min-width: 0; } &__aside { @@ -92,6 +93,7 @@ &__info { display: flex; flex-direction: column; + min-width: 0; &--text { color: var(--black) !important; @@ -110,6 +112,17 @@ } } + /* stylelint-disable value-no-vendor-prefix */ + &__project-name { + display: -webkit-box; + overflow: hidden; + color: var(--black); + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + /* stylelint-enable value-no-vendor-prefix */ + &__date { display: flex; flex-direction: column; @@ -125,6 +138,7 @@ display: flex; gap: 6px; align-items: center; + min-width: 0; color: var(--grey-for-text); &--status { @@ -164,6 +178,12 @@ } } + &__contact { + min-width: 0; + word-break: normal; + overflow-wrap: anywhere; + } + @include expandable-list; } diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.spec.ts b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.spec.ts new file mode 100644 index 000000000..2dc76fef7 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/components/profile-right-side/profile-right-side.component.spec.ts @@ -0,0 +1,51 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideRouter } from "@angular/router"; +import { ExpandService } from "@api/expand/expand.service"; +import { ProfileDetailUIInfoService } from "@api/profile/facades/detail/ui/profile-detail-ui-info.service"; +import { User } from "@domain/auth/user.model"; +import { ProfileRightSideComponent } from "./profile-right-side.component"; + +describe("ProfileRightSideComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfileRightSideComponent], + providers: [ + provideRouter([]), + { + provide: ExpandService, + useValue: { readAll: signal({}), toggleReadAll: vi.fn() }, + }, + { + provide: ProfileDetailUIInfoService, + useValue: { isShowModal: signal(false), applyOpenWorkInfoModal: vi.fn() }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ProfileRightSideComponent); + }); + + it("keeps the complete long project title in the profile project list DOM", () => { + const projectName = "Очень длинное название проекта без жесткого символьного обрезания"; + fixture.componentRef.setInput("user", { + personal: { links: [] }, + relations: { + education: [], + workExperience: [], + projects: [ + { id: 1, name: projectName, imageAddress: "", collaborator: { role: "Участник" } }, + ], + }, + } as unknown as User); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector(".lists__project-name").textContent.trim()).toBe( + projectName, + ); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/profile/detail/main/main.component.ts b/projects/social_platform/src/app/ui/pages/profile/detail/main/main.component.ts index 00adf03ba..88f48ec19 100644 --- a/projects/social_platform/src/app/ui/pages/profile/detail/main/main.component.ts +++ b/projects/social_platform/src/app/ui/pages/profile/detail/main/main.component.ts @@ -1,15 +1,6 @@ /** @format */ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ElementRef, - inject, - OnDestroy, - OnInit, - viewChild, -} from "@angular/core"; +import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit } from "@angular/core"; import { CommonModule } from "@angular/common"; import { ProfileDetailInfoService } from "@api/profile/facades/detail/profile-detail-info.service"; import { ProfileDetailUIInfoService } from "@api/profile/facades/detail/ui/profile-detail-ui-info.service"; @@ -32,9 +23,7 @@ import { ProfileMidSideComponent } from "./components/profile-mid-side/profile-m providers: [ProfileDetailInfoService, ProfileDetailUIInfoService, ExpandService], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ProfileMainComponent implements OnInit, AfterViewInit, OnDestroy { - readonly descEl = viewChild("descEl"); - +export class ProfileMainComponent implements OnInit, OnDestroy { private readonly profileDetailInfoService = inject(ProfileDetailInfoService); private readonly profileDetailUIInfoService = inject(ProfileDetailUIInfoService); @@ -44,10 +33,6 @@ export class ProfileMainComponent implements OnInit, AfterViewInit, OnDestroy { this.profileDetailInfoService.initializationProfile(); } - ngAfterViewInit(): void { - this.profileDetailInfoService.initCheckDescription(this.descEl()); - } - ngOnDestroy(): void { this.profileDetailInfoService.destroy(); } diff --git a/projects/social_platform/src/app/ui/pages/profile/edit/edit.component.html b/projects/social_platform/src/app/ui/pages/profile/edit/edit.component.html index 83fb1e96b..4ac7cece1 100644 --- a/projects/social_platform/src/app/ui/pages/profile/edit/edit.component.html +++ b/projects/social_platform/src/app/ui/pages/profile/edit/edit.component.html @@ -189,8 +189,16 @@

Выберите специальность

} @defer (when skillsGroupsModalOpen()) { - +