Frontend upgrade#80
Merged
Merged
Conversation
… frontend applications
… Tailwind Upgraded the Nx monorepo (web-portal + admin-cms) from Angular 19 to Angular 21.2.9. Replaced @ngx-translate with @jsverse/transloco and Bootstrap grid utilities with Tailwind CSS v3 backed by the DGA design token palette. - All @angular/* packages pinned to 21.2.9, Nx to 20.5, TypeScript to 5.9.3 - tsconfig.base.json: moduleResolution set to "bundler" for v21 compatibility - ng-packagr locked to ~19.2.0 to resolve Nx 20 / Angular 21 executor mismatch - Migrated 838 pipe usages from `| translate` to `| transloco` workspace-wide - provideTransloco() wired in both app configs via TranslocoHttpLoader - Tailwind directives added to both global stylesheets; @use declarations moved above @tailwind to satisfy Dart Sass ordering rules - Both apps already use bootstrapApplication() + standalone components - api-client lib generates typed clients from external/internal OpenAPI specs using @hey-api/openapi-ts
… structure
Move duplicated TranslocoHttpLoader and LocaleSwitcherComponent out of
both apps and into libs/i18n, making them available via the @frontend/i18n
path alias. Align admin-cms top-level orphan folders into their correct
locations under core/ and features/.
- Add TranslocoHttpLoader to libs/i18n (properly typed Observable<Translation>)
- Add LocaleSwitcherComponent to libs/i18n with inline template and fixed
spec (spy was targeting the removed translate.use() instead of setActiveLang)
- Both app.config.ts and layout components now import from @frontend/i18n
- Remove 4 duplicated files: translate-loader.factory.ts + locale-switcher/
from both web-portal and admin-cms
- Move admin-cms auth-callback/ → core/auth/auth-callback.page.ts
- Move admin-cms auth-toolbar/ → core/layout/auth-toolbar.component.*
(spec rewritten against DevAuthService, replacing stale OidcSecurityService mock)
- Move admin-cms profile/ → features/account/profile.*
…, Sass migration, cities lazy-load
Four independent improvements across web-portal and admin-cms:
follows-registry → follows-store
- Rename FollowsRegistryService to FollowsStoreService for naming
consistency with map-viewer-store and scenario-builder-store
- Update follow.directive and all spec files accordingly
Mock files → testing/ subfolders
- Move knowledge-center/mock-data.ts, countries/countries-mock.ts, and
translations/translations-mock.ts into testing/ subdirectories so
fallback/demo data is visually distinct from production code
- Fix relative import paths inside moved files (./types → ../types)
- Update all consumer import paths
admin-cms Sass: @import → @use
- Replace deprecated @import of _fancy.scss and _admin-polish.scss with
@use, hoisted above @tailwind directives per Dart Sass 3.0 requirements
Cities data: static TS bundles → lazy-loaded JSON assets
- Delete cities.data.ts (794 lines) and cities-extra.data.ts (194 lines)
from the JS bundle entirely
- Add world-map.types.ts with clean type-only definitions
- Add CitiesService: ensureLoaded() fetches cities.json + cities-extra.json
in parallel via HttpClient, caches result in a signal (idempotent)
- WorldMapComponent: parallel-fetches cities alongside topology in ngOnInit,
uses citiesService.allCities() in D3 render/fitToFilter/rebuildBoundaries
(fixed this-context bug in rebuildBoundaries by capturing reference before
the D3 .each() callback)
- WorldMapPage: injects CitiesService, converts ALL_CITIES usages to
computed(() => citiesService.allCities()) for reactive updates
- Extract isInternalUrl() from both admin-cms interceptors into a shared
core/http/is-internal-url.ts, matching the pattern already used in
web-portal (removes triplication of the same 5-line function)
- Move serverErrorInterceptor from both apps into libs/ui-kit so a single
implementation is tested and maintained in one place; both apps now
import from @frontend/ui-kit
- Extend serverErrorInterceptor to handle status 0 (network failure →
errors.network) and status 429 (rate limit → errors.rateLimit); add
errors.rateLimit key to en/ar translation files
- Add localeInterceptor to libs/i18n — stamps Accept-Language: ar|en on
every request so the backend can return localised error messages;
registered first in both apps' interceptor chains
- Add guestGuard to web-portal — redirects authenticated users away from
/login and /register to /me, matching the pattern in the cce-admin app
Replace the BFF/cookie pattern with a full JWT-based auth flow:
Auth core
- Add AuthApiService with typed wrappers for all /api/auth/* endpoints
- Unwrap API envelope { success, code, data } in login() and refresh()
- Update TokenPair to match real response shape (accessTokenExpiresAtUtc,
refreshTokenExpiresAtUtc, tokenType, user embedded in response)
- AuthService: access token in memory signal, refresh token in
localStorage;
setSession() now stores the user directly from the token response — no
extra /api/me call needed after login or refresh
- Add token interceptor: attaches Authorization: Bearer header to internal
requests, skips /api/auth/* paths
Pages & routes
- Login: ReactiveFormsModule, POST /api/auth/login, returnUrl redirect
- Register: 8 BRD fields with full validators (minLength 12, maxLength 20,
complexity pattern, cross-field passwordsMatch); template extracted to
HTML
- Forgot password: single email field, POST /api/auth/forgot-password
- Reset password: reads email + token from query params, same password
rules
- Add forgot-password and reset-password routes with guestGuard
i18n
- Add account.forgotPassword, account.resetPassword, account.logout keys
- Update account.register with 8 BRD field labels and password hint
- Arabic translations for all new keys
Config & styling
- MAT_FORM_FIELD_DEFAULT_OPTIONS: appearance=outline applied globally
- reRenderOnLangChange: true — fixes language switch requiring page reload
- Global outline: none on .mat-mdc-input-element:focus (double border fix)
- proxy.conf.json: point /api to live external API
Contracts
- Refresh openapi.external.json and openapi.internal.json from live Swagger
Tech debt
- Add BRD/tech-debt/SEC001: HttpOnly cookie migration for refresh token
cycle
Remove all dev cookie shims and the
angular-auth-oidc-client setup;
replace with the same JWT pattern used by web-portal.
Auth core
- Add AuthApiService: login(), refresh(), logout() with
API envelope unwrap
- Rewrite AuthService: access token in memory signal,
refresh token in
localStorage (cce_admin_rt); setSession() derives
permissions from roles
using the existing role→permission map (cce-admin,
cce-editor, cce-reviewer)
- Add tokenInterceptor: Bearer token on internal
requests, skips auth paths
- Add authGuard: redirects to /login with returnUrl if
not authenticated
- Delete DevAuthService, devAuthGuard, AuthCallbackPage,
authInterceptor
Login page
- Add login.page.ts + login.page.html + login.page.scss
- Full-screen dark-blue card layout, email + password
with visibility toggle
- Handles 401 (invalid credentials) and 403 (no admin
access) distinctly
Routing & layout
- app.component.html is now a plain router-outlet (login
renders without shell)
- Shell is a layout parent route; all protected routes
are children
- permissionGuard via canMatch on each child route
Config
- Remove angular-auth-oidc-client and
provideAnimationsAsync
- Add reRenderOnLangChange: true,
MAT_FORM_FIELD_DEFAULT_OPTIONS
- Swap authInterceptor → tokenInterceptor
Other
- AuthToolbarComponent uses real AuthService (user name +
logout icon)
- ProfilePage reads from AuthService.currentUser, removes
OidcSecurityService
- i18n: add account.login.errorForbidden,
account.profile.roles (en + ar)
refactor(event-detail): wrap calendar button content in ng-container refactor(resource-detail): wrap download button content in ng-container fix(world-map): ensure totalCities is a function call in stats display chore(styles): update styles to use @use for fancy animations
…overage - Replace all [dir="rtl"] with :host-context([dir="rtl"]) across 14 component SCSS files so Angular emulated encapsulation applies them correctly - Add missing RTL arrow flips to news-detail, resource-detail, assistant, and home page CTA arrows (scaleX(-1) + reversed hover translateX) - Fix mega-menu dropdown z-index and RTL anchor direction - Add missing outlined text-field error/disabled/caret CSS tokens to _fancy.scss - Move focus glow from box-shadow to outline to prevent overlap with floating label Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eb-portal - Split CceRole into CceAdminRole (SuperAdmin, Admin, ContentManager, StateRepresentative) and CcePortalRole (Expert, User) in shared contracts - Add CcePermission const with all 15 permission strings - Admin-cms: derive typed permissions from roles in AuthService; add hasRole/hasAnyRole/hasPermission/hasAnyPermission helpers - Admin-cms: add IfRoleDirective and roleGuard using CceAdminRole - Admin-cms: upgrade PermissionDirective and permissionGuard to CcePermission type - Admin-cms: replace raw permission strings in app.routes and nav-config - Admin-cms: harden authGuard to reject non-admin JWT holders - Web-portal: add hasRole/hasAnyRole/roles signal for CcePortalRole - Web-portal: add IfRoleDirective and roleGuard using CcePortalRole - Both apps: type AuthUser.roles as (CceAdminRole | CcePortalRole)[] Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ResourceCenterUpdate, NewsPublish, NewsDelete to CcePermission - Add 7 granular Report.* permissions to CcePermission - Update ALL_PERMISSIONS and ContentManager/StateRepresentative role maps - Type ReportConfig.permission as CcePermission (was string) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…revent label overlap box-shadow renders inside the stacking context and overlaps the floating label; outline renders outside the element boundary and does not. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Two-panel login/forgot-password/reset-password layout in admin-cms with locale switcher and RTL prefix-icon label fix in both apps - auth interceptor in both apps: 401 triggers silent refresh + one retry before redirecting to login; concurrent refresh calls deduped - Proactive access-token refresh scheduled 60 s before expiry in both AuthServices - returnUrl open-redirect fix: only same-origin paths accepted on login - bffCredentialsInterceptor wired in web-portal; withCredentials added per request in admin-cms auth interceptor - Global 404 toast in serverErrorInterceptor; SUPPRESS_ERROR_TOAST context token lets components (forgot-password) suppress it for inline handling - guest guard added to admin-cms auth routes; web-portal guest guard redirect corrected to /me/profile - Email verification page in web-portal (/verify-email?token=) - Resend verification email button on register success state - Specs added for auth/guest guards in both apps and updated for auth interceptors and serverErrorInterceptor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ixes
- Add toApiFieldErrors() to error-formatter to map the CCE array-based
400 body { errors: [{field, code, message}] } to a per-field message
map, normalising PascalCase field names to camelCase
- Register form sets control.setErrors({ serverError }) per field on 400;
clearServerError() strips only that key on user input so Angular
validator errors are preserved
- Remove email verification flow from registration: success message
updated to "You can now sign in", resend button and related state removed
- Fix vertically off-center text on anchor mat-flat-button in auth pages:
replace display:block with display:flex + align-items:center on
.cce-auth__submit (affects forgot-password and reset-password)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix NG8011: split @else block in reports button into two @if blocks so mat-icon is the sole root node and projects correctly into MatButton slot - Raise initial bundle budget warning from 500 kB to 1 MB (actual ~930 kB is expected for a full Angular Material admin CMS) - Raise component style budget warning from 4 kB to 8 kB to cover community-moderation, translations and settings pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove role check from authGuard (backend returns empty roles; temporary until API is fixed) - Simplify guestGuard to redirect any authenticated user away from login pages - Bind [dir] on mat-sidenav-container so Angular Material CDK tracks RTL reactively; position="start" moves sidenav to the right in Arabic - Replace one-time translate() call with toSignal(selectTranslate()) so shell title updates on language switch - Replace hardcoded "Admin Console" and "Carbon Circular Economy" in sidenav with transloco keys - Add nav.adminConsole, nav.footerTagline, account.logout.button keys to en.json and ar.json - Translate common.locale.en as "الإنجليزية" in Arabic so language switcher is readable in RTL mode - Delete orphaned auth-toolbar.component.html (component uses inline template) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Temporary fallback in setSession: if the API returns roles:[] the user gets ALL_PERMISSIONS so the sidenav is fully visible. Once the backend includes roles in the login/refresh response the derivePermissions mapping will take over automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wire search & role filter on users list (GET /api/admin/users params)
- Add create user dialog (US041): firstName, lastName, email, password,
phone, country, role with full validation and API error mapping
- Add delete user with confirmation dialog (US042)
- Row click navigates to user detail (US040)
- Scope apiEnvelopeInterceptor to /api/admin/ only — fixes login double-unwrap
- Fix toFeatureError for CCE 400 array format { errors: [{field,message}] }
so field-level validation errors surface on the correct form controls
- Centralise password rules in PASSWORD_STRENGTH_VALIDATORS (ui-kit) shared
by register page and admin create-user dialog
- Add RoleLabelPipe: maps cce-* role values to i18n keys; used in detail
chips, role-assign select, and users table cell
- Define KNOWN_ROLE_OPTIONS and ASSIGNABLE_ROLES with cce-* enum values;
ASSIGNABLE_ROLES scoped to Admin, Content Manager, Country Representative
- Translate mat-paginator via TranslocoPaginatorIntl provided in PagedTableComponent
- Add paginator.* and users.* i18n keys to en/ar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rebuild user detail page: avatar with initials fallback, active/inactive status chip, country name resolved via API (locale-aware), translated knowledge level and locale preference, roles as translated chips - Remove Assign Roles from detail page (no longer needed) - Fix RTL back arrow: arrow_forward in Arabic, arrow_back in English - Replace header username label with account_circle icon button that opens a mat-menu dropdown showing full name, email, and sign out action - Scope UserDelete permission to SuperAdmin only; Admin keeps ALL_PERMISSIONS without delete; add UserDelete to contracts and SUPER_ADMIN_PERMISSIONS - Fix paginator i18n: subscribe to translationLoadSuccess so labels update once the translation file finishes loading (not just on lang change) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After successful registration, the user is redirected to /verify-phone where they enter a 6-digit SMS code before accessing the app. The OTP input is a 6-box component with auto-advance, backspace navigation, and paste support. Handles rate-limit (ERR124) and generic API errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ds setup email POST /api/admin/users no longer accepts a password field. The system sends a password reset link to the new user's email automatically. Removed the password input, show/hide toggle, and strength validators from the create user dialog. Added email hint to inform the admin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added nationality/dial-code dropdown to the registration form, populated from GET /api/country-codes. Fixed envelope unwrapping and corrected the CountryCode type to match the actual API response shape (name.ar/en). Added common.search and common.noResults i18n keys. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ode picker (Sprint 06) - Profile page: add firstName, lastName, jobTitle, organizationName, countryCodeId fields to view and edit mode; fix getProfile/updateProfile envelope unwrap (res.data) - Media upload: new MediaApiService (POST /api/media) — replaces plain avatarUrl text input with file picker + preview + upload flow - Country codes cache: memoize isActive=true result in CountriesApiService for session lifetime - Registration: split phone field into dial-code DDL + local number; concatenate on submit; countryCodeId renamed to nationality - i18n: add profile field labels (en/ar), avatar upload keys, phone code label Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…become-an-expert CTA - Expert request: add CV upload via POST /api/assets (MediaApiService.uploadAsset); cvAssetFileId included in submit payload; file picker with progress + filename feedback - Profile: nationality DDL shows country name only (no dial code); remove countryId/listCountries — use countryCodeId from /api/country-codes same as registration - Profile: add Become an Expert CTA card (links to /me/expert-request); loads expert status in parallel on init; shows pending/approved badge when applicable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Display Pending (amber), Approved (green), and Rejected (red) badges with contextual icons. Show reapply CTA when status is Rejected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RTL icon fix - New /experts/:id detail page showing full submission (bio AR/EN, tags, CV) plus decision info (rejection reasons or approval date) and Approve/Reject actions for Pending requests - Bio preview added to Approve and Reject dialogs so admins read the submission before deciding - Shared _expert-dialog.scss partial; individual SCSS files per dialog - Expert requests list simplified to a single chevron "View" link per row - Directional icons (chevron_right, arrow_back) flip correctly in RTL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix envelope unwrap in getExpertStatus() and submitExpertRequest() so expert status badge renders correctly on the profile page - Add phoneNumber field to UserProfile type and display it in view mode - Expert status shown as a labeled field row (Pending/Approved/Rejected badge or "Not applied") consistent with other profile fields - Default expert requests list filter changed to "All" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New admin screens under /publishing for managing all sprint-02 content:
- About Settings: description (AR/EN), how-to-use video URL, glossary terms
CRUD, knowledge partners CRUD. Native HTML inputs with branded styling
and bilingual side-by-side layout.
- Homepage Settings: video URL, objective AR/EN, CCE concepts AR/EN,
participating countries multi-select.
- Policies Settings: multi-section editor with title/content/type/order
and reorder action.
- Homepage admin overview page.
Publishing API service hardened for the real backend shape:
- GET responses unwrap envelope and map nested {ar,en} -> flat fields
(description, term, definition, name, objective).
- POST/PUT bodies send flat fields (descriptionAr/En etc.) since writes
expect a different shape than reads.
- participatingCountries[].id extracted into participatingCountryIds[]
so the multi-select pre-binds on load.
i18n: aboutSettings, homepageSettings, policiesSettings, footer keys
added in AR + EN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
added 27 commits
June 22, 2026 15:42
… & voting Create-post form: - Type/topic as dropdowns (Figma); attachments drop-zone (optional) that uploads to /api/assets on pick with uploaded-file rows; poll builder for poll-type posts (options, deadline, allow-multiple/anonymous/show-results); full CreatePostPayload (attachments + poll) on submit. Post detail: - Render and vote on polls for type=Poll posts: option list (single/multi), results bars with percentages, the user's choice, total votes / closing / anonymous meta. Single-choice votes on click; multi-choice stages + submits. Refetches getPollResults after voting. Reads an inline `poll` on PublicPost (dormant until the API includes it).
…llowing dialog - Feed sort: add MostCommented (3) across feed, my-profile and user-profile; expose full PostFeedSort enum (Hot/Newest/TopVoted/MostCommented) and fix the third option that was mislabeled onto Hot(0). - Followed-posts tab now filters via isWatchlisted=true on the feed API. - Profile pages: render localized expert bio (expertBioAr/En), country (countryNameAr/En) and joining date (joinedDate) on both my-profile and user-profile. - Add "Members I follow" dialog (getMyFollows + getCommunityUser) opened from the following count on my-profile. - Restyle my-profile tabs: distribute evenly (space-around) with primary text and secondary green underline on the active tab.
…type
Web-portal:
- Community sidebar topics now use GET /api/community/topics for real post
counts, the "All" total, and per-topic follow status (isFollowed) with a
bookmark toggle that persists via /api/me/follows.
Admin moderation:
- Detail dialog reads /api/admin/community/posts/{id} and /{id}/replies
(auto-unwrapped admin envelope).
- List filter is now by post type (Informational/Question/Poll) like the
web-portal; table shows the type chip instead of status and the title
instead of content; detail dialog header leads with the type chip.
…thod
- post-detail + topic-detail: add a flush() helper that drains the floating
ngOnInit→load() chain (whenStable resolved before signals were set).
- sign-in-cta, rate-post-control, topic-detail: replace the invalid
TranslocoModule.forRoot() with TranslocoTestingModule.forRoot({...}).
- community-api: add ratePost(postId, stars) → POST /posts/{id}/rate { stars }
(the rate-post control and its spec referenced a method missing from the
service); update topic-detail compose-dialog payload assertion.
…tion UI Add @frontend/real-time lib and wire real-time updates across both apps. Core (@frontend/real-time): - RealtimeHubService: auth-driven connect/stop, withAutomaticReconnect + subscription-registry replay, reconnect on token rotation, typed event streams (rxjs), case-insensitive payload normalizer, connectionState signal. - provideRealtime(configFactory) wired into web-portal and admin-cms. - Hub uses the relative /hubs/notifications path (same-origin, proxied like /api); added /hubs ws proxy entries (both apps -> External API). - @microsoft/signalr dependency. web-portal: - Live bell + drawer via ReceiveNotification, replacing the 60s poll (kept a 5-min reconciliation fallback). - Community: post-detail live NewReply/VoteChanged/PollResultsChanged/ PostModerated; topic-detail + feed "new posts" pill (NewPost) and moderation tombstoning. - Redesigned payload-driven notification toast (branded card via openFromComponent): localized subject/body, View action, live dot, depleting bar; locale-based position (AR top-left, EN top-right); toasts everywhere (also on NewPost); dedupe guard to prevent flashing; 10s duration. - Dev-only connection indicator in the header + event logger (config.debug). - Fix: unwrap enveloped /api/me/notifications responses (drawer/badge/toast). Also includes carried-over community poll voting fix and the admin poll-results detail dialog. i18n keys added (ar + en).
…ilter row - Seed sidebar followed-topic flags from /api/me/follows (the topics endpoint doesn't return isFollowed), so bookmarks reflect actual follow state. - Wrap the feed filter bar so the type dropdown no longer overflows the page or clips the sort chips on narrow widths.
…, card & admin fixes
Content detail audit (web-portal) — surface all admin-form fields, localized:
- Resource detail: add Countries fact row (countryNames), localized.
- Event detail: add topic chip + tags row (topicNameAr/En, tags).
- News detail: tags chips now show the topic + real `tags` (was faking from topic).
- News card: drop dead publisher() block (read non-existent authorName/publishedBy).
- i18n: resources.detail.factCountries (ar + en).
Share dialog — same UX everywhere (posts, country, resource, event, news):
- Generalize SharePostDialogComponent data from { postId, postTitle } to { url, title }.
- Update community callers (post-detail, post-summary) to pass a URL.
- country/resource/event/news detail "Share" CTAs now open the same dialog
(removed bespoke navigator.share / social-link rows / copyLink). Reuse share.button.
Card hover border fix (resource/news/event cards):
- Move content-visibility + contain-intrinsic-size from :host onto the card element
so the card's own border (notably the bottom edge) isn't clipped by the host's
paint containment. Perf optimization retained.
Admin countries endpoint:
- country-api.service listCountries() now calls /api/admin/countries (was /api/countries)
and relies on envelope auto-unwrap (dropped manual res.data), matching the other
methods. Flows to all admin country dropdowns.
- Unwrap RealtimeEnvelope { eventId, occurredOn, payload }; dedup on eventId
(ring buffer) so reconnect replays don't double-fire; expose lastEventTime
cursor for reconnect catch-up.
- admin-cms connects the Internal hub (admin JWT): /hubs proxy -> Internal API
(External hub rejects admin JWTs).
- Refine payload types: structured ReceiveNotification (+metaData.postId),
NewReply (+body/author), VoteChanged (+downvoteCount), PollResultsChanged
(+totalVotes/options), NewPost (+title).
- Toast built directly from the ReceiveNotification payload (no list fetch) +
deep-link via metaData.postId; NewPost toast uses payload.title; post-detail
poll updates in-place from PollResultsChanged (no refetch).
community-moderation list subscribes to ContentModerated (moderation room, auto-joined via the Internal hub). Another moderator's action toasts + refreshes the list; the moderator's own action is skipped (already handled locally). i18n key communityModeration.toastModerated (ar + en).
Mock @microsoft/signalr and cover the core hub behaviours: connect on auth / disconnect on logout, RealtimeEnvelope unwrap + eventId dedup + occurredOn cursor, Subscribe invoke + replay after reconnect, and idle without config.
Hub exposes reconnected$ (fires on auto-reconnect + token-rotation reconnect,
not initial connect). post-detail catches up via
GET /api/community/posts/{id}/activity?since={lastEventTime} and applies the
delta: post vote count (minus optimistic), poll results in-place, and a replies
refetch only when newReplies/replyCount changed. Adds PostActivity type +
CommunityApiService.getPostActivity.
- Presence: subscribe PresenceChanged -> 'N viewing' chip (shown when >1). - Typing: subscribe TypingChanged -> animated typing indicator under the composer (set of user ids with 6s auto-expiry, cleared on destroy). - Reply composer drives StartTyping/StopTyping: throttled re-emit on input, auto-stop on idle/blur/submit/destroy. - i18n community.presence.viewing + community.typing.one/many (ar + en).
- Attachment kind was reversed: now 0 = Media (image/video), 1 = Document
(per backend contract). Post detail renders Media (kind 0) inline as an
image; Documents stay as a download card.
- Download now uses MediaApiService.downloadAsset(assetFileId) (GET
/api/assets/{id}/download -> blob -> save) in both the post detail card and
the posts-list card (the list button was previously a no-op); direct url
links failed for cross-origin / auth-gated assets.
Anonymous users who click download on the post-detail card or the posts-list card now get the sign-in/register dialog (CommunityAuthPromptService) instead of the download — same gate as vote/reply. Adds community.authDialog.messageDownload (ar + en).
The detail API returns only attachmentIds (no full attachments array), so the page renders that fallback branch — which hardcoded a disabled download span. Make it an active, auth-gated download button (the id is the asset id) via MediaApiService.downloadAsset.
The author name in cce-post-summary now links to /community/users/{id} when an
author id is present (anonymous stays plain text). Covers the feed, my-profile
followed posts, topic detail, and user-profile pages (shared card).
Avatar now routes to /community/users/{id} (hover ring); marked tabindex=-1 /
aria-hidden since the author-name link already provides the accessible nav.
Bio paragraph used text-align:end (left in RTL, right in LTR — wrong for both). Switch to text-align:start so it follows reading direction: right in Arabic, left in English. Applies to user + my profile pages.
Implement the two Figma empty-state designs on community-my-profile: - My posts (لم تنشر شيئًا بعد): file-plus icon, title + desc, solid primary CTA 'انشاء منشور جديد' (square-plus) -> openCreatePost(). - Followed posts (لا توجد منشورات تتابعها): file-plus icon, title + desc, outlined CTA 'تصفّح المجتمع' -> /community. Shared card (neutrals-50 bg, neutrals-200 border, 64px icon, 36px title, 20px body, full-width 64px button). i18n updated (emptyDesc, noPosts) + added noPostsDesc, browseCommunity (ar + en).
…nowledge Community' - Feed type filter: 'كل الأنواع' -> 'كل الفئات' (All types -> All categories). - Header nav link: 'المجتمع' -> 'مجتمع المعرفة' (Community -> Knowledge Community). Create-post type label already reads 'فئة المنشور' / 'Post category' (unchanged).
- cmp__posts-list gap 0 -> 16px to match the feed list (applies to both the 'my posts' and 'followed posts' tabs). - Remove the white background from the profile tab bar (cmp__tabs).
Add margin-bottom:24px to cmp__filter-bar so the first post isn't flush against the filters (matches the search box's spacing).
….ToHttpResult()
Backend migrated 48 endpoints to return flat responses; remove the
{ data: T } wrapper and .data access from account-api and pages-api
for the affected /api/me, /api/me/*, /api/users/expert-request, and
/api/policies endpoints. Also removes color/background buttons from
the shared Quill toolbar.
…enums
All CCE endpoints return { success, code, message, data: T }. The previous
commit incorrectly assumed the migration removed the envelope — verified
against the live API. This commit:
- Reverts the wrongly-removed .data unwrapping in account-api and pages-api
- Adds missing envelope unwrapping to knowledge-maps, interactive-city,
kapsarc, and pages/slug (these services were typed as flat all along)
- Normalises camelCase enum values from the wire ("approved", "sector",
"parentOf") to PascalCase at every service boundary so all downstream
comparisons, i18n keys, and CSS selectors stay unchanged
- Adds lastModifiedOn?: string | null to SavedScenario (per spec)
- Fixes KapsarcSnapshot.classification to string | null (nullable in spec)
- Makes KnowledgeMapNode optional fields (descriptionAr/En, iconUrl) that
the API does not currently include in responses
- Remove MentionedUserIds from CreateReplyPayload; mentions now embedded in content as @[userId:displayName] tags per updated backend contract - Textarea shows @DisplayName (clean UX); _mentionMap rebuilds tags at submit via encodeMentions() before sending to server - Replace client-side participant filter with server-side autocomplete GET /api/community/communities/{id}/mentionable-users (≥2 chars, 250ms debounce, awaits ensureLoaded() for communityId); shows isFollowed/isMember badges in picker dropdown - reply.component: parse new @[uuid:name] format directly; fall back to old @name + mentionedUsers resolution for pre-migration replies - Add My Mentions 4th tab to /community/me: GET /api/me/mentions paged, lazy-loaded, click navigates to post with reply fragment anchor - New types: MentionableUser, MentionItem; new API methods: getMentionableUsers(), getMyMentions() - i18n: myMentions, mentions.empty, mentions.mentionedBy, mention.followedBadge, mention.memberBadge (ar + en)
…polish - post-detail: compute threadParticipants (deduped, excludes self); replace flat orderedReplies with replyTree grouping children under their parent; add replyingTo signal driving the compose-form chip - reply: render contentParts() as inline @mention chips (linked when userId resolved, plain otherwise); add Reply button hidden for child replies; render children recursively via cce-reply--child modifier - reply scss: mention chip styles (brand color, bg, hover cursor); children indent + connector line - compose-reply-form scss: mention picker dropdown, badge, loading, empty state styles - user-profile filter bar: swap DOM order — type+topic dropdowns at inline-start, sort chips at inline-end (correct RTL reading order)
|
|
||
| private setState(state: RealtimeConnectionState): void { | ||
| this._state.set(state); | ||
| if (this.config?.debug) console.debug('[realtime] state →', state); |
| payload = env.payload; | ||
| } | ||
| const normalized = normalizePayload(payload); | ||
| if (this.config?.debug) console.debug(`[realtime] ◀ ${event}`, normalized); |
| @@ -97,7 +128,7 @@ | |||
| if (q) { | |||
| const title = this.locale() === 'ar' ? a.titleAr : a.titleEn; | |||
| const content = this.locale() === 'ar' ? a.contentAr : a.contentEn; | |||
| const hay = [title, content.replace(/<[^>]*>/g, ''), a.slug].join(' ').toLowerCase(); | |||
| const hay = [title, content.replace(/<[^>]*>/g, '')].join(' ').toLowerCase(); | |||
|
|
||
| excerpt(article: NewsArticle): string { | ||
| const raw = this.isAr() ? article.contentAr : article.contentEn; | ||
| const stripped = (raw ?? '').replace(/<[^>]*>/g, '').trim(); |
| return firstLine.length > 100 ? firstLine.slice(0, 100) + '…' : firstLine; | ||
| const t = this.post().title; | ||
| if (t) return t.length > 130 ? t.slice(0, 130) + '…' : t; | ||
| const stripped = (this.post().content ?? '').replace(/<[^>]*>/g, '').trim(); |
| } | ||
| return ''; | ||
| if (this.post().title) { | ||
| const stripped = (this.post().content ?? '').replace(/<[^>]*>/g, '').trim(); |
| } | ||
| return rest.length > 200 ? rest.slice(0, 200) + '…' : rest; | ||
| const stripped = (this.post().content ?? '').replace(/<[^>]*>/g, '').trim(); |
| }); | ||
|
|
||
| function run() { | ||
| return TestBed.runInInjectionContext(() => guestGuard({} as never, {} as never)); |
| <button type="button" mat-icon-button (click)="startEdit(row)" [disabled]="editingId() !== null"> | ||
| <mat-icon>edit</mat-icon> | ||
| </button> | ||
| <button type="button" mat-icon-button color="warn" (click)="delete(row)" [disabled]="editingId() !== null"> |
| mat-icon-button | ||
| color="warn" | ||
| *ccePermission="'Resource.Center.Delete'" | ||
| (click)="delete(row)" |
| }); | ||
|
|
||
| function run() { | ||
| return TestBed.runInInjectionContext(() => guestGuard({} as never, {} as never)); |
| import type { Country, CountryProfile, KapsarcSnapshot } from './country.types'; | ||
| import { CountryCardComponent } from './country-card.component'; | ||
| import { CountriesWorldMapComponent } from './countries-world-map.component'; | ||
| import { getMockCardStats, getMockKapsarc } from './testing/countries-mock'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test plan
dotnet test backend/CCE.slngreenpnpm nx run-many -t lint,testgreen./scripts/check-contracts-clean.shgreenpnpm nx run-many -t e2e(web-portal-e2e + admin-cms-e2e) greenSecurity checklist
BRD traceability
Screenshots / output (optional)