Skip to content

Commit 6c7ca9b

Browse files
committed
fix: resolve zoneless/Angular 20 regressions from smoke test
Reactivity (zoneless change detection): - avatar-control: markForCheck after FileReader/Image.onload so the cropper opens - approve-skill: markForCheck on approve/unapprove (skill.approves mutated in place) - news widgets: pass DestroyRef to takeUntilDestroyed() in method context (NG0203) Data / state: - project.repository: invalidate entity cache in update() so detail reflects edits - projects list: seed /all & /my from resolver route.data; load subscriptions only on /subscriptions - program list: searchedList is now computed over list()+searchQuery (fixes filter lag/reset); resolver returns empty page instead of EMPTY (no blank outlet); consume isFirstLoad on first emission so reset after a filtered reload refetches all projects Bugs: - message-input: drop stray `module` reference (ReferenceError broke project chat) - program detail: null-safe non_field_errors render (page crash) - news copy-link: bind contentId so URL is not /projects/NaN/ - assign-to-program: error modal only on 500, snackbar on 400 - vacancy response: snackbar on error; remove duplicate submit (click + type=submit) - profile edit: success snackbar; specialty suggestions bind SearchesService.inlineSpecs - dashboard: subscriptions block links to /subscriptions (was /all) - approve-skill count: use approves().length, show "N человек" only when > 3
1 parent aa668b8 commit 6c7ca9b

19 files changed

Lines changed: 378 additions & 343 deletions

File tree

projects/social_platform/src/app/api/paths/app-routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const AppRoutes = {
3232
projects: {
3333
all: () => "/office/projects/all",
3434
my: () => "/office/projects/my",
35+
subscriptions: () => "/office/projects/subscriptions",
3536
detail: (projectId: number | string) => `/office/projects/${projectId}`,
3637
edit: (projectId: number | string) => `/office/projects/${projectId}/edit`,
3738
chat: (projectId: number | string) => `/office/projects/${projectId}/chat`,

projects/social_platform/src/app/api/profile/facades/edit/profile-edit-info.service.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { EditStep } from "@core/lib/models/edit-step";
1313
import { SaveProfileUseCase } from "@api/profile/use-cases/save-profile.use-case";
1414
import { ProfileInfoService } from "../profile-info.service";
1515
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
16+
import { SnackbarService } from "@domain/shared/snackbar.service";
1617

1718
/** Фасад редактирования профиля: сбор формы, `SaveProfileUseCase`, раскрытие групп. */
1819
@Injectable()
@@ -27,6 +28,7 @@ export class ProfileEditInfoService {
2728
private readonly profileInfoService = inject(ProfileInfoService);
2829

2930
private readonly saveProfileUseCase = inject(SaveProfileUseCase);
31+
private readonly snackbarService = inject(SnackbarService);
3032

3133
private readonly profileForm = this.profileFormService.getForm();
3234

@@ -122,13 +124,13 @@ export class ProfileEditInfoService {
122124
if (hasLengthOverflow) {
123125
this.isModalErrorSkillsChoose.set(true);
124126
this.isModalErrorSkillChooseText.set(
125-
"Превышено допустимое количество символов в одном из полей"
127+
"Превышено допустимое количество символов в одном из полей",
126128
);
127129
return;
128130
}
129131

130132
const mainFieldsValid = ["firstName", "lastName", "birthday", "speciality", "city"].every(
131-
name => this.profileForm.get(name)?.valid
133+
name => this.profileForm.get(name)?.valid,
132134
);
133135

134136
if (!mainFieldsValid || this.profileFormSubmitting$().status === "loading") {
@@ -149,8 +151,8 @@ export class ProfileEditInfoService {
149151
.map((file: any) => (typeof file === "string" ? file : file.link))
150152
.filter(Boolean)
151153
: achievement.files
152-
? [achievement.files]
153-
: [],
154+
? [achievement.files]
155+
: [],
154156
}));
155157

156158
// Построение объекта профиля с только необходимыми полями
@@ -208,6 +210,7 @@ export class ProfileEditInfoService {
208210

209211
this.profileInfoService.applyProfileUpdated(result.value);
210212
this.profileFormSubmitting$.set(success(undefined));
213+
this.snackbarService.success("Профиль сохранён");
211214
this.navigationService.profileRedirect(result.value.id);
212215
});
213216
}

projects/social_platform/src/app/api/program/facades/detail/program-detail-list-info.service.ts

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { GetAllMembersUseCase } from "../../use-cases/get-all-members.use-case";
2626
import { GetProjectSubscriptionsUseCase } from "../../../project/use-cases/get-project-subscriptions.use-case";
2727
import { FilterProjectRatingsUseCase } from "../../use-cases/filter-project-ratings.use-case";
2828
import { GetProjectRatingsUseCase } from "../../use-cases/get-project-ratings.use-case";
29-
import Fuse from "fuse.js";
3029
import { Project } from "@domain/project/project.model";
3130
import { User } from "@domain/auth/user.model";
3231
import { isSuccess, loading, success } from "@domain/shared/async-state";
@@ -69,10 +68,10 @@ export class ProgramDetailListInfoService {
6968
.pipe(
7069
tap(data => this.listType.set(data["listType"])),
7170
switchMap(r => of(r["data"])),
72-
takeUntilDestroyed(this.destroyRef)
71+
takeUntilDestroyed(this.destroyRef),
7372
)
7473
.subscribe(data => {
75-
this.programDetailListUIInfoService.list$.set(success(data.results));
74+
this.programDetailListUIInfoService.list$.set(success(data?.results ?? []));
7675
});
7776

7877
this.setupSearch();
@@ -91,7 +90,7 @@ export class ProgramDetailListInfoService {
9190
this.logger.error("Scroll error:", err);
9291
return of({});
9392
}),
94-
takeUntilDestroyed(this.destroyRef)
93+
takeUntilDestroyed(this.destroyRef),
9594
)
9695
.subscribe();
9796
}
@@ -121,10 +120,10 @@ export class ProgramDetailListInfoService {
121120
this.route.queryParams
122121
.pipe(
123122
map(q => q["search"]),
124-
takeUntilDestroyed(this.destroyRef)
123+
takeUntilDestroyed(this.destroyRef),
125124
)
126125
.subscribe(search => {
127-
this.programDetailListUIInfoService.searchedList.set(this.applySearch(search));
126+
this.programDetailListUIInfoService.searchQuery.set(search ?? "");
128127
});
129128
}
130129

@@ -133,7 +132,7 @@ export class ProgramDetailListInfoService {
133132
.pipe(
134133
filter(profile => !!profile),
135134
switchMap(p => this.getProjectSubscriptionsUseCase.execute(p!.id)),
136-
takeUntilDestroyed(this.destroyRef)
135+
takeUntilDestroyed(this.destroyRef),
137136
)
138137
.subscribe({
139138
next: result => {
@@ -154,6 +153,9 @@ export class ProgramDetailListInfoService {
154153
.pipe(
155154
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)),
156155
concatMap(q => {
156+
const isFirstLoad = this.isFirstLoad;
157+
this.isFirstLoad = false;
158+
157159
const prev = this.programDetailListUIInfoService.list();
158160
this.programDetailListUIInfoService.list$.set(loading(prev));
159161

@@ -180,7 +182,7 @@ export class ProgramDetailListInfoService {
180182
}
181183

182184
return result.value;
183-
})
185+
}),
184186
);
185187
}
186188
return this.getProjectRatingsUseCase.execute(programId, params).pipe(
@@ -191,7 +193,7 @@ export class ProgramDetailListInfoService {
191193
}
192194

193195
return result.value;
194-
})
196+
}),
195197
);
196198
}
197199

@@ -204,12 +206,11 @@ export class ProgramDetailListInfoService {
204206
}
205207

206208
return result.value;
207-
})
209+
}),
208210
);
209211
}
210212

211-
if (this.isFirstLoad && !q["search"]) {
212-
this.isFirstLoad = false;
213+
if (isFirstLoad && !q["search"]) {
213214
const prefetched = this.prefetchedProjects();
214215
if (prefetched) {
215216
return of(prefetched);
@@ -224,14 +225,14 @@ export class ProgramDetailListInfoService {
224225
}
225226

226227
return result.value;
227-
})
228+
}),
228229
);
229230
}),
230231
catchError(err => {
231232
this.logger.error("Error in setupFilters:", err);
232233
return of(this.emptyPage<ProjectRate | Project>());
233234
}),
234-
takeUntilDestroyed(this.destroyRef)
235+
takeUntilDestroyed(this.destroyRef),
235236
)
236237
.subscribe(result => {
237238
if (!result) return;
@@ -321,7 +322,7 @@ export class ProgramDetailListInfoService {
321322
this.programDetailListUIInfoService.list$.update(state =>
322323
isSuccess(state)
323324
? success([...state.data, ...result.value.results])
324-
: success(result.value.results)
325+
: success(result.value.results),
325326
);
326327
this.programDetailListUIInfoService.loadingMore.set(false);
327328
}),
@@ -330,7 +331,7 @@ export class ProgramDetailListInfoService {
330331
this.listPage.update(p => p - 1);
331332
return of(this.emptyPage<ProjectRate>());
332333
}),
333-
takeUntilDestroyed(this.destroyRef)
334+
takeUntilDestroyed(this.destroyRef),
334335
);
335336
}
336337

@@ -345,7 +346,7 @@ export class ProgramDetailListInfoService {
345346
this.programDetailListUIInfoService.list$.update(state =>
346347
isSuccess(state)
347348
? success([...state.data, ...result.value.results])
348-
: success(result.value.results)
349+
: success(result.value.results),
349350
);
350351
this.programDetailListUIInfoService.loadingMore.set(false);
351352
}),
@@ -354,7 +355,7 @@ export class ProgramDetailListInfoService {
354355
this.listPage.update(p => p - 1);
355356
return of(this.emptyPage<ProjectRate>());
356357
}),
357-
takeUntilDestroyed(this.destroyRef)
358+
takeUntilDestroyed(this.destroyRef),
358359
);
359360
}
360361

@@ -375,7 +376,7 @@ export class ProgramDetailListInfoService {
375376
this.programDetailListUIInfoService.list$.update(state =>
376377
isSuccess(state)
377378
? success([...state.data, ...result.value.results])
378-
: success(result.value.results)
379+
: success(result.value.results),
379380
);
380381
this.programDetailListUIInfoService.loadingMore.set(false);
381382
}),
@@ -384,7 +385,7 @@ export class ProgramDetailListInfoService {
384385
this.listPage.update(p => p - 1);
385386
return of(this.emptyPage<Project>());
386387
}),
387-
takeUntilDestroyed(this.destroyRef)
388+
takeUntilDestroyed(this.destroyRef),
388389
);
389390
}
390391

@@ -400,7 +401,7 @@ export class ProgramDetailListInfoService {
400401
this.programDetailListUIInfoService.list$.update(state =>
401402
isSuccess(state)
402403
? success([...state.data, ...result.value.results])
403-
: success(result.value.results)
404+
: success(result.value.results),
404405
);
405406
this.programDetailListUIInfoService.loadingMore.set(false);
406407
}),
@@ -409,7 +410,7 @@ export class ProgramDetailListInfoService {
409410
this.listPage.update(p => p - 1);
410411
return of(this.emptyPage<User>());
411412
}),
412-
takeUntilDestroyed(this.destroyRef)
413+
takeUntilDestroyed(this.destroyRef),
413414
);
414415
}
415416

@@ -442,8 +443,6 @@ export class ProgramDetailListInfoService {
442443
return;
443444
}
444445

445-
// Для projects search — плоский query-param, не filter-ключ.
446-
// Если уйдёт в filters body — бэк возвращает 400 (search не входит в список фильтр-полей).
447446
if (this.listType() === "projects" && key === "search") {
448447
extraParams["search"] = value;
449448
return;
@@ -455,20 +454,6 @@ export class ProgramDetailListInfoService {
455454
return { filters, extraParams };
456455
}
457456

458-
private applySearch(search: string) {
459-
if (!search) return this.programDetailListUIInfoService.list();
460-
461-
const searchKeys =
462-
this.listType() === "projects" || this.listType() === "rating"
463-
? ["name"]
464-
: ["firstName", "lastName"];
465-
466-
const fuse = new Fuse(this.programDetailListUIInfoService.list(), {
467-
keys: searchKeys,
468-
});
469-
return fuse.search(search).map(r => r.item);
470-
}
471-
472457
private prefetchedProjects(): ApiPagination<Project> | undefined {
473458
return this.route.snapshot.data["data"] as ApiPagination<Project> | undefined;
474459
}

projects/social_platform/src/app/api/program/facades/detail/ui/program-detail-list-ui-info.service.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
import { computed, inject, Injectable, signal } from "@angular/core";
44
import { FormBuilder } from "@angular/forms";
5-
import { User } from "@domain/auth/user.model";
65
import { ApiPagination } from "@domain/other/api-pagination.model";
76
import { PartnerProgramFields } from "@domain/program/partner-program-fields.model";
8-
import { ProjectRate } from "@domain/project/project-rate";
97
import { Project } from "@domain/project/project.model";
108
import { AsyncState, initial, isLoading, isSuccess } from "@domain/shared/async-state";
119
import { AppRoutes } from "@api/paths/app-routes";
10+
import Fuse from "fuse.js";
1211

1312
/** Состояние интерфейса списков программы: проекты, участники, рейтинг, фильтры и пагинация. */
1413
@Injectable()
@@ -24,7 +23,7 @@ export class ProgramDetailListUIInfoService {
2423

2524
readonly list$ = signal<AsyncState<any[]>>(initial());
2625
readonly loadingMore = signal(false);
27-
readonly searchedList = signal<any[]>([]);
26+
readonly searchQuery = signal<string>("");
2827

2928
readonly list = computed(() => {
3029
const state = this.list$();
@@ -33,6 +32,15 @@ export class ProgramDetailListUIInfoService {
3332
return [];
3433
});
3534

35+
readonly searchedList = computed<any[]>(() => {
36+
const list = this.list();
37+
const search = this.searchQuery().trim();
38+
if (!search) return list;
39+
40+
const keys = this.listType() === "members" ? ["firstName", "lastName"] : ["name"];
41+
return new Fuse(list, { keys }).search(search).map(r => r.item);
42+
});
43+
3644
readonly profileSubscriptions = signal<Project[]>([]);
3745
readonly profileProjSubsIds = computed(() => this.profileSubscriptions().map(sub => sub.id));
3846

@@ -42,8 +50,8 @@ export class ProgramDetailListUIInfoService {
4250
return this.listType() === "rating"
4351
? 10
4452
: this.listType() === "projects"
45-
? this.perPage()
46-
: this.listTake();
53+
? this.perPage()
54+
: this.listTake();
4755
});
4856

4957
searchParamName = computed(() => {

0 commit comments

Comments
 (0)