diff --git a/docs/project-cards-activity/README.md b/docs/project-cards-activity/README.md new file mode 100644 index 000000000..69004ba7f --- /dev/null +++ b/docs/project-cards-activity/README.md @@ -0,0 +1,148 @@ + + +# Карточки проектов и «Моя активность» — DEV + +База: `b9ffb77ec79819ab555fef0ee614cd3ea00bc616`, актуальный DEV после #367. +Ветка: `fix/dev-project-cards-no-industry-activity`. +Проверка: 22 сентября 2026, Windows, Node 20.20.2, npm 10.8.2. + +## Изменения представления + +Отрасль занимала отдельную строку компактной карточки и сокращала место для +названия и описания. Удалены только её разметка, стили, подсказка на ссылке +проекта и локальная зависимость InfoCard от IndustryRepositoryPort. +Это касается подписок, витрины, полного списка и приглашений, включая dashboard. +`Project.industry`, справочник, API и фильтрация по `industry` сохранены. + +В публичной карточке описание теперь занимает до четырёх строк / 52 px вместо +двух / 26 px. Название приглашения использует до двух строк; полный текст +сохранён в DOM и `title`. Фиксированной пустой строки под отрасль нет. +Размер карточек 156×180 px, аватар 70 px, lifecycle, роль, доступ, CTA, +bookmark и действия приглашений сохранены. В «Моих проектах» сохранён lifecycle, +а не заменён отраслью; описание по-прежнему ограничено двумя строками. + +«Моя активность» увеличена до 156×180 px. Заголовок 14/600/18, подписи +11/400/14, значения 14/600/17 с табличными цифрами, круг иконки 22 px, +иконка 12 px. Четыре строки равномерно заполняют доступную высоту. +При очень больших числах CSS сокращает только видимое значение; полное число +остаётся в DOM и подсказке. В loading/error подсказка с прежним числом отсутствует. + +Расчёты четырёх метрик, `count$`, `countState$`, `refreshCount()`, обновление +после принятия приглашения и условия показа блока не менялись. +Активность остаётся только на dashboard/my. Backend, React, зависимости, +workflows и тестовая конфигурация не изменены. + +## Измерения и адаптивность + +Измерены `getBoundingClientRect()` реальных Angular-компонентов. Для сравнения +использованы одинаковые viewport, масштаб 100% и тестовые данные; исходный +preview собран до правок из указанной базы. Это локальная проверка с подменой +API/facade тестовыми данными, не проверка живого DEV или пользовательских данных. + +| Параметр | До | После | +| -------------------------------------------- | -------------- | -------------- | +| Активность, desktop | 157×141 | 156×180 | +| Project/invite card | 156×180 | 156×180 | +| Правая колонка, desktop: X / ширина | 1075,484 / 157 | 1075,484 / 157 | +| Активность, desktop: Y | 429,188 | 429,188 | +| Правая колонка, tablet: X / ширина | 0 / 753 | 0 / 753 | +| Активность, tablet: Y относительно документа | 1562,969 | 1562,969 | +| Правая колонка, mobile: X / ширина | 0 / 375 | 0 / 375 | +| Высота каждой строки активности | около 26 | 31,75 | +| Трансформация заголовка | uppercase | none | + +Desktop: 1440×900, полный dashboard 1440×1320. Tablet: 768×1024. +Mobile: 390×844. Крупный план приглашения рядом с активностью: 440×320. +Ширины 753 и 375 учитывают штатный вертикальный scrollbar 15 px. + +Проверены dashboard, my, subscriptions, all, invites: горизонтального overflow +нет, размеры карточек сохранены. На mobile/tablet правая колонка остаётся +ниже списка в существующем адаптивном layout. Приглашение и активность не +перекрываются. Длинные названия/описания сокращаются внутри карточек, +описание не пересекается с CTA. Зазор title–description публичной карточки 2 px. + +Числа 0, 26, 123 и 9999 полностью помещаются. Для 999999999999 работает +ellipsis с полным значением в DOM/title. Во всех этих случаях и при loading/error +активность остаётся 156×180 px. Нули отображаются как 0; loading/error — четыре «—». +Нет дополнительных кнопок или метрик. Иконки остаются `aria-hidden`. + +Исходные измерения: [measurements.json](measurements.json). Мобильный Y в JSON +зависит от прокрутки к кнопкам приглашения; сравнение положения по X не зависит. +В консоли локального preview нет ошибок; есть существующее предупреждение +NG0912 о двух IconComponent, одинаковое до и после. + +## Скриншоты + +| Поверхность | До | После | +| -------------------------------- | --------------------------------------------- | ----------------------------------------------- | +| Dashboard целиком | [до](screenshots/dashboard-before.png) | [после](screenshots/dashboard-after.png) | +| Мои подписки | [до](screenshots/subscriptions-before.png) | [после](screenshots/subscriptions-after.png) | +| Витрина / все проекты | [до](screenshots/all-before.png) | [после](screenshots/all-after.png) | +| Приглашения | [до](screenshots/invites-before.png) | [после](screenshots/invites-after.png) | +| Приглашение рядом с активностью | [до](screenshots/invite-activity-before.png) | [после](screenshots/invite-activity-after.png) | +| Mobile, первый экран | [до](screenshots/mobile-dashboard-before.png) | [после](screenshots/mobile-dashboard-after.png) | +| Mobile, приглашение и активность | [до](screenshots/mobile-right-before.png) | [после](screenshots/mobile-right-after.png) | +| Tablet, dashboard | [до](screenshots/tablet-dashboard-before.png) | [после](screenshots/tablet-dashboard-after.png) | + +Дополнительно: [mobile с длинным именем приглашающего](screenshots/mobile-invites-after.png), +[длинные публичные карточки](screenshots/subscriptions-long-after.png), +tablet: [my](screenshots/tablet-my-after.png), +[subscriptions](screenshots/tablet-subscriptions-after.png), +[all](screenshots/tablet-all-after.png), [invites](screenshots/tablet-invites-after.png). + +Состояния активности: [0](screenshots/activity-0.png), [26](screenshots/activity-26.png), +[123](screenshots/activity-123.png), [9999](screenshots/activity-9999.png), +[очень большое число](screenshots/activity-999999999999.png), +[loading](screenshots/activity-loading.png), [error](screenshots/activity-error.png). + +![Приглашение и активность после правки](screenshots/invite-activity-after.png) + +## Проверки + +```sh +npm ci +npm run test:ci -- --pool=forks projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts projects/social_platform/src/app/ui/widgets/projects-filter/projects-filter.component.spec.ts projects/social_platform/src/app/ui/pages/projects +npm run lint:ts +npx stylelint projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss +npm run test:ci +npm run test:ci -- --pool=forks +npm run build:prod +git diff --check +``` + +- `npm ci`: exit 0; зависимости и lockfile не менялись. +- Targeted: **114/114**, 26 файлов, exit 0. Включены InfoCard, Activity, + фактический DOM ProjectsComponent, Dashboard/List и отраслевой фильтр. + Проверены отсутствие отрасли и её скрытого title, lifecycle/роль/доступ, + маршруты, подписки, приглашения, четыре метрики и смена состояния загрузки. +- `lint:ts`: exit 0; шесть прежних `Unused eslint-disable` предупреждений + в logger.service.ts, chat-message.model.ts и message-input.component.ts. +- Scoped Stylelint: exit 0. +- Scoped Prettier `--check`: exit 0 для изменённых TS/HTML и документации. + SCSS по существующему `.prettierignore` проверяется Stylelint. + Полный `format:check` не запускался; чужие файлы не форматировались. +- Обычный `npm run test:ci`: **exit 1**, 378 suites не начали тесты из-за + `NG0401: No platform exists!` в `test-setup.ts:31 → setupTestBed()`. + Эта же ошибка воспроизведена на чистом checkout базы указанным ниже тестом. + Это не успешный штатный запуск; harness и конфигурация не исправлялись. +- Полный повтор с `--pool=forks`: **1741/1741**, 378 файлов, **exit 0**, + 200,86 секунды; без NG0401 и unhandled errors. Ошибка teardown ngx-autosize + в этом запуске не возникла. +- `build:prod`: exit 0. Есть прежние Sass/CommonJS предупреждения. + Windows-команда `build:sprite` генерирует пустой sprite из-за кавычек glob; + её локальный побочный diff восстановлен из HEAD и в PR не включён. + Браузерные preview используют исходный sprite; production-артефакт не развёртывался. +- `git diff --check`: exit 0. + +Проверка NG0401 на **чистой базе b9ffb77ec79819ab555fef0ee614cd3ea00bc616**: + +```sh +npm run test:ci -- projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts +``` + +Результат: exit 1, 1 suite, тесты не начали выполнение, та же ошибка +`NG0401 → setupTestBed()`. Файлы базы не менялись, `git status --short` пуст; +использован тот же установленный набор зависимостей через локальный junction. + +Merge и deploy не выполнялись. Действия с живыми приглашениями или подписками +не выполнялись; их поведение проверено существующими и обновлёнными тестами. diff --git a/docs/project-cards-activity/measurements.json b/docs/project-cards-activity/measurements.json new file mode 100644 index 000000000..9dd01a42c --- /dev/null +++ b/docs/project-cards-activity/measurements.json @@ -0,0 +1,784 @@ +[ + { + "version": "before", + "surface": "dashboard", + "viewport": 1440, + "activity": { + "height": 141, + "width": 157, + "x": 1075.484375, + "y": 429.1875 + }, + "card": { + "height": 180, + "width": 156, + "x": 368.65625, + "y": 230.59375 + }, + "invite": { + "height": 180, + "width": 156, + "x": 1075.484375, + "y": 233.1875 + }, + "overflow": false, + "right": { + "height": 1131.78125, + "width": 157, + "x": 1075.484375, + "y": 117 + } + }, + { + "version": "after", + "surface": "dashboard", + "viewport": 1440, + "activity": { + "height": 180, + "width": 156, + "x": 1075.484375, + "y": 429.1875 + }, + "card": { + "height": 180, + "width": 156, + "x": 368.65625, + "y": 230.59375 + }, + "invite": { + "height": 180, + "width": 156, + "x": 1075.484375, + "y": 233.1875 + }, + "overflow": false, + "right": { + "height": 1131.78125, + "width": 157, + "x": 1075.484375, + "y": 117 + } + }, + { + "version": "after", + "surface": "activity", + "value": 0, + "height": 180, + "overflow": false, + "rows": [ + { + "height": 31.75, + "label": "Проектов", + "overlap": false, + "title": "0", + "value": "0", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Я лидер", + "overlap": false, + "title": "0", + "value": "0", + "valueClipped": false + }, + { + "height": 31.75, + "label": "В программе", + "overlap": false, + "title": "0", + "value": "0", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Сдано", + "overlap": false, + "title": "0", + "value": "0", + "valueClipped": false + } + ], + "width": 156 + }, + { + "version": "after", + "surface": "activity", + "value": 26, + "height": 180, + "overflow": false, + "rows": [ + { + "height": 31.75, + "label": "Проектов", + "overlap": false, + "title": "26", + "value": "26", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Я лидер", + "overlap": false, + "title": "26", + "value": "26", + "valueClipped": false + }, + { + "height": 31.75, + "label": "В программе", + "overlap": false, + "title": "26", + "value": "26", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Сдано", + "overlap": false, + "title": "26", + "value": "26", + "valueClipped": false + } + ], + "width": 156 + }, + { + "version": "after", + "surface": "activity", + "value": 123, + "height": 180, + "overflow": false, + "rows": [ + { + "height": 31.75, + "label": "Проектов", + "overlap": false, + "title": "123", + "value": "123", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Я лидер", + "overlap": false, + "title": "123", + "value": "123", + "valueClipped": false + }, + { + "height": 31.75, + "label": "В программе", + "overlap": false, + "title": "123", + "value": "123", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Сдано", + "overlap": false, + "title": "123", + "value": "123", + "valueClipped": false + } + ], + "width": 156 + }, + { + "version": "after", + "surface": "activity", + "value": 9999, + "height": 180, + "overflow": false, + "rows": [ + { + "height": 31.75, + "label": "Проектов", + "overlap": false, + "title": "9999", + "value": "9999", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Я лидер", + "overlap": false, + "title": "9999", + "value": "9999", + "valueClipped": false + }, + { + "height": 31.75, + "label": "В программе", + "overlap": false, + "title": "9999", + "value": "9999", + "valueClipped": false + }, + { + "height": 31.75, + "label": "Сдано", + "overlap": false, + "title": "9999", + "value": "9999", + "valueClipped": false + } + ], + "width": 156 + }, + { + "version": "after", + "surface": "activity", + "value": 999999999999, + "height": 180, + "overflow": false, + "rows": [ + { + "height": 31.75, + "label": "Проектов", + "overlap": false, + "title": "999999999999", + "value": "999999999999", + "valueClipped": true + }, + { + "height": 31.75, + "label": "Я лидер", + "overlap": false, + "title": "999999999999", + "value": "999999999999", + "valueClipped": true + }, + { + "height": 31.75, + "label": "В программе", + "overlap": false, + "title": "999999999999", + "value": "999999999999", + "valueClipped": true + }, + { + "height": 31.75, + "label": "Сдано", + "overlap": false, + "title": "999999999999", + "value": "999999999999", + "valueClipped": true + } + ], + "width": 156 + }, + { + "version": "after", + "surface": "activity", + "state": "loading", + "height": 180, + "values": [ + { + "text": "—", + "title": null + }, + { + "text": "—", + "title": null + }, + { + "text": "—", + "title": null + }, + { + "text": "—", + "title": null + } + ], + "width": 156 + }, + { + "version": "after", + "surface": "activity", + "state": "error", + "height": 180, + "values": [ + { + "text": "—", + "title": null + }, + { + "text": "—", + "title": null + }, + { + "text": "—", + "title": null + }, + { + "text": "—", + "title": null + } + ], + "width": 156 + }, + { + "version": "before", + "surface": "subscriptions", + "viewport": 1440, + "activityCount": 0, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 3, + "overflow": false + }, + { + "version": "after", + "surface": "subscriptions", + "viewport": 1440, + "activityCount": 0, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "before", + "surface": "all", + "viewport": 1440, + "activityCount": 0, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 3, + "overflow": false + }, + { + "version": "after", + "surface": "all", + "viewport": 1440, + "activityCount": 0, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "before", + "surface": "invites", + "viewport": 1440, + "activityCount": 0, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 2, + "overflow": false + }, + { + "version": "after", + "surface": "invites", + "viewport": 1440, + "activityCount": 0, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "before", + "surface": "dashboard", + "viewport": 390, + "activity": { + "height": 141, + "width": 157, + "x": 0, + "y": 602.96875 + }, + "invite": { + "height": 180, + "width": 156, + "x": 0 + }, + "overflow": false, + "overlap": false, + "right": { + "width": 375, + "x": 0 + } + }, + { + "version": "after", + "surface": "dashboard", + "viewport": 390, + "activity": { + "height": 180, + "width": 156, + "x": 0, + "y": 563.96875 + }, + "invite": { + "height": 180, + "width": 156, + "x": 0 + }, + "overflow": false, + "overlap": false, + "right": { + "width": 375, + "x": 0 + } + }, + { + "version": "after", + "surface": "my", + "viewport": 390, + "long": true, + "activityCount": 1, + "activitySize": { + "h": 180, + "w": 156 + }, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "after", + "surface": "subscriptions", + "viewport": 390, + "long": true, + "activityCount": 0, + "activitySize": null, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "after", + "surface": "all", + "viewport": 390, + "long": true, + "activityCount": 0, + "activitySize": null, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "after", + "surface": "invites", + "viewport": 390, + "long": true, + "activityCount": 0, + "activitySize": null, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "after", + "surface": "my", + "viewport": 768, + "long": true, + "activityCount": 1, + "activitySize": { + "h": 180, + "w": 156 + }, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "after", + "surface": "subscriptions", + "viewport": 768, + "long": true, + "activityCount": 0, + "activitySize": null, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "after", + "surface": "all", + "viewport": 768, + "long": true, + "activityCount": 0, + "activitySize": null, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "after", + "surface": "invites", + "viewport": 768, + "long": true, + "activityCount": 0, + "activitySize": null, + "cardSizes": [ + { + "h": 180, + "w": 156 + }, + { + "h": 180, + "w": 156 + } + ], + "industries": 0, + "overflow": false + }, + { + "version": "before", + "surface": "dashboard", + "viewport": 768, + "activity": { + "height": 141, + "width": 157, + "x": 0, + "y": 1562.96875 + }, + "overflow": false, + "right": { + "width": 753, + "x": 0 + } + }, + { + "version": "after", + "surface": "dashboard", + "viewport": 768, + "activity": { + "height": 180, + "width": 156, + "x": 0, + "y": 1562.96875 + }, + "overflow": false, + "right": { + "width": 753, + "x": 0 + } + } +] diff --git a/docs/project-cards-activity/screenshots/activity-0.png b/docs/project-cards-activity/screenshots/activity-0.png new file mode 100644 index 000000000..beb325e3f Binary files /dev/null and b/docs/project-cards-activity/screenshots/activity-0.png differ diff --git a/docs/project-cards-activity/screenshots/activity-123.png b/docs/project-cards-activity/screenshots/activity-123.png new file mode 100644 index 000000000..20f14d9e5 Binary files /dev/null and b/docs/project-cards-activity/screenshots/activity-123.png differ diff --git a/docs/project-cards-activity/screenshots/activity-26.png b/docs/project-cards-activity/screenshots/activity-26.png new file mode 100644 index 000000000..65c76997c Binary files /dev/null and b/docs/project-cards-activity/screenshots/activity-26.png differ diff --git a/docs/project-cards-activity/screenshots/activity-9999.png b/docs/project-cards-activity/screenshots/activity-9999.png new file mode 100644 index 000000000..307cab8a2 Binary files /dev/null and b/docs/project-cards-activity/screenshots/activity-9999.png differ diff --git a/docs/project-cards-activity/screenshots/activity-999999999999.png b/docs/project-cards-activity/screenshots/activity-999999999999.png new file mode 100644 index 000000000..3b16b5c4f Binary files /dev/null and b/docs/project-cards-activity/screenshots/activity-999999999999.png differ diff --git a/docs/project-cards-activity/screenshots/activity-error.png b/docs/project-cards-activity/screenshots/activity-error.png new file mode 100644 index 000000000..07b6a6335 Binary files /dev/null and b/docs/project-cards-activity/screenshots/activity-error.png differ diff --git a/docs/project-cards-activity/screenshots/activity-loading.png b/docs/project-cards-activity/screenshots/activity-loading.png new file mode 100644 index 000000000..07b6a6335 Binary files /dev/null and b/docs/project-cards-activity/screenshots/activity-loading.png differ diff --git a/docs/project-cards-activity/screenshots/all-after.png b/docs/project-cards-activity/screenshots/all-after.png new file mode 100644 index 000000000..89adf6cb2 Binary files /dev/null and b/docs/project-cards-activity/screenshots/all-after.png differ diff --git a/docs/project-cards-activity/screenshots/all-before.png b/docs/project-cards-activity/screenshots/all-before.png new file mode 100644 index 000000000..fd7504138 Binary files /dev/null and b/docs/project-cards-activity/screenshots/all-before.png differ diff --git a/docs/project-cards-activity/screenshots/dashboard-after.png b/docs/project-cards-activity/screenshots/dashboard-after.png new file mode 100644 index 000000000..b96247e0e Binary files /dev/null and b/docs/project-cards-activity/screenshots/dashboard-after.png differ diff --git a/docs/project-cards-activity/screenshots/dashboard-before.png b/docs/project-cards-activity/screenshots/dashboard-before.png new file mode 100644 index 000000000..5cd985386 Binary files /dev/null and b/docs/project-cards-activity/screenshots/dashboard-before.png differ diff --git a/docs/project-cards-activity/screenshots/invite-activity-after.png b/docs/project-cards-activity/screenshots/invite-activity-after.png new file mode 100644 index 000000000..a3b3a72bd Binary files /dev/null and b/docs/project-cards-activity/screenshots/invite-activity-after.png differ diff --git a/docs/project-cards-activity/screenshots/invite-activity-before.png b/docs/project-cards-activity/screenshots/invite-activity-before.png new file mode 100644 index 000000000..f1f51ea85 Binary files /dev/null and b/docs/project-cards-activity/screenshots/invite-activity-before.png differ diff --git a/docs/project-cards-activity/screenshots/invites-after.png b/docs/project-cards-activity/screenshots/invites-after.png new file mode 100644 index 000000000..d6e1ca3dc Binary files /dev/null and b/docs/project-cards-activity/screenshots/invites-after.png differ diff --git a/docs/project-cards-activity/screenshots/invites-before.png b/docs/project-cards-activity/screenshots/invites-before.png new file mode 100644 index 000000000..c73440b0b Binary files /dev/null and b/docs/project-cards-activity/screenshots/invites-before.png differ diff --git a/docs/project-cards-activity/screenshots/mobile-dashboard-after.png b/docs/project-cards-activity/screenshots/mobile-dashboard-after.png new file mode 100644 index 000000000..517d4de7e Binary files /dev/null and b/docs/project-cards-activity/screenshots/mobile-dashboard-after.png differ diff --git a/docs/project-cards-activity/screenshots/mobile-dashboard-before.png b/docs/project-cards-activity/screenshots/mobile-dashboard-before.png new file mode 100644 index 000000000..acac76cea Binary files /dev/null and b/docs/project-cards-activity/screenshots/mobile-dashboard-before.png differ diff --git a/docs/project-cards-activity/screenshots/mobile-invites-after.png b/docs/project-cards-activity/screenshots/mobile-invites-after.png new file mode 100644 index 000000000..09a9ba990 Binary files /dev/null and b/docs/project-cards-activity/screenshots/mobile-invites-after.png differ diff --git a/docs/project-cards-activity/screenshots/mobile-right-after.png b/docs/project-cards-activity/screenshots/mobile-right-after.png new file mode 100644 index 000000000..80d36f8d3 Binary files /dev/null and b/docs/project-cards-activity/screenshots/mobile-right-after.png differ diff --git a/docs/project-cards-activity/screenshots/mobile-right-before.png b/docs/project-cards-activity/screenshots/mobile-right-before.png new file mode 100644 index 000000000..4fb1a7f46 Binary files /dev/null and b/docs/project-cards-activity/screenshots/mobile-right-before.png differ diff --git a/docs/project-cards-activity/screenshots/subscriptions-after.png b/docs/project-cards-activity/screenshots/subscriptions-after.png new file mode 100644 index 000000000..c85e389e3 Binary files /dev/null and b/docs/project-cards-activity/screenshots/subscriptions-after.png differ diff --git a/docs/project-cards-activity/screenshots/subscriptions-before.png b/docs/project-cards-activity/screenshots/subscriptions-before.png new file mode 100644 index 000000000..c4882757b Binary files /dev/null and b/docs/project-cards-activity/screenshots/subscriptions-before.png differ diff --git a/docs/project-cards-activity/screenshots/subscriptions-long-after.png b/docs/project-cards-activity/screenshots/subscriptions-long-after.png new file mode 100644 index 000000000..decc1b5ee Binary files /dev/null and b/docs/project-cards-activity/screenshots/subscriptions-long-after.png differ diff --git a/docs/project-cards-activity/screenshots/tablet-all-after.png b/docs/project-cards-activity/screenshots/tablet-all-after.png new file mode 100644 index 000000000..d1900e866 Binary files /dev/null and b/docs/project-cards-activity/screenshots/tablet-all-after.png differ diff --git a/docs/project-cards-activity/screenshots/tablet-dashboard-after.png b/docs/project-cards-activity/screenshots/tablet-dashboard-after.png new file mode 100644 index 000000000..cec118de7 Binary files /dev/null and b/docs/project-cards-activity/screenshots/tablet-dashboard-after.png differ diff --git a/docs/project-cards-activity/screenshots/tablet-dashboard-before.png b/docs/project-cards-activity/screenshots/tablet-dashboard-before.png new file mode 100644 index 000000000..adf540a82 Binary files /dev/null and b/docs/project-cards-activity/screenshots/tablet-dashboard-before.png differ diff --git a/docs/project-cards-activity/screenshots/tablet-invites-after.png b/docs/project-cards-activity/screenshots/tablet-invites-after.png new file mode 100644 index 000000000..0b528dab1 Binary files /dev/null and b/docs/project-cards-activity/screenshots/tablet-invites-after.png differ diff --git a/docs/project-cards-activity/screenshots/tablet-my-after.png b/docs/project-cards-activity/screenshots/tablet-my-after.png new file mode 100644 index 000000000..5deda65e2 Binary files /dev/null and b/docs/project-cards-activity/screenshots/tablet-my-after.png differ diff --git a/docs/project-cards-activity/screenshots/tablet-subscriptions-after.png b/docs/project-cards-activity/screenshots/tablet-subscriptions-after.png new file mode 100644 index 000000000..1645b0204 Binary files /dev/null and b/docs/project-cards-activity/screenshots/tablet-subscriptions-after.png differ diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts index 5e8882dbf..d6a350a39 100644 --- a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboard.component.spec.ts @@ -120,7 +120,10 @@ describe("ProjectsDashboard: контексты карточек", () => { } else { expect(c.isSubscribed).toBe(true); expect(el.nativeElement.querySelector(".card__status")).toBeNull(); - expect(!!el.nativeElement.querySelector(".card__context--industry")).toBe(i !== 3); + expect(el.nativeElement.querySelector(".card__context--industry")).toBeNull(); + expect(el.nativeElement.querySelector(".card__project-link").hasAttribute("title")).toBe( + false, + ); expect( el.nativeElement.querySelector(".card__project-action-label").textContent.trim(), ).toBe("Открыть"); diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html index fb532db80..1d1784488 100644 --- a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html @@ -1,18 +1,18 @@
-

моя активность

+

Моя активность

@for (metric of metrics(); track metric.label) {
{{ metric.label }}
-
+
@if (isLoaded()) { {{ metric.value }} } @else { diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss index c8a41b967..8e05edbc8 100644 --- a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss @@ -1,28 +1,33 @@ :host { display: block; - width: 157px; - height: 141px; + width: 156px; + height: 180px; } .activity { box-sizing: border-box; + display: flex; + flex-direction: column; width: 100%; height: 100%; - padding: 9px 10px; + padding: 12px 10px; background-color: var(--light-white); border: 0.5px solid var(--medium-grey-for-outline); border-radius: var(--rounded-lg); &__title { - margin: 0 0 5px; - font-size: 11px; + margin: 0 0 9px; + font-size: 14px; font-weight: 600; - line-height: 14px; + line-height: 18px; color: var(--accent); - text-transform: uppercase; } &__metrics { + display: grid; + flex: 1; + grid-template-rows: repeat(4, minmax(0, 1fr)); + min-height: 0; margin: 0; } @@ -30,7 +35,6 @@ display: flex; align-items: center; justify-content: space-between; - min-height: 26px; & + & { border-top: 0.5px solid var(--medium-grey-for-outline); @@ -42,19 +46,18 @@ gap: 4px; align-items: center; min-width: 0; - font-size: 9px; + font-size: 11px; font-weight: 400; - line-height: 12px; + line-height: 14px; color: var(--black); - white-space: nowrap; } &__icon { display: grid; - flex: 0 0 20px; + flex: 0 0 22px; place-items: center; - width: 20px; - height: 20px; + width: 22px; + height: 22px; color: var(--accent); background-color: var(--accent-light); border-radius: 50%; @@ -62,10 +65,18 @@ &__value { flex: 0 0 auto; + + // Очень большие числа не вытесняют подпись; полное значение остаётся в DOM и title. + max-width: 45px; margin: 0 0 0 4px; - font-size: 13px; + overflow: hidden; + font-size: 14px; font-weight: 600; - line-height: 16px; + font-variant-numeric: tabular-nums; + line-height: 17px; color: var(--accent); + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; } } diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts index 434569b54..8ccee9677 100644 --- a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts @@ -47,6 +47,7 @@ describe("ProjectActivityCardComponent", () => { expect(element.textContent).not.toContain("скоро"); expect(element.textContent).not.toContain("пока закрыто"); expect(element.textContent).not.toContain("Подробнее"); + expect(element.querySelector("button, a")).toBeNull(); }); it.each(["loading", "error"] as const)("не показывает выдуманные числа в состоянии %s", state => { @@ -55,4 +56,33 @@ describe("ProjectActivityCardComponent", () => { Array.from(element.querySelectorAll(".activity__value")).map(x => x.textContent?.trim()), ).toEqual(["—", "—", "—", "—"]); }); + + it("сохраняет заголовок в обычном регистре и декоративные иконки", () => { + const element = render().nativeElement as HTMLElement; + expect(element.querySelector("h2")?.textContent?.trim()).toBe("Моя активность"); + expect(element.querySelectorAll('.activity__icon[aria-hidden="true"]')).toHaveLength(4); + }); + + it.each([0, 26, 123, 9999, 999999999999])( + "не подменяет значение %s сокращённым числом", + value => { + const element = render(count({ my: value })).nativeElement as HTMLElement; + const counter = element.querySelector(".activity__value")!; + expect(counter.textContent?.trim()).toBe(String(value)); + expect(counter.title).toBe(String(value)); + }, + ); + + it("при повторной загрузке убирает устаревшие числа, включая title", () => { + const fixture = render(count({ my: 9999 })); + for (const state of ["loading", "error"] as const) { + fixture.componentRef.setInput("state", state); + fixture.detectChanges(); + const values = Array.from( + fixture.nativeElement.querySelectorAll(".activity__value"), + ) as HTMLElement[]; + expect(values.map(element => element.textContent?.trim())).toEqual(["—", "—", "—", "—"]); + expect(values.every(element => !element.hasAttribute("title"))).toBe(true); + } + }); }); diff --git a/projects/social_platform/src/app/ui/pages/projects/projects-presentation.spec.ts b/projects/social_platform/src/app/ui/pages/projects/projects-presentation.spec.ts new file mode 100644 index 000000000..9d109fb64 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/projects-presentation.spec.ts @@ -0,0 +1,114 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { FormControl, FormGroup } from "@angular/forms"; +import { provideRouter } from "@angular/router"; +import { of } from "rxjs"; +import { OfficeInfoService } from "@api/office/facades/office-info.service"; +import { ProjectsInfoService } from "@api/project/facades/projects-info.service"; +import { ProjectsUIInfoService } from "@api/project/facades/ui/projects-ui-info.service"; +import { AddProjectSubscriptionUseCase } from "@api/project/use-cases/add-project-subscription.use-case"; +import { DeleteProjectSubscriptionUseCase } from "@api/project/use-cases/delete-project-subscription.use-case"; +import { SwipeService } from "@api/swipe/swipe.service"; +import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { ok } from "@domain/shared/result.type"; +import { projectCardFixture } from "@ui/widgets/info-card/info-card.fixture"; +import { ProjectsComponent } from "./projects.component"; + +describe("ProjectsComponent: приглашение и активность на странице", () => { + let section: string; + const office = { onAcceptInvite: vi.fn(), onRejectInvite: vi.fn() }; + + beforeEach(async () => { + section = "dashboard"; + vi.clearAllMocks(); + await TestBed.configureTestingModule({ + imports: [ProjectsComponent], + providers: [ + provideRouter([]), + { provide: OfficeInfoService, useValue: office }, + { + provide: IndustryRepositoryPort, + useValue: { industries: signal([{ id: 1, name: "Образование" }]) }, + }, + { provide: AddProjectSubscriptionUseCase, useValue: { execute: () => of(ok(undefined)) } }, + { + provide: DeleteProjectSubscriptionUseCase, + useValue: { execute: () => of(ok(undefined)) }, + }, + ], + }) + .overrideComponent(ProjectsComponent, { + set: { + providers: [ + { + provide: ProjectsInfoService, + useValue: { + isDashboard: () => section === "dashboard", + isMy: () => section === "my", + isSubs: () => section === "subscriptions", + isInvites: () => section === "invites", + isAll: () => section === "all", + projectCount: signal({ my: 26, myLeader: 23, myInProgram: 1, mySubmitted: 4 }), + projectCountState: signal("loaded"), + initializationProjects: vi.fn(), + }, + }, + { + provide: ProjectsUIInfoService, + useValue: { + searchForm: new FormGroup({ search: new FormControl("") }), + myInvites: signal([ + projectCardFixture({ inviteId: 801, industry: 1, name: "Проект приглашения" }), + ]), + }, + }, + { provide: SwipeService, useValue: { isFilterOpen: signal(false) } }, + ], + }, + }) + .compileComponents(); + }); + + afterEach(async () => { + // Завершаем отложенное прикрепление portal общей модалки до уничтожения TestBed. + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + it.each(["dashboard", "my", "subscriptions", "invites", "all"])( + "сохраняет размещение активности в разделе %s", + current => { + section = current; + const fixture = TestBed.createComponent(ProjectsComponent); + fixture.detectChanges(); + const activity = fixture.nativeElement.querySelector("app-project-activity-card"); + if (current === "dashboard" || current === "my") { + expect(activity.querySelector("h2").textContent.trim()).toBe("Моя активность"); + expect( + Array.from(activity.querySelectorAll(".activity__value"), (node: unknown) => + (node as HTMLElement).textContent?.trim(), + ), + ).toEqual(["26", "23", "1", "4"]); + } else { + expect(activity).toBeNull(); + } + }, + ); + + it("приглашение справа не содержит отрасль и сохраняет оба действия", () => { + const fixture = TestBed.createComponent(ProjectsComponent); + fixture.detectChanges(); + const invitation: HTMLElement = fixture.nativeElement.querySelector( + ".page__invites app-info-card", + ); + expect(invitation.textContent).not.toContain("Образование"); + expect(invitation.querySelector(".card__industry, .card__context--industry")).toBeNull(); + expect(invitation.querySelector(".card__name")?.textContent).toContain("Проект приглашения"); + const buttons = Array.from(invitation.querySelectorAll("button")); + buttons.find(button => button.textContent?.trim() === "принять")!.click(); + buttons.find(button => button.textContent?.trim() === "отклонить")!.click(); + expect(office.onAcceptInvite).toHaveBeenCalledExactlyOnceWith(801); + expect(office.onRejectInvite).toHaveBeenCalledExactlyOnceWith(801); + }); +}); diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html index 6f87e5597..b07e47fbe 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.html @@ -40,7 +40,6 @@ class="card__project-link" [routerLink]="AppRoutes.projects.detail(info()?.id)" [attr.aria-label]="'Открыть проект «' + info()?.name + '»'" - [attr.title]="projectIndustry()" > @@ -60,10 +59,6 @@

{{ presentation.statusLabel }}

- } @else if (projectIndustry(); as industry) { -

- {{ industry }} -

}
@@ -118,13 +113,8 @@

{{ info().name }}

} @else if (type() === "projects" || type() === "invite") { @if (info().name) { -

{{ info().name | truncate: 12 }}

+

{{ info().name }}

} -

- @if (industryRepository.getOne(info()?.industry!); as industry) { - {{ industry?.name }} - } -

} @else {

{{ info()?.firstName | truncate: 10 }}

{{ info()?.lastName | truncate: 10 }}

@@ -147,7 +137,7 @@ class="card__description" [class.card__description--project]="type() === 'projects'" [style.max-height]=" - isProjectCard() ? '26px' : type() === 'invite' ? null : type() === 'members' ? '50px' : '24px' + isProjectCard() || type() === 'invite' ? null : type() === 'members' ? '50px' : '24px' " [class.invite-description]="type() === 'invite'" > diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss index 806c321c9..eb97e32e3 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss @@ -139,13 +139,6 @@ color: var(--accent); } - &__industry { - padding: 2px 36px; - color: var(--light-white); - background-color: var(--green); - border-radius: var(--rounded-xl); - } - &__description { display: flex; flex: 1; @@ -286,18 +279,19 @@ } .card__description { - flex: 0 0 26px; + // Место удалённой отрасли отдано описанию без увеличения внешней карточки. + flex: 0 0 52px; width: 100%; - height: 26px; - min-height: 26px; + height: 52px; + min-height: 52px; p { - /* stylelint-disable-next-line value-no-vendor-prefix -- Префикс сохраняет двухстрочное сокращение описания. */ + /* stylelint-disable-next-line value-no-vendor-prefix -- Префикс ограничивает описание четырьмя строками. */ display: -webkit-box; overflow: hidden; font-size: 10px; line-height: 13px; - -webkit-line-clamp: 2; + -webkit-line-clamp: 4; -webkit-box-orient: vertical; } } @@ -350,6 +344,7 @@ p { font-size: 9px; line-height: 12px; + -webkit-line-clamp: 2; } } @@ -381,16 +376,16 @@ } } - .card__industry { - width: 100%; - padding: 0 8px; + .card__name { + /* stylelint-disable-next-line value-no-vendor-prefix -- Освободившаяся строка сохраняет длинное название. */ + display: -webkit-box; + max-height: 30px; overflow: hidden; - font-size: 11px; - font-weight: 600; - line-height: 14px; - color: var(--black); - text-overflow: ellipsis; - white-space: nowrap; + line-height: 15px; + overflow-wrap: anywhere; + white-space: normal; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } .card__description { @@ -466,22 +461,6 @@ white-space: nowrap; background: var(--light-gray); border-radius: 11px; - - &--industry { - font-size: 11px; - color: var(--accent-dark); - background: var(--accent-light); - - span { - overflow: hidden; - text-overflow: ellipsis; - } - - &:focus-visible { - outline: 2px solid var(--accent-dark); - outline-offset: 2px; - } - } } &__status { diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts index 321a713dc..c08c35bd1 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.spec.ts @@ -233,7 +233,7 @@ describe("InfoCardComponent: статусы моих проектов", () => { }); it.each(["base", "subs"] as const)( - "%s: отрасль, полный текст, Открыть и отсутствие lifecycle/шума", + "%s: полный текст и Открыть без отрасли и lifecycle", appearance => { const project = projectCardFixture({ draft: true, partnerProgram: projectCardProgram(true) }); fixture.componentRef.setInput("info", project); @@ -243,9 +243,8 @@ describe("InfoCardComponent: статусы моих проектов", () => { expect(card().querySelector(".card__access")).toBeNull(); expect(fixture.nativeElement.querySelector(".card--project")).not.toBeNull(); expect(card().querySelector(".card__name")?.textContent).toBe(project.name); - expect(card().querySelector(".card__context--industry")?.textContent?.trim()).toBe( - "Образование", - ); + expect(card().querySelector(".card__context--industry")).toBeNull(); + expect(card().textContent).not.toContain("Образование"); expect(card().querySelector(".card__project-action")?.textContent?.trim()).toBe( "Открыть", ); @@ -268,17 +267,33 @@ describe("InfoCardComponent: статусы моих проектов", () => { expect(card().querySelector(".card__name")?.textContent).toBe(projectCardFixture().name); }); - it("длинная отрасль доступна целиком в подсказке и с клавиатуры", () => { + it.each(["base", "subs"] as const)("%s: отрасль не остаётся в скрытой подсказке", appearance => { const name = "Очень длинное название отрасли проекта"; - vi.spyOn(TestBed.inject(IndustryRepositoryPort), "getOne").mockReturnValue({ id: 1, name }); - fixture.componentRef.setInput("appereance", "subs"); + const getIndustry = vi.spyOn(TestBed.inject(IndustryRepositoryPort), "getOne"); + getIndustry.mockReturnValue({ id: 1, name }); + fixture.componentRef.setInput("appereance", appearance); fixture.detectChanges(); - const context = card().querySelector(".card__context--industry")!; - expect(context.title).toBe(name); - expect(context.textContent?.trim()).toBe(name); + expect(card().querySelector(".card__context--industry, .card__industry")).toBeNull(); + expect(card().textContent).not.toContain(name); const link = card().querySelector(".card__project-link")!; - expect(link.title).toBe(name); + expect(link.hasAttribute("title")).toBe(false); expect(link.tabIndex).toBe(0); + expect(getIndustry).not.toHaveBeenCalled(); + }); + + it("приглашение сохраняет название, действия и данные проекта без отрасли", () => { + const project = projectCardFixture({ + name: "Длинное название приглашённого проекта", + inviteId: 801, + }); + fixture.componentRef.setInput("type", "invite"); + fixture.componentRef.setInput("info", project); + fixture.detectChanges(); + expect(card().querySelector(".card__industry, .card__context--industry")).toBeNull(); + expect(card().textContent).not.toContain("Образование"); + expect(card().querySelector(".card__name")?.textContent).toBe(project.name); + expect(card().querySelectorAll(".card__invite-actions button")).toHaveLength(2); + expect(project.industry).toBe(projectCardFixture().industry); }); it.each(["base", "subs"] as const)( diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts index 03470a66b..1a4b946e3 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.ts @@ -24,7 +24,6 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { AddProjectSubscriptionUseCase } from "@api/project/use-cases/add-project-subscription.use-case"; import { DeleteProjectSubscriptionUseCase } from "@api/project/use-cases/delete-project-subscription.use-case"; import { AppRoutes } from "@api/paths/app-routes"; -import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; import { Project } from "@domain/project/project.model"; interface MyProjectPresentation { @@ -62,7 +61,6 @@ export class InfoCardComponent { private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly addProjectSubscriptionUseCase = inject(AddProjectSubscriptionUseCase); private readonly deleteProjectSubscriptionUseCase = inject(DeleteProjectSubscriptionUseCase); - public readonly industryRepository = inject(IndustryRepositoryPort); private readonly router = inject(Router); private readonly logger = inject(LoggerService); @@ -84,15 +82,6 @@ export class InfoCardComponent { () => this.type() === "projects" && this.appereance() !== "empty", ); - /** Отрасль берётся из уже загруженного справочника; пустая плашка не занимает строку. */ - protected readonly projectIndustry = computed(() => { - if (!this.isProjectCard() || this.appereance() === "my") return null; - const industryId = this.info()?.industry; - return industryId == null - ? null - : this.industryRepository.getOne(industryId)?.name?.trim() || null; - }); - /** * Lifecycle зависит только от проекта: submitted > draft > program > published. * Роль определяется отдельно по текущему профилю. Отсутствующие ID не делают diff --git a/projects/social_platform/src/app/ui/widgets/projects-filter/projects-filter.component.spec.ts b/projects/social_platform/src/app/ui/widgets/projects-filter/projects-filter.component.spec.ts index d62228af1..932edeabf 100644 --- a/projects/social_platform/src/app/ui/widgets/projects-filter/projects-filter.component.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/projects-filter/projects-filter.component.spec.ts @@ -3,10 +3,11 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ProjectsFilterComponent } from "./projects-filter.component"; -import { provideRouter } from "@angular/router"; +import { provideRouter, Router } from "@angular/router"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; import { signal } from "@angular/core"; +import { ProjectsFilterInfoService } from "./service/projects-filter-info.service"; describe("ProjectsFilterComponent", () => { let component: ProjectsFilterComponent; @@ -14,7 +15,7 @@ describe("ProjectsFilterComponent", () => { beforeEach(async () => { const industrySpy = { - industries: signal([]), + industries: signal([{ id: 5, name: "EdTech" }]), }; await TestBed.configureTestingModule({ @@ -32,4 +33,20 @@ describe("ProjectsFilterComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + it("сохраняет справочник отраслей и фильтрацию через query-параметр industry", async () => { + await fixture.whenStable(); + const service = fixture.debugElement.injector.get(ProjectsFilterInfoService); + expect(service.industries()).toEqual([{ id: 5, label: "EdTech", value: "EdTech" }]); + const navigate = vi.spyOn(TestBed.inject(Router), "navigate").mockResolvedValue(true); + component.onFilterByIndustry(5); + expect(navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: { industry: 5 }, + queryParamsHandling: "merge", + }), + ); + vi.restoreAllMocks(); + }); });