feat(organizations): soft suggestedJoin onboarding banner + recovery-screen copy#4176
Conversation
Captures suggestedJoin:{orgId,orgName}|null from signup response,
persists client-side via localStorage (mirrors existing auth prefs),
and adds setSuggestedJoin / dismissSuggestedJoin /
clearSuggestedJoinIfMember actions. signout clears state + storage.
Backward-compatible: field absent = graceful no-op.
14 new TDD tests (RED → GREEN), 0 regressions (69/69 store, 165/165 auth).
… CTA (D2) - New component `organizations.suggestedJoinBanner.component.vue`: renders a persistent snackbar when `authStore.suggestedJoin` is truthy; no-op when null (globally-mountable safe). CTA calls `organizationsStore.createJoinRequest`. - Benign terminal rejections (already a member, pending request exists, 404 org not found) → neutral info toast + `dismissSuggestedJoin` — never a red toast. - Genuine unexpected errors (500 / network) → error toast, dismiss NOT called so the user can retry. - Dismiss control → `authStore.dismissSuggestedJoin()` (persisted, no nag on reload). - Mounted app-level in `app.vue` alongside other global banners. - 11 unit tests: no-op when null, render with org name, CTA success, benign matrix (3 cases parametrized), genuine error x2, dismiss, loading state, guard. 106/106 organizations tests + 168/168 auth tests green.
… cross-org-cap classification
replace onboarding heading/intro ("Organization Required", "You need
to belong to an organization") with recovery tone — "No workspace
found" + "You are not a member of any workspace. Create one or ask
for an invitation." all interactive elements unchanged.
tests: assert new copy present, old onboarding copy absent, and
functional surface intact (create-org, sign-out, request-to-join,
check-status).
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (7)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4176 +/- ##
=======================================
Coverage 99.55% 99.55%
=======================================
Files 31 31
Lines 1114 1136 +22
Branches 321 328 +7
=======================================
+ Hits 1109 1131 +22
Misses 5 5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a new “suggested workspace join” onboarding affordance by persisting suggestedJoin from the signup response in the auth store and surfacing it as a dismissible app-level banner, plus updates the “organization required” screen copy to a recovery framing.
Changes:
- Persist
suggestedJoininauth.store(init from localStorage, set/dismiss/clear helpers, clear on signout) and capture it from the signup response. - Introduce
organizationsSuggestedJoinBannerand comprehensive unit tests covering success, benign rejection, and error flows. - Update
organizations.required.viewcopy and extend unit tests for the new recovery wording.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/modules/organizations/views/organizations.required.view.vue | Updates heading/intro text to recovery framing (“No workspace found”). |
| src/modules/organizations/tests/organizations.required.view.unit.tests.js | Adds assertions for the updated recovery copy and confirms key UI actions remain present. |
| src/modules/organizations/components/organizations.suggestedJoinBanner.component.vue | New dismissible banner + CTA to request access, with benign-error handling and feedback snackbar. |
| src/modules/organizations/tests/organizations.suggestedJoinBanner.component.unit.tests.js | New unit tests for banner rendering, request flow, benign errors, and double-submit guard. |
| src/modules/auth/stores/auth.store.js | Adds suggestedJoin state, persistence/init, and lifecycle actions; sets from signup response and clears on signout. |
| src/modules/auth/tests/auth.store.unit.tests.js | Adds unit tests for suggestedJoin state lifecycle, persistence, and storage corruption handling. |
| src/modules/app/app.vue | Mounts the new organizations suggested-join banner at the app level. |
Comments suppressed due to low confidence (2)
src/modules/auth/stores/auth.store.js:172
setSuggestedJoin()only checks for a generic object, so arrays or objects missingorgId/orgNamewill be persisted to localStorage and later treated as a valid suggested join. Validate thatpayload.orgIdandpayload.orgNameare non-empty strings (and ignore + avoid persisting otherwise) to keep the store contract consistent.
setSuggestedJoin(payload) {
if (!payload || typeof payload !== 'object') return;
this.suggestedJoin = payload;
localStorage.setItem(`${config.cookie.prefix}SuggestedJoin`, JSON.stringify(payload));
},
src/modules/organizations/components/organizations.suggestedJoinBanner.component.vue:8
- The banner uses
v-model="visible"butvisibleis a plain data field with no setter side-effect. If the snackbar closes via internal behavior (e.g., ESC / programmatic model update),visiblebecomes false whilesuggestedJoinstays persisted, leaving the banner permanently hidden until storage is cleared. Mirrororganizations.adminPendingBanner's pattern: implementvisibleas a computed with a setter that callsdismiss()when set to false (or watchvisibleand dismiss).
<v-snackbar
v-model="visible"
location="top right"
color="info"
:timeout="-1"
multi-line
Two Copilot-surfaced bugs fixed: 1. auth.store setSuggestedJoin + initFromStorage: tighten payload guard to require non-empty string orgId + orgName; reject arrays, objects missing fields, and non-string values. Invalid-but-parseable localStorage values now also purge the key (not only corrupt JSON). 4 new tests cover arrays, missing fields, and non-string types. 2. organizations.suggestedJoinBanner: move feedback v-snackbar outside the v-if="suggestedJoin" guard. Previously dismissSuggestedJoin() (called right after showFeedback) set suggestedJoin null, collapsing the outer template v-if before the feedback snackbar could render. Now the primary banner uses standalone v-if; feedback snackbar is a sibling at root level (Vue 3 multi-root). Test updated accordingly. lint: 0 errors — full suite: 110 files / 1847 tests, 0 failures.
# [2.1.0](v2.0.0...v2.1.0) (2026-06-01) ### Bug Fixes * **billing:** 4 pricing page bugs (signed-in features, hero bg, truncation, guest CTA) ([#4123](#4123)) ([bd399bd](bd399bd)) * **billing:** add aria-expanded + touch support to BillingComputeGauge components ([#4181](#4181)) ([7e7fa97](7e7fa97)) * **billing:** billing UX hardening — 5 reliability fixes ([#4079](#4079)) ([dd62b3d](dd62b3d)), closes [#4078](#4078) * **billing:** canonical FA6 icons + meterError lifecycle + reject URL credentials ([#4082](#4082)) ([be16045](be16045)), closes [#4081](#4081) * **billing:** clear intentId on Stripe cancel-redirect ([#4085](#4085)) ([11be9ab](11be9ab)) * **billing:** drop empty overage aria-live div + add fr overDetail key ([#4142](#4142)) ([10e5d81](10e5d81)) * **billing:** i18n migration + Intl.NumberFormat USD (audit Codex P1) ([#4069](#4069)) ([3d3a4f6](3d3a4f6)) * **billing:** mega vue hardening v3 — UX + design + a11y + V4 ([#4063](#4063)) ([802cbc4](802cbc4)) * **billing:** send {} body on portal POST to satisfy Zod PortalRequest schema ([#4137](#4137)) ([bf6b9c8](bf6b9c8)) * **billing:** send intentId UUID to close extras double-charge window ([#4084](#4084)) ([3e27c4c](3e27c4c)) * **billing:** subscription state safety + webhook lag polling (audit Codex P1) ([#4068](#4068)) ([328700b](328700b)) * **billing:** surface meterError + remove dead 409 dialog from subscriptions ([#4081](#4081)) ([61f5d53](61f5d53)) * **billing:** V5 polish — F5 polling recovery + visibility refetch + i18n residual + locale null-guard ([#4070](#4070)) ([4908adb](4908adb)) * **billing:** V6 polish — sessionStorage guard + locale BCP47 + i18n plural + NaN guard ([#4076](#4076)) ([2ad018c](2ad018c)) * **build:** drop ARG defaults for analytics_* + filter empty env vars ([#4112](#4112)) ([afd336e](afd336e)), closes [Vue#4110](https://github.com/Vue/issues/4110) [#4110](#4110) * **build:** restore Layer 5 empty-string env override (revert [#4112](#4112) part 2) ([c9aa9e6](c9aa9e6)), closes [Vue#4110](https://github.com/Vue/issues/4110) [comes-io/trawl_vue#880](https://github.com/comes-io/trawl_vue/issues/880) [Vue#4110](https://github.com/Vue/issues/4110) * **core:** pin CorePageHeader to content size in flex-column contexts ([#4210](#4210)) ([e182512](e182512)), closes [#4202](#4202) * **layout:** route /pricing outside app shell — no drawer offset on signed-in ([#4124](#4124)) ([d862fbb](d862fbb)) * **legal:** cookie banner UA-detection + appName fallback to app.title ([#4113](#4113)) ([04cc70a](04cc70a)), closes [trawl_vue#876](https://github.com/trawl_vue/issues/876) [#4109](#4109) * **legal:** cookie consent + footer polish (5 QA bugs) ([#4098](#4098)) ([a9bf0a3](a9bf0a3)) * **legal:** gate cookie banner on isMounted to prevent prerender hydration double-render ([#4109](#4109)) ([3198e48](3198e48)), closes [#app](https://github.com/pierreb-devkit/Vue/issues/app) * **router:** redirect authenticated users to config.sign.route instead of hardcoded '/' ([#4088](#4088)) ([ca9094e](ca9094e)), closes [#4083](#4083) * **tasks:** propagate store errors so views gate navigation on success ([#4221](#4221)) ([b029d39](b029d39)), closes [#4218](#4218) * **ui:** chrome convergence — restore section title + surface backgrounds + homogeneous gutter ([#4188](#4188)) ([1e5fdf1](1e5fdf1)) * **users:** re-apply route tab when serverConfig.billing arrives async ([#4138](#4138)) ([87cc7b2](87cc7b2)) * **users:** refetch billing subscription on auth state change ([#4125](#4125)) ([5a3425a](5a3425a)) ### Features * **admin:** invitations management tab ([#4217](#4217)) ([61ad86c](61ad86c)), closes [invitedBy/#actions](https://github.com/pierreb-devkit/Vue/issues/actions) * **analytics:** add identify and reset helpers, wire auth store ([#4104](#4104)) ([c4d8b87](c4d8b87)) * **auth:** invite-gated signup UI ([#4212](#4212)) ([d17a841](d17a841)) * **billing:** add 'Manage subscription' footer link in meterDrawer ([#4056](#4056)) ([#4057](#4057)) ([7d494af](7d494af)) * **billing:** align packs.component on BillingCardComponent (V4 unified schema) ([#4147](#4147)) ([7f5dd41](7f5dd41)) * **billing:** combined pool gauge + linear breakdown bars + alerts cleanup ([f5034b1](f5034b1)) * **billing:** config-driven static-content resolver (no file replace) ([e95d944](e95d944)) * **billing:** expose netRemainingRaw + overage in useMeter for negative quota display ([#4061](#4061)) ([111e64b](111e64b)), closes [#4060](#4060) * **billing:** meter gauges display % primary, compute units in overflow tooltip ([#4139](#4139)) ([50498ba](50498ba)) * **billing:** meter UX refonte — drop drawer + subscriptions tab ([#4059](#4059)) ([891b5ae](891b5ae)), closes [#subscriptions](https://github.com/pierreb-devkit/Vue/issues/subscriptions) [#1](#1) [#2](#2) [#3](#3) [#4](#4) * **billing:** post-grant upgrade prompt variant for depleted signupGrant ([#4128](#4128)) ([0ae8558](0ae8558)) * **billing:** pricing page redesign — multi-mode + auto-savings + sectioned features + FAQ ([#4102](#4102)) ([2c83ab6](2c83ab6)) * **billing:** pricingCard Free CTA — 'Sign up' for guests, route to /signup ([#4106](#4106)) ([cc92ac8](cc92ac8)) * **billing:** redesign Subscriptions view to match user-tabs aesthetic ([#4127](#4127)) ([15fd466](15fd466)) * **billing:** relocate billing under Organization settings ([#4175](#4175)) ([1f36137](1f36137)) * **billing:** sidenav compute gauge above sign-out row (meter mode) ([#4126](#4126)) ([fe1497f](fe1497f)) * **billing:** sidenav compute gauge redesign — button-shape above sign-out row ([#4140](#4140)) ([9d3a090](9d3a090)) * **billing:** sidenav compute gauge revamp — v-progress-circular + v-tooltip ([#4144](#4144)) ([5d60f1b](5d60f1b)), closes [#prepend](https://github.com/pierreb-devkit/Vue/issues/prepend) * **billing:** subscriptions view 2-col layout — drop dup bar, CTA to /pricing#units ([#4141](#4141)) ([b7ef516](b7ef516)), closes [pricing#units](https://github.com/pricing/issues/units) * **billing:** unified BillingCardComponent + annual toggle disabled state ([#4146](#4146)) ([474bb11](474bb11)) * **billing:** unified BillingCardComponent + annual toggle disabled state ([#4149](#4149)) ([d1403ff](d1403ff)) * **billing:** v4 hardening + Phase 3 polish — equivalences chips + UX gaps + a11y ([#4066](#4066)) ([dd40b2a](dd40b2a)) * **billing:** wire netRemainingRaw + overage into devkit components ([#4062](#4062)) ([61d6897](61d6897)), closes [#4061](#4061) * **core:** reusable PageTabs component + Account view refactor ([#4183](#4183)) ([dc2504a](dc2504a)) * **core:** unified logo+title lockup in header and sidenav ([#4086](#4086)) ([ec457db](ec457db)), closes [#4083](#4083) * **feature:** read ERRORS.md in Phase 0 before coding ([#4089](#4089)) ([ddf8f60](ddf8f60)) * **legal:** Add legal module + cookie consent (RGPD) ([#4097](#4097)) ([595e6c4](595e6c4)), closes [#3204116570](https://github.com/pierreb-devkit/Vue/issues/3204116570) [#3204116650](https://github.com/pierreb-devkit/Vue/issues/3204116650) [#1](#1) [#19](#19) [#18](#18) [#13](#13) [#11](#11) [#18](#18) [#3](#3) [#10](#10) [#12](#12) [#14](#14) [#15](#15) [#17](#17) [#20](#20) [#4](#4) [#5](#5) [#6](#6) [#7](#7) [#8](#8) [#9](#9) [#16](#16) [#3](#3) [#16](#16) [#22](#22) * **legal:** liquid glass cookie banner — Vuetify-only with friendlier copy ([#4115](#4115)) ([3d74789](3d74789)), closes [#4114](#4114) * **monitoring:** single-source PostHog Error Tracking (drop Sentry) ([#4118](#4118)) ([82dfca6](82dfca6)) * **organizations:** add Organization tab + rename General route to tab-addressable ([#4184](#4184)) ([fdfbf0b](fdfbf0b)) * **organizations:** soft suggestedJoin onboarding banner + recovery-screen copy ([#4176](#4176)) ([25a3ceb](25a3ceb)) * **seo:** enrich seoInjectPlugin — multi-schemas + rich SoftwareApplication + themeColor ([#4092](#4092)) ([#4111](#4111)) ([7f8bd09](7f8bd09)) * **skill/feature:** add Phase 0.0 issue claim-on-start ([#4117](#4117)) ([a582430](a582430)), closes [pierreb-projects/infra#28](https://github.com/pierreb-projects/infra/issues/28) * **skills/update-stack:** block on undeclared drift vs upstream ([#4228](#4228)) ([ab8ee68](ab8ee68)), closes [#4227](#4227) * **users:** subs full-width + delete account danger zone + halo full-bleed fix ([#4143](#4143)) ([981f073](981f073))
What
PR (d) — the final PR of the Devkit onboarding/billing/UI-decoupling effort (PR(b) #4170, PR(c) #4175, PR(a) #3680 all merged). Consumes the
suggestedJoinsignup-response field PR(a) added.suggestedJoinlifecycle — captured from the signup response (graceful no-op if absent → backward/forward compatible);setSuggestedJoin/dismissSuggestedJoin/clearSuggestedJoinIfMember; cleared on signout; self-healing localStorage persistence (try/catch + payload guard) mirroring the existingCookieExpire/UserRolespattern.organizations.suggestedJoinBanner— app-level dismissible banner (mounted inapp.vueafteradminPendingBanner, mirrors that sibling). Renders only whensuggestedJointruthy (safe global no-op when null). CTA reusesorganizationsStore.createJoinRequest. Benign-rejection handling (verified against the merged Node strings):Already a member of this organization/A pending request already exists/ cross-org one-pending capYou already have a pending request…/ 404 → neutral info toast + dismiss, never red. Genuine 500/network → error toast, NOT dismissed (retry). Double-submit guarded (Vuetify 4:loadingdoesn't block clicks).organizations.required.view.vuerecentred from onboarding to recovery framing ("No workspace found. Create one or ask for an invitation."). Copy-only; all flows (request-join / create-org / check-status / sign-out) byte-identical.Verification
npm run lint: 0 errors. Full suite: 110 files / 1843 tests, 0 failures.Risk
Low — additive auth-store state + an opt-in banner that no-ops when
suggestedJoinis absent (older/other backends unaffected). Recovery copy is behavior-neutral. Downstream propagates via/update-stack.6 commits (
f7ba701b→a7ccc3c2).