From c5def7c7577881c081ac790a0e975355ba72864e Mon Sep 17 00:00:00 2001 From: jmgasper Date: Fri, 9 Oct 2026 16:22:37 +1100 Subject: [PATCH 1/3] PM-6555: Update app drawer menu with new Talent and Staff tools What was broken The app drawer (tool selector) was outdated. Members still saw removed items (Community Home, Practice, a Customer section whose "Meet our Talent" host now redirects to www), and staff only had Work Manager and Connect, which both redirect to the Work app, instead of the new staff tools. Styling and icons did not match the new design. Root cause The drawer config (tool-selector-nav-items.ts) and its ToolMenu component had not been updated for the new Figma design. The old layout also could not express the design's two-column grouping: it relied on a max-height wrap. What was changed - Talent: Earn (Opportunities, Engagement Portal, Copilot Portal, Review App, Wallet) and Compete in the left column; AI, Learn (Topcoder Academy, Articles, Archive) and Connect (Public Forums, Discord) in the right column. Community Home, Practice and the Customer section were removed. - Staff: Delivery & Payments (Work App, Wallet Admin), Sales & Customers (Customer Portal, Salesforce, Sales Pipeline, Procurement) and Insights (Analytics, Reports) in the left column; Community & Content (Support, Payload CMS, Mass Email, Campus) and Team & Platform (Leave Tracker, System Admin) in the right column. - New hosts in hosts.ts follow the .${TC_DOMAIN} pattern of the platform-ui apps. Payload CMS is cms./admin, Archive is archive. and Salesforce is topcoder.my.salesforce.com. - Updated descriptions to the design copy. Descriptions are hidden until a tool is hovered or keyboard focused, one at a time. - Sections now list explicit columns (section -> columns -> groups -> items), and ToolMenu renders them as flex columns that stack on mobile. - Restyled the drawer to the design: 560px card, Nunito Sans, TALENT/STAFF and group titles, 32px tinted icon tiles and 8px item spacing. - Replaced the drawer icons with tinted tiles using Material Symbols Rounded (Apache-2.0) and Tabler (MIT) glyphs, colored by group. Rebuilt sprite.svg, removed the icons of deleted items and documented the format in public/assets/tools/README.md. - ToolSelector hides the drawer trigger when no section matches the user's roles (eg. customer-only users), instead of opening an empty popup. Any added/updated tests - Added tests/tool-selector-nav-items.test.mjs. It loads the real drawer modules through Vite's SSR loader with prod env and a stubbed tcjwt cookie. It checks that staff see the designed Talent and Staff layout, that members only see Talent, and that customer-only users get no items. It also checks that every tool has a description, an icon present in sprite.svg and its expected production URL. - npm run lint (svelte-check), npm test and npm run build pass. Co-Authored-By: Claude Opus 5.5 --- public/assets/tools/README.md | 16 + public/assets/tools/ai-hub.svg | 28 +- public/assets/tools/analytics.svg | 1 + public/assets/tools/archive.svg | 1 + public/assets/tools/articles.svg | 2 +- public/assets/tools/campus.svg | 1 + public/assets/tools/challenges.svg | 2 +- public/assets/tools/connect.svg | 22 - public/assets/tools/copilot-portal.svg | 19 +- public/assets/tools/customer-portal.svg | 1 + public/assets/tools/discord.svg | 2 +- public/assets/tools/forums.svg | 2 +- public/assets/tools/gigs.svg | 2 +- public/assets/tools/leave-tracker.svg | 1 + public/assets/tools/mass-email.svg | 1 + public/assets/tools/mm-tournament.svg | 23 +- public/assets/tools/mm.svg | 2 +- public/assets/tools/payload-cms.svg | 1 + public/assets/tools/payments.svg | 2 +- public/assets/tools/practice.svg | 1 - public/assets/tools/procurement.svg | 1 + public/assets/tools/reports.svg | 1 + public/assets/tools/review.svg | 2 +- public/assets/tools/sales-pipeline.svg | 1 + public/assets/tools/salesforce.svg | 1 + public/assets/tools/sprite.svg | 400 ++++-------------- public/assets/tools/support.svg | 1 + public/assets/tools/system-admin.svg | 1 + public/assets/tools/talent-search.svg | 1 - public/assets/tools/tcacademy.svg | 2 +- public/assets/tools/topcrowd.svg | 1 - public/assets/tools/wallet-admin.svg | 1 + public/assets/tools/work-app.svg | 1 + public/assets/tools/work-manager.svg | 22 - .../tool-selector/ToolMenu.module.scss | 137 +++--- .../components/tool-selector/ToolMenu.svelte | 93 ++-- .../tool-selector/ToolSelector.svelte | 48 ++- src/lib/config/hosts.ts | 18 + .../config/nav-menu/all-nav-items.config.ts | 153 +++++-- .../nav-menu/tool-selector-nav-items.ts | 146 ++++--- tests/tool-selector-nav-items.test.mjs | 136 ++++++ types/src/lib/config/hosts.d.ts | 16 + .../nav-menu/tool-selector-nav-items.d.ts | 7 + 43 files changed, 639 insertions(+), 681 deletions(-) create mode 100644 public/assets/tools/analytics.svg create mode 100644 public/assets/tools/archive.svg create mode 100644 public/assets/tools/campus.svg delete mode 100644 public/assets/tools/connect.svg create mode 100644 public/assets/tools/customer-portal.svg create mode 100644 public/assets/tools/leave-tracker.svg create mode 100644 public/assets/tools/mass-email.svg create mode 100644 public/assets/tools/payload-cms.svg delete mode 100644 public/assets/tools/practice.svg create mode 100644 public/assets/tools/procurement.svg create mode 100644 public/assets/tools/reports.svg create mode 100644 public/assets/tools/sales-pipeline.svg create mode 100644 public/assets/tools/salesforce.svg create mode 100644 public/assets/tools/support.svg create mode 100644 public/assets/tools/system-admin.svg delete mode 100644 public/assets/tools/talent-search.svg delete mode 100644 public/assets/tools/topcrowd.svg create mode 100644 public/assets/tools/wallet-admin.svg create mode 100644 public/assets/tools/work-app.svg delete mode 100644 public/assets/tools/work-manager.svg create mode 100644 tests/tool-selector-nav-items.test.mjs diff --git a/public/assets/tools/README.md b/public/assets/tools/README.md index 73ce119a..c6e5a4c3 100644 --- a/public/assets/tools/README.md +++ b/public/assets/tools/README.md @@ -15,3 +15,19 @@ eg: ``` + +## App drawer icons +The app drawer icons are 32x32 tiles: a tinted rounded square (`rx="4"`) with a 24x24 glyph centered on it, colored per drawer group: + +| Groups | Tile | Glyph | +| --- | --- | --- | +| Earn, Delivery & Payments | `#DEFBE6` | `#198038` | +| Compete | `#FFF2E8` | `#BA4E00` | +| AI, Insights | `#FFE5EC` | `#C1294F` | +| Learn | `#E5F3FF` | `#2C5771` | +| Connect, Community & Content | `#F6F2FF` | `#8A3FFC` | +| Sales & Customers | `#FCF4D6` | `#8E6A00` | +| Team & Platform | `#D9FBFB` | `#007D79` | + +Glyphs come from [Material Symbols Rounded](https://github.com/google/material-design-icons) (weight 600, Apache-2.0), except `copilot-portal`, `mm`, `sales-pipeline` and `discord`, which use [Tabler Icons](https://tabler.io/icons) (MIT). +Each icon also exists as `./{file-name}.svg`; keep its `` in `./sprite.svg` in sync, and avoid inner `id`s so symbols don't clash inside the shared sprite. diff --git a/public/assets/tools/ai-hub.svg b/public/assets/tools/ai-hub.svg index d6b65143..00470065 100644 --- a/public/assets/tools/ai-hub.svg +++ b/public/assets/tools/ai-hub.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/public/assets/tools/analytics.svg b/public/assets/tools/analytics.svg new file mode 100644 index 00000000..b3d9cc51 --- /dev/null +++ b/public/assets/tools/analytics.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/archive.svg b/public/assets/tools/archive.svg new file mode 100644 index 00000000..a17d5f2e --- /dev/null +++ b/public/assets/tools/archive.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/articles.svg b/public/assets/tools/articles.svg index f38ae9e7..fb57fe1a 100644 --- a/public/assets/tools/articles.svg +++ b/public/assets/tools/articles.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/campus.svg b/public/assets/tools/campus.svg new file mode 100644 index 00000000..32ac314c --- /dev/null +++ b/public/assets/tools/campus.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/challenges.svg b/public/assets/tools/challenges.svg index 162bb4a6..2a55a448 100644 --- a/public/assets/tools/challenges.svg +++ b/public/assets/tools/challenges.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/connect.svg b/public/assets/tools/connect.svg deleted file mode 100644 index d82908d6..00000000 --- a/public/assets/tools/connect.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/assets/tools/copilot-portal.svg b/public/assets/tools/copilot-portal.svg index 421ec81b..1dee056e 100644 --- a/public/assets/tools/copilot-portal.svg +++ b/public/assets/tools/copilot-portal.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - - - - + diff --git a/public/assets/tools/customer-portal.svg b/public/assets/tools/customer-portal.svg new file mode 100644 index 00000000..7b23e4d9 --- /dev/null +++ b/public/assets/tools/customer-portal.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/discord.svg b/public/assets/tools/discord.svg index 4ceb835c..0ba6cb1e 100644 --- a/public/assets/tools/discord.svg +++ b/public/assets/tools/discord.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/forums.svg b/public/assets/tools/forums.svg index 2981f072..a9b20fe8 100644 --- a/public/assets/tools/forums.svg +++ b/public/assets/tools/forums.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/gigs.svg b/public/assets/tools/gigs.svg index ade9b2b6..347cba30 100644 --- a/public/assets/tools/gigs.svg +++ b/public/assets/tools/gigs.svg @@ -1 +1 @@ - + diff --git a/public/assets/tools/leave-tracker.svg b/public/assets/tools/leave-tracker.svg new file mode 100644 index 00000000..94a19187 --- /dev/null +++ b/public/assets/tools/leave-tracker.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/mass-email.svg b/public/assets/tools/mass-email.svg new file mode 100644 index 00000000..c7d64d80 --- /dev/null +++ b/public/assets/tools/mass-email.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/mm-tournament.svg b/public/assets/tools/mm-tournament.svg index 7eb48396..81b2b997 100644 --- a/public/assets/tools/mm-tournament.svg +++ b/public/assets/tools/mm-tournament.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - - - - - - - + diff --git a/public/assets/tools/mm.svg b/public/assets/tools/mm.svg index 1af6911d..7061701f 100644 --- a/public/assets/tools/mm.svg +++ b/public/assets/tools/mm.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/payload-cms.svg b/public/assets/tools/payload-cms.svg new file mode 100644 index 00000000..8f72cfff --- /dev/null +++ b/public/assets/tools/payload-cms.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/payments.svg b/public/assets/tools/payments.svg index fcdc23ba..76b7b130 100644 --- a/public/assets/tools/payments.svg +++ b/public/assets/tools/payments.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/practice.svg b/public/assets/tools/practice.svg deleted file mode 100644 index ec03a261..00000000 --- a/public/assets/tools/practice.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/assets/tools/procurement.svg b/public/assets/tools/procurement.svg new file mode 100644 index 00000000..0fff3b67 --- /dev/null +++ b/public/assets/tools/procurement.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/reports.svg b/public/assets/tools/reports.svg new file mode 100644 index 00000000..44ebc528 --- /dev/null +++ b/public/assets/tools/reports.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/review.svg b/public/assets/tools/review.svg index 21041f9a..1f2da4cc 100644 --- a/public/assets/tools/review.svg +++ b/public/assets/tools/review.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/sales-pipeline.svg b/public/assets/tools/sales-pipeline.svg new file mode 100644 index 00000000..f2c7285f --- /dev/null +++ b/public/assets/tools/sales-pipeline.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/salesforce.svg b/public/assets/tools/salesforce.svg new file mode 100644 index 00000000..40718a15 --- /dev/null +++ b/public/assets/tools/salesforce.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/sprite.svg b/public/assets/tools/sprite.svg index dcd72689..e8f0613d 100644 --- a/public/assets/tools/sprite.svg +++ b/public/assets/tools/sprite.svg @@ -1,104 +1,19 @@ - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -117,20 +32,8 @@ - - - - - - - - - - - - - - + + @@ -164,87 +67,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -261,109 +86,22 @@ - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -382,35 +120,71 @@ - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/tools/support.svg b/public/assets/tools/support.svg new file mode 100644 index 00000000..d3fe516f --- /dev/null +++ b/public/assets/tools/support.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/system-admin.svg b/public/assets/tools/system-admin.svg new file mode 100644 index 00000000..1b5712b5 --- /dev/null +++ b/public/assets/tools/system-admin.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/talent-search.svg b/public/assets/tools/talent-search.svg deleted file mode 100644 index 36706d43..00000000 --- a/public/assets/tools/talent-search.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/public/assets/tools/tcacademy.svg b/public/assets/tools/tcacademy.svg index f42595be..a735cc9d 100644 --- a/public/assets/tools/tcacademy.svg +++ b/public/assets/tools/tcacademy.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/topcrowd.svg b/public/assets/tools/topcrowd.svg deleted file mode 100644 index 79e0d9a3..00000000 --- a/public/assets/tools/topcrowd.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/assets/tools/wallet-admin.svg b/public/assets/tools/wallet-admin.svg new file mode 100644 index 00000000..68e5c04d --- /dev/null +++ b/public/assets/tools/wallet-admin.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/work-app.svg b/public/assets/tools/work-app.svg new file mode 100644 index 00000000..47f28069 --- /dev/null +++ b/public/assets/tools/work-app.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/work-manager.svg b/public/assets/tools/work-manager.svg deleted file mode 100644 index ef1c5b25..00000000 --- a/public/assets/tools/work-manager.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/lib/components/tool-selector/ToolMenu.module.scss b/src/lib/components/tool-selector/ToolMenu.module.scss index e8f19bdc..83d11347 100644 --- a/src/lib/components/tool-selector/ToolMenu.module.scss +++ b/src/lib/components/tool-selector/ToolMenu.module.scss @@ -1,17 +1,19 @@ @use 'lib/styles/mixins.scss' as *; @use 'lib/styles/fonts.scss' as *; -@use 'lib/styles/colors.scss' as *; .toolMenuWrap { - width: 472px; + width: 496px; + // completes the popup's vertical padding to 32px + padding: 8px 0; - font-family: $roboto; + font-family: $nunito; font-style: normal; font-size: 14px; - color: #767676; + color: #6F6F6F; @include mobile { width: auto; + padding: 0; } > svg { @@ -20,19 +22,20 @@ } .title { - font-family: $roboto; + font-family: $nunito; font-style: normal; font-weight: 700; - font-size: 12px; - line-height: 16px; - letter-spacing: 1px; + font-size: 14px; + line-height: 20px; text-transform: uppercase; + color: #161616; } .toolSection { &Title { @extend .title; - margin-bottom: 16px; + line-height: 24px; + margin-bottom: 12px; } // hides the menu title when the section is alone in the whole menu @@ -55,19 +58,24 @@ } } -.toolGroups { +.toolColumns { display: flex; - gap: 24px; - flex-wrap: wrap; + gap: 48px; - .toolSection.talent & { + @include mobile { flex-direction: column; - max-height: 420px; + gap: 0; } +} + +.toolGroups { + display: flex; + flex-direction: column; + gap: 24px; + flex: 1 1 0; + min-width: 0; @include mobile { - flex-direction: column; - flex-wrap: nowrap; gap: 0; } } @@ -77,34 +85,26 @@ flex-direction: column; align-items: flex-start; - flex: 1 1 calc(50% - 24px); - &Title { @extend .title; - font-weight: 400; + font-weight: 600; + font-size: 12px; + line-height: 16px; + color: #6F6F6F; - margin-bottom: 12px; + margin-bottom: 8px; } @include mobile { padding: 16px; - order: var(--order); - - &.hasCtas { - border-top: 1px solid #D4D4D4; - } } } .toolNavItems { + display: flex; + flex-direction: column; + gap: 8px; width: 100%; - line-height: 1; - - @include mobile { - display: flex; - flex-direction: column; - align-items: flex-start; - } } .toolNavItem { @@ -112,57 +112,21 @@ align-items: center; gap: 8px; width: 100%; - overflow: hidden; - &.toolNavItem { - font-weight: 500; - color: color(hoverMenuLink); - } - - & + & { - margin-top: 12px; - } - - &:global(.navButton) { - display: inline-flex; - font-weight: 700; - font-size: 12px; - line-height: 24px; - letter-spacing: 0.008em; - text-transform: uppercase; - - color: #16679A; - border: 2px solid #16679A; - border-radius: 24px; - - padding: 0 16px; - width: auto; - - &:hover { - color: #219174; - border-color: #219174; - } - - .navItemLabel { - margin-top: 0; - margin-bottom: 0; - } - } - - &:hover { - font-weight: 700; - color: #0c0c0c; - color: color(hoverMenuLink, hover); + height: 32px; + // the description is revealed for the hovered (or keyboard focused) tool only + &:hover, + &:focus-visible { .navItemDescription { + max-height: 14px; opacity: 1; - pointer-events:unset; - font-weight: 400; - color: #767676; } } } .toolIcon { + flex: 0 0 auto; + img, svg { display: block; width: 32px; @@ -173,23 +137,32 @@ .navItemContent { display: flex; flex-direction: column; - margin: -3px 0 -4px; + min-width: 0; } .navItemLabel { display: block; - line-height: 22px; - margin-bottom: -4px; - margin-top: 4px; + font-size: 14px; + line-height: 18px; + color: #161616; + white-space: nowrap; } .navItemDescription { + display: block; font-style: italic; - font-size: 10px; - line-height: 18px; + font-size: 12px; + line-height: 14px; + color: #6F6F6F; + white-space: nowrap; + + max-height: 0; + // collapse vertically only, long descriptions may run into the columns gap + overflow-x: visible; + overflow-y: clip; opacity: 0; pointer-events: none; - transition: 0.2s ease-in-out; + transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out; } .toolMenuSpacer { diff --git a/src/lib/components/tool-selector/ToolMenu.svelte b/src/lib/components/tool-selector/ToolMenu.svelte index e19efad6..9632b95c 100644 --- a/src/lib/components/tool-selector/ToolMenu.svelte +++ b/src/lib/components/tool-selector/ToolMenu.svelte @@ -1,73 +1,68 @@
{#each navMenuItems as section, sectionIndex} {#if section} -
+
{section.label}
-
- {#each section.children ?? [] as group} -
- {#if group.label} -
- {group.label} -
- {/if} - -
- {#each group.children ?? [] as navItem} - - {#if navItem.type !== 'cta'} -
- {#if navItem.icon} - - - - {:else} - {navItem.label} - {/if} -
- {/if} -
diff --git a/src/lib/components/tool-selector/ToolSelector.svelte b/src/lib/components/tool-selector/ToolSelector.svelte index f71c55ad..50d5a5c4 100644 --- a/src/lib/components/tool-selector/ToolSelector.svelte +++ b/src/lib/components/tool-selector/ToolSelector.svelte @@ -1,6 +1,7 @@ -
popupIsVisible = true} - on:keydown={() => {}} -> - - - Tool - -
+{#if hasToolMenuItems} +
popupIsVisible = true} + on:keydown={() => {}} + > + + + Tool + +
-{#if !$isMobile} - - - -{:else if popupIsVisible} - popupIsVisible = false}> - - + {#if !$isMobile} + + + + {:else if popupIsVisible} + popupIsVisible = false}> + + + {/if} {/if} diff --git a/src/lib/config/hosts.ts b/src/lib/config/hosts.ts index 8ebdae49..072e7630 100644 --- a/src/lib/config/hosts.ts +++ b/src/lib/config/hosts.ts @@ -36,3 +36,21 @@ export const ACCOUNT_SETTINGS_HOST: string = `https://account-settings.${TC_DOMA export const WALLETAPP_HOST: string = `https://wallet.${TC_DOMAIN}`; export const COPILOT_PORTAL_HOST: string = `https://copilots.${TC_DOMAIN}`; export const ENGAGEMENT_PORTAL_HOST: string = `https://engagements.${TC_DOMAIN}`; + +// App drawer (tool selector) hosts +export const ARCHIVE_HOST: string = `https://archive.${TC_DOMAIN}`; +export const FORUMS_HOST: string = `https://forums.${TC_DOMAIN}`; +export const WORK_APP_HOST: string = `https://work.${TC_DOMAIN}`; +export const WALLET_ADMIN_HOST: string = `https://wallet-admin.${TC_DOMAIN}`; +export const CUSTOMER_PORTAL_HOST: string = `https://customer.${TC_DOMAIN}`; +export const SALES_HOST: string = `https://sales.${TC_DOMAIN}`; +export const PROCUREMENT_HOST: string = `https://procurement.${TC_DOMAIN}`; +export const ANALYTICS_HOST: string = `https://analytics.${TC_DOMAIN}`; +export const REPORTS_HOST: string = `https://reports.${TC_DOMAIN}`; +export const SUPPORT_HOST: string = `https://support.${TC_DOMAIN}`; +export const PAYLOAD_CMS_HOST: string = `https://cms.${TC_DOMAIN}`; +export const CONTACT_HOST: string = `https://contact.${TC_DOMAIN}`; +export const CAMPUS_HOST: string = `https://campus.${TC_DOMAIN}`; +export const CALENDAR_HOST: string = `https://calendar.${TC_DOMAIN}`; +export const SYSTEM_ADMIN_HOST: string = `https://system-admin.${TC_DOMAIN}`; +export const SALESFORCE_URL: string = 'https://topcoder.my.salesforce.com'; diff --git a/src/lib/config/nav-menu/all-nav-items.config.ts b/src/lib/config/nav-menu/all-nav-items.config.ts index aa550bd3..efab905e 100644 --- a/src/lib/config/nav-menu/all-nav-items.config.ts +++ b/src/lib/config/nav-menu/all-nav-items.config.ts @@ -4,16 +4,29 @@ import type { NavMenuItem } from "../../functions/nav-menu-item.model"; import { CHALLENGE_HOST, COMMUNITY_HOST, - CONNECT_HOST, COPILOT_PORTAL_HOST, REVIEW_APP_HOST, - TALENT_SEARCH_HOST, TCACADEMY_HOST, PLATFORM_APP_HOST, WALLETAPP_HOST, - WORK_MANAGER_HOST, AUTH0_AUTHENTICATOR_URL, ENGAGEMENT_PORTAL_HOST, + ARCHIVE_HOST, + FORUMS_HOST, + WORK_APP_HOST, + WALLET_ADMIN_HOST, + CUSTOMER_PORTAL_HOST, + SALES_HOST, + PROCUREMENT_HOST, + ANALYTICS_HOST, + REPORTS_HOST, + SUPPORT_HOST, + PAYLOAD_CMS_HOST, + CONTACT_HOST, + CAMPUS_HOST, + CALENDAR_HOST, + SYSTEM_ADMIN_HOST, + SALESFORCE_URL, } from '..'; export const allNavItems: {[key: string]: NavMenuItem} = { @@ -42,21 +55,21 @@ export const allNavItems: {[key: string]: NavMenuItem} = { mmTournament: { label: 'Marathon Match Tournament', icon: 'mm-tournament', - description: 'Join the Excitement', + description: 'Compete for the MM championship', marketingPathname: '/marathon-match-tournament', url: getMarketingUrl('/marathon-match-tournament'), }, aiHub: { label: 'AI Hub', icon: 'ai-hub', - description: 'Collaborate, Compete, Create', + description: 'Build and compete with AI', marketingPathname: '/ai-hub', url: getMarketingUrl('/ai-hub'), }, copilotPortal: { label: 'Copilot Portal', icon: 'copilot-portal', - description: 'Copilot Opportunities', + description: 'Copilot opportunities', url: COPILOT_PORTAL_HOST, }, howItWorks: { @@ -93,9 +106,15 @@ export const allNavItems: {[key: string]: NavMenuItem} = { articles: { label: 'Articles', icon: 'articles', - description: 'Get insights and tips about Topcoder', + description: 'Tutorials and how-tos', url: `${COMMUNITY_HOST}/thrive`, }, + archive: { + label: 'Archive', + icon: 'archive', + description: 'Past problems & TCO history', + url: ARCHIVE_HOST, + }, blog: { label: 'Blog', url: getMarketingUrl('/blog'), @@ -124,6 +143,12 @@ export const allNavItems: {[key: string]: NavMenuItem} = { icon: 'discord', description: 'Chat live with members', }, + publicForums: { + label: 'Public Forums', + url: FORUMS_HOST, + icon: 'forums', + description: 'Q&A and discussions', + }, home: { label: 'Home', marketingPathname: '/', @@ -141,30 +166,12 @@ export const allNavItems: {[key: string]: NavMenuItem} = { icon: 'payments', description: 'Get paid', }, - practice: { - label: 'Practice', - icon: 'practice', - url: `${CHALLENGE_HOST}/challenges?search=practice&tracks%5BDS%5D=true&tracks%5BDes%5D=true&tracks%5BDev%5D=true&tracks%5BQA%5D=true&types%5B%5D=CH&types%5B%5D=F2F&types%5B%5D=TSK`, - description: 'Learn to compete', - }, review: { - label: 'Review', + label: 'Review App', url: REVIEW_APP_HOST, icon: 'review', description: 'Review submissions', }, - communityHome: { - label: 'Community Home', - description: 'Recent challenges and news.', - icon: 'topcrowd', - url: `${COMMUNITY_HOST}/home`, - }, - talentSearchApp: { - label: 'Meet our Talent', - icon: 'talent-search', - description: 'Find skilled talent for your next project', - url: TALENT_SEARCH_HOST, - }, talkToAnExpert: { label: 'Talk to an Expert', description: 'Speak with a Topcoder expert to get started.', @@ -177,17 +184,89 @@ export const allNavItems: {[key: string]: NavMenuItem} = { icon: 'tcacademy', description: 'Learn new skills', }, - workManager: { - label: 'Work Manager', - icon: 'work-manager', - description: 'Work Manager App', - url: `${WORK_MANAGER_HOST}`, - }, - staffConnect: { - label: 'Connect', - icon: 'connect', - description: 'Connect App', - url: `${CONNECT_HOST}`, + workApp: { + label: 'Work App', + icon: 'work-app', + description: 'Launch and manage work', + url: WORK_APP_HOST, + }, + walletAdmin: { + label: 'Wallet Admin', + icon: 'wallet-admin', + description: 'Manage member payouts', + url: WALLET_ADMIN_HOST, + }, + customerPortal: { + label: 'Customer Portal', + icon: 'customer-portal', + description: 'Talent, stats & showcases', + url: CUSTOMER_PORTAL_HOST, + }, + salesforce: { + label: 'Salesforce', + icon: 'salesforce', + description: 'Manage accounts & leads', + url: SALESFORCE_URL, + }, + salesPipeline: { + label: 'Sales Pipeline', + icon: 'sales-pipeline', + description: 'Track deals by stage', + url: SALES_HOST, + }, + procurement: { + label: 'Procurement', + icon: 'procurement', + description: 'Vendors and purchasing', + url: PROCUREMENT_HOST, + }, + analytics: { + label: 'Analytics', + icon: 'analytics', + description: 'Platform and member data', + url: ANALYTICS_HOST, + }, + reports: { + label: 'Reports', + icon: 'reports', + description: 'Pull data on demand', + url: REPORTS_HOST, + }, + supportApp: { + label: 'Support', + icon: 'support', + description: 'Handle member tickets', + url: SUPPORT_HOST, + }, + payloadCms: { + label: 'Payload CMS', + icon: 'payload-cms', + description: 'Edit site content', + url: `${PAYLOAD_CMS_HOST}/admin`, + }, + massEmail: { + label: 'Mass Email', + icon: 'mass-email', + description: 'Email members at scale', + url: CONTACT_HOST, + }, + campus: { + label: 'Campus', + icon: 'campus', + description: 'University programs', + url: CAMPUS_HOST, + }, + leaveTracker: { + label: 'Leave Tracker', + icon: 'leave-tracker', + description: 'Plan and track time off', + url: CALENDAR_HOST, + }, + systemAdmin: { + label: 'System Admin', + icon: 'system-admin', + description: 'Platform-wide admin tools', + url: SYSTEM_ADMIN_HOST, }, bugHunt: { label: 'Bug Hunt', diff --git a/src/lib/config/nav-menu/tool-selector-nav-items.ts b/src/lib/config/nav-menu/tool-selector-nav-items.ts index 9aceab55..59be6ec4 100644 --- a/src/lib/config/nav-menu/tool-selector-nav-items.ts +++ b/src/lib/config/nav-menu/tool-selector-nav-items.ts @@ -1,86 +1,120 @@ import type { NavMenuItem } from "../../functions/nav-menu-item.model"; -import { CUSTOMER_ROUTE_GUARD, STAFF_ROUTE_GUARD, TALENT_ROUTE_GUARD } from "../auth" +import { STAFF_ROUTE_GUARD, TALENT_ROUTE_GUARD } from "../auth" import { allNavItems } from "./all-nav-items.config" +/** + * App drawer (tool selector) menu. + * + * Structure: section (role guarded) -> columns -> groups -> nav items. + * Each section renders its columns side by side on desktop and stacked on mobile, + * so the groups listed in a column are displayed top to bottom in that column. + */ export const toolSelectorNavItems: NavMenuItem = { children: [ { - label: "Customer", + label: "Talent", children: [ { children: [ - allNavItems.talentSearchApp, + { + label: "Earn", + children: [ + allNavItems.challengesApp, + allNavItems.engagementsApp, + allNavItems.copilotPortal, + allNavItems.review, + allNavItems.payments, + ] + }, + { + label: "Compete", + children: [ + allNavItems.marathonMatchesApp, + allNavItems.mmTournament, + ] + }, ] }, { children: [ - allNavItems.talkToAnExpert, - allNavItems.bookADemo, + { + label: "AI", + children: [ + allNavItems.aiHub, + ] + }, + { + label: "Learn", + children: [ + allNavItems.topcoderAcademyApp, + { + ...allNavItems.articles, + url: `${allNavItems.articles.url}?navTool=tool` + }, + allNavItems.archive, + ] + }, + { + label: "Connect", + children: [ + allNavItems.publicForums, + allNavItems.discordApp, + ] + }, ] - } + }, ], - ...CUSTOMER_ROUTE_GUARD, + ...TALENT_ROUTE_GUARD, }, { - label: "Talent", + label: "Staff", children: [ { - label: "Learn", - groupOrder: 1, - children: [ - allNavItems.communityHome, - allNavItems.topcoderAcademyApp, - allNavItems.practice, - ] - }, - { - label: "Earn", - groupOrder: 2, - children: [ - allNavItems.challengesApp, - allNavItems.engagementsApp, - allNavItems.review, - allNavItems.payments, - allNavItems.copilotPortal, - ] - }, - { - label: "AI", - groupOrder: 3, - children: [ - allNavItems.aiHub, - ] - }, - { - label: "Compete", - groupOrder: 3, children: [ - allNavItems.marathonMatchesApp, - allNavItems.mmTournament, + { + label: "Delivery & Payments", + children: [ + allNavItems.workApp, + allNavItems.walletAdmin, + ] + }, + { + label: "Sales & Customers", + children: [ + allNavItems.customerPortal, + allNavItems.salesforce, + allNavItems.salesPipeline, + allNavItems.procurement, + ] + }, + { + label: "Insights", + children: [ + allNavItems.analytics, + allNavItems.reports, + ] + }, ] }, { - label: "Connect", - groupOrder: 4, children: [ { - ...allNavItems.articles, - url: `${allNavItems.articles.url}?navTool=tool` + label: "Community & Content", + children: [ + allNavItems.supportApp, + allNavItems.payloadCms, + allNavItems.massEmail, + allNavItems.campus, + ] + }, + { + label: "Team & Platform", + children: [ + allNavItems.leaveTracker, + allNavItems.systemAdmin, + ] }, - allNavItems.discordApp, - ] - } - ], - ...TALENT_ROUTE_GUARD, - }, - { - label: "Staff", - children: [ - { - children: [ - allNavItems.workManager, - allNavItems.staffConnect, ] }, ], diff --git a/tests/tool-selector-nav-items.test.mjs b/tests/tool-selector-nav-items.test.mjs new file mode 100644 index 00000000..4b1bc7c6 --- /dev/null +++ b/tests/tool-selector-nav-items.test.mjs @@ -0,0 +1,136 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { after, before, test } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { createServer } from 'vite'; + +const root = fileURLToPath(new URL('..', import.meta.url)); + +// expected app drawer (PM-6555 design): section -> columns -> groups -> tool labels +const TALENT = ['Talent', [ + [ + ['Earn', ['Opportunities', 'Engagement Portal', 'Copilot Portal', 'Review App', 'Wallet']], + ['Compete', ['Marathon Matches', 'Marathon Match Tournament']], + ], + [ + ['AI', ['AI Hub']], + ['Learn', ['Topcoder Academy', 'Articles', 'Archive']], + ['Connect', ['Public Forums', 'Discord']], + ], +]]; +const STAFF = ['Staff', [ + [ + ['Delivery & Payments', ['Work App', 'Wallet Admin']], + ['Sales & Customers', ['Customer Portal', 'Salesforce', 'Sales Pipeline', 'Procurement']], + ['Insights', ['Analytics', 'Reports']], + ], + [ + ['Community & Content', ['Support', 'Payload CMS', 'Mass Email', 'Campus']], + ['Team & Platform', ['Leave Tracker', 'System Admin']], + ], +]]; + +let server; +let getToolSelectorItems; + +before(async () => { + process.env.VITE_APP_HOST_ENV = 'prod'; + server = await createServer({ + root, + configFile: false, + logLevel: 'silent', + appType: 'custom', + resolve: { alias: { lib: `${root}src/lib` } }, + server: { middlewareMode: true, hmr: false, watch: null }, + }); + ({ getToolSelectorItems } = await server.ssrLoadModule('/src/lib/functions/tool-selector-menu.provider.ts')); +}); + +after(() => server?.close()); + +/** + * Stubs the browser globals read by the jwt helpers with a `tcjwt` cookie holding the given roles. + * @param {string[]} roles JWT roles of the signed in user + */ +function signIn(roles) { + const payload = Buffer.from(JSON.stringify({ 'https://topcoder.com/roles': roles })).toString('base64url'); + globalThis.window = { atob: globalThis.atob }; + globalThis.document = { cookie: `tcjwt=e30.${payload}.signature` }; +} + +/** Reduces the drawer menu to [section, [column[[group, labels]]]] for comparison with the design. */ +const toLayout = (sections) => sections.map((section) => [ + section.label, + section.children.map((column) => column.children.map((group) => [ + group.label, + group.children.map((item) => item.label), + ])), +]); + +const toItems = (sections) => sections.flatMap((section) => section.children + .flatMap((column) => column.children.flatMap((group) => group.children))); + +test('staff see the Talent and Staff sections laid out as in the design', () => { + signIn(['Topcoder User', 'Topcoder Staff']); + assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT, STAFF]); +}); + +test('members only see the Talent section', () => { + for (const roles of [['Topcoder User'], ['Topcoder User', 'Topcoder Talent']]) { + signIn(roles); + assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT]); + } +}); + +test('customers without talent or staff roles get no drawer items', () => { + signIn(['Topcoder Customer']); + assert.deepEqual(getToolSelectorItems(), []); +}); + +test('every drawer tool has a description, a sprite icon and its production url', async () => { + const sprite = await readFile(`${root}public/assets/tools/sprite.svg`, 'utf8'); + signIn(['Topcoder Staff']); + const items = toItems(getToolSelectorItems()); + + for (const item of items) { + assert.ok(item.description, `${item.label} has a description`); + assert.ok(sprite.includes(`id="${item.icon}"`), `${item.label} icon "${item.icon}" is in the sprite`); + } + + const urls = Object.fromEntries(items.map((item) => [item.label, item.url])); + assert.deepEqual({ + 'Archive': urls['Archive'], + 'Public Forums': urls['Public Forums'], + 'Work App': urls['Work App'], + 'Wallet Admin': urls['Wallet Admin'], + 'Customer Portal': urls['Customer Portal'], + 'Salesforce': urls['Salesforce'], + 'Sales Pipeline': urls['Sales Pipeline'], + 'Procurement': urls['Procurement'], + 'Analytics': urls['Analytics'], + 'Reports': urls['Reports'], + 'Support': urls['Support'], + 'Payload CMS': urls['Payload CMS'], + 'Mass Email': urls['Mass Email'], + 'Campus': urls['Campus'], + 'Leave Tracker': urls['Leave Tracker'], + 'System Admin': urls['System Admin'], + }, { + 'Archive': 'https://archive.topcoder.com', + 'Public Forums': 'https://forums.topcoder.com', + 'Work App': 'https://work.topcoder.com', + 'Wallet Admin': 'https://wallet-admin.topcoder.com', + 'Customer Portal': 'https://customer.topcoder.com', + 'Salesforce': 'https://topcoder.my.salesforce.com', + 'Sales Pipeline': 'https://sales.topcoder.com', + 'Procurement': 'https://procurement.topcoder.com', + 'Analytics': 'https://analytics.topcoder.com', + 'Reports': 'https://reports.topcoder.com', + 'Support': 'https://support.topcoder.com', + 'Payload CMS': 'https://cms.topcoder.com/admin', + 'Mass Email': 'https://contact.topcoder.com', + 'Campus': 'https://campus.topcoder.com', + 'Leave Tracker': 'https://calendar.topcoder.com', + 'System Admin': 'https://system-admin.topcoder.com', + }); +}); diff --git a/types/src/lib/config/hosts.d.ts b/types/src/lib/config/hosts.d.ts index 0c5526f8..503bf21e 100644 --- a/types/src/lib/config/hosts.d.ts +++ b/types/src/lib/config/hosts.d.ts @@ -19,3 +19,19 @@ export declare const ACCOUNT_SETTINGS_HOST: string; export declare const WALLETAPP_HOST: string; export declare const COPILOT_PORTAL_HOST: string; export declare const ENGAGEMENT_PORTAL_HOST: string; +export declare const ARCHIVE_HOST: string; +export declare const FORUMS_HOST: string; +export declare const WORK_APP_HOST: string; +export declare const WALLET_ADMIN_HOST: string; +export declare const CUSTOMER_PORTAL_HOST: string; +export declare const SALES_HOST: string; +export declare const PROCUREMENT_HOST: string; +export declare const ANALYTICS_HOST: string; +export declare const REPORTS_HOST: string; +export declare const SUPPORT_HOST: string; +export declare const PAYLOAD_CMS_HOST: string; +export declare const CONTACT_HOST: string; +export declare const CAMPUS_HOST: string; +export declare const CALENDAR_HOST: string; +export declare const SYSTEM_ADMIN_HOST: string; +export declare const SALESFORCE_URL: string; diff --git a/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts b/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts index 6b23ef7e..db1ca51e 100644 --- a/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts +++ b/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts @@ -1,2 +1,9 @@ import type { NavMenuItem } from "../../functions/nav-menu-item.model"; +/** + * App drawer (tool selector) menu. + * + * Structure: section (role guarded) -> columns -> groups -> nav items. + * Each section renders its columns side by side on desktop and stacked on mobile, + * so the groups listed in a column are displayed top to bottom in that column. + */ export declare const toolSelectorNavItems: NavMenuItem; From 318700778f899d181711d1ee6a017ce3fe6936b0 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 13 Oct 2026 15:23:01 +1100 Subject: [PATCH 2/3] fix: show app drawer for administrators (PM-6555) --- src/README.md | 15 +++++++++++ src/lib/config/auth.ts | 5 ++-- src/lib/functions/user-profile.provider.ts | 12 +++++++-- tests/tool-selector-nav-items.test.mjs | 26 ++++++++++++++++--- types/src/lib/config/auth.d.ts | 1 + .../lib/functions/user-profile.provider.d.ts | 7 +++++ 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/README.md b/src/README.md index b8a30e3e..77f9959a 100644 --- a/src/README.md +++ b/src/README.md @@ -11,6 +11,7 @@ For information re how to integrate the UniNav into a web property, see the main - [Serve the local app](#serve-the-local-app) - [Demo Apps](#demo-apps) - [Instructions for using the Demo Apps](#instructions-for-using-the-demo-apps) +- [App Drawer Roles](#app-drawer-roles) - [Production Distribution](#production-distribution) - [Recommended IDE Setup](#recommended-ide-setup) - [Typescript Type Declaration Generation](#typescript-type-declaration-generation) @@ -82,6 +83,20 @@ There are 2 sample integrations in the [/demo](./demo/) folder: - Marketing Nav: http://local.topcoder-dev.com:8081 - Tool Nav: http://local.topcoder-dev.com:8082 +## App Drawer Roles + +The signed-in user's app drawer is filtered using the roles in the `tcjwt` +cookie by `getUserAppRoles` and the guards in `src/lib/config/auth.ts`. +`Topcoder Staff` and `administrator` users see both Talent and Staff sections, +including administrators who also have `Topcoder Customer` or `Topcoder Talent`. +The administrator role is matched case-insensitively, so legacy `Administrator` +claims receive the same navigation. Other role names retain their exact matching. + +Members with `Topcoder Talent`, or without a recognized navigation role, see +Talent only. Customer-only users have no drawer items, so the drawer trigger is +hidden. Application-specific roles such as `Payment Admin` do not grant the Staff +section. Each linked application still enforces its own access permissions. + ## Production Distribution Run `npm run build` to build the files for production. The output is to be found in the `dist` folder. diff --git a/src/lib/config/auth.ts b/src/lib/config/auth.ts index 4c9c1d56..d8327032 100644 --- a/src/lib/config/auth.ts +++ b/src/lib/config/auth.ts @@ -2,14 +2,15 @@ export enum AUTH_USER_ROLE { talent = 'Topcoder Talent', customer = 'Topcoder Customer', staff = 'Topcoder Staff', + administrator = 'administrator', noRole = 'no-role', } export const AUTH_USER_ROLE_VALUES = Object.values(AUTH_USER_ROLE); -export const AUTH_TALENT_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.talent, AUTH_USER_ROLE.staff, AUTH_USER_ROLE.noRole]; +export const AUTH_TALENT_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.talent, AUTH_USER_ROLE.staff, AUTH_USER_ROLE.administrator, AUTH_USER_ROLE.noRole]; export const AUTH_CUSTOMER_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.customer, AUTH_USER_ROLE.staff, AUTH_USER_ROLE.noRole]; -export const AUTH_STAFF_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.staff]; +export const AUTH_STAFF_ROLES: AUTH_USER_ROLE[] = [AUTH_USER_ROLE.staff, AUTH_USER_ROLE.administrator]; export const TALENT_ROUTE_GUARD = { auth: { diff --git a/src/lib/functions/user-profile.provider.ts b/src/lib/functions/user-profile.provider.ts index 6c6d0732..59532710 100644 --- a/src/lib/functions/user-profile.provider.ts +++ b/src/lib/functions/user-profile.provider.ts @@ -18,9 +18,17 @@ export const getJwtUserRoles = (): AuthUser['roles'] | undefined => { return getAuthJwtDomainProp('roles') } -// get the user roles that match the config/auth's user roles definitions +/** + * Reads the current tcjwt cookie and returns the roles recognized by navigation guards. + * Administrator roles are matched case-insensitively and returned in their canonical form. + * Takes no parameters; unrelated roles are omitted from the returned array. + * @returns Recognized navigation roles, or an empty array when no roles are present. + * @throws If the tcjwt cookie cannot be decoded or contains invalid JSON. + */ export const getUserAppRoles = (): AuthUser['roles'] => { - return (getJwtUserRoles() ?? []).filter(d => AUTH_USER_ROLE_VALUES.includes(d as AUTH_USER_ROLE)) + return (getJwtUserRoles() ?? []) + .map(role => role.toLowerCase() === AUTH_USER_ROLE.administrator ? AUTH_USER_ROLE.administrator : role) + .filter(role => AUTH_USER_ROLE_VALUES.includes(role as AUTH_USER_ROLE)) } export const checkUserAppRole = (role: AUTH_USER_ROLE): boolean => ( diff --git a/tests/tool-selector-nav-items.test.mjs b/tests/tool-selector-nav-items.test.mjs index 4b1bc7c6..80eca69d 100644 --- a/tests/tool-selector-nav-items.test.mjs +++ b/tests/tool-selector-nav-items.test.mjs @@ -75,16 +75,34 @@ test('staff see the Talent and Staff sections laid out as in the design', () => assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT, STAFF]); }); +for (const roles of [ + ['administrator'], + ['Topcoder User', 'administrator', 'Topcoder Customer'], + ['administrator', 'Topcoder Talent'], + ['Administrator', 'Topcoder Customer'], +]) { + test(`administrators see Talent and Staff with roles: ${roles.join(', ')}`, () => { + signIn(roles); + assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT, STAFF]); + }); +} + test('members only see the Talent section', () => { - for (const roles of [['Topcoder User'], ['Topcoder User', 'Topcoder Talent']]) { + for (const roles of [[], ['Topcoder User'], ['Topcoder User', 'Topcoder Talent'], ['Payment Admin']]) { signIn(roles); assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT]); } }); -test('customers without talent or staff roles get no drawer items', () => { - signIn(['Topcoder Customer']); - assert.deepEqual(getToolSelectorItems(), []); +test('customers without talent, staff or administrator roles get no drawer items', () => { + for (const roles of [ + ['Topcoder Customer'], + ['Topcoder Customer', 'Payment Admin'], + ['Topcoder Customer', 'not-administrator'], + ]) { + signIn(roles); + assert.deepEqual(getToolSelectorItems(), []); + } }); test('every drawer tool has a description, a sprite icon and its production url', async () => { diff --git a/types/src/lib/config/auth.d.ts b/types/src/lib/config/auth.d.ts index fe692176..d6e54c47 100644 --- a/types/src/lib/config/auth.d.ts +++ b/types/src/lib/config/auth.d.ts @@ -2,6 +2,7 @@ export declare enum AUTH_USER_ROLE { talent = "Topcoder Talent", customer = "Topcoder Customer", staff = "Topcoder Staff", + administrator = "administrator", noRole = "no-role" } export declare const AUTH_USER_ROLE_VALUES: AUTH_USER_ROLE[]; diff --git a/types/src/lib/functions/user-profile.provider.d.ts b/types/src/lib/functions/user-profile.provider.d.ts index b5898c65..c972eb5e 100644 --- a/types/src/lib/functions/user-profile.provider.d.ts +++ b/types/src/lib/functions/user-profile.provider.d.ts @@ -3,6 +3,13 @@ import { AUTH_USER_ROLE } from '../config/auth'; export type fetchUserProfileFn = () => AuthUser | null; export declare const getJwtUserhandle: () => AuthUser["handle"] | undefined; export declare const getJwtUserRoles: () => AuthUser["roles"] | undefined; +/** + * Reads the current tcjwt cookie and returns the roles recognized by navigation guards. + * Administrator roles are matched case-insensitively and returned in their canonical form. + * Takes no parameters; unrelated roles are omitted from the returned array. + * @returns Recognized navigation roles, or an empty array when no roles are present. + * @throws If the tcjwt cookie cannot be decoded or contains invalid JSON. + */ export declare const getUserAppRoles: () => AuthUser["roles"]; export declare const checkUserAppRole: (role: AUTH_USER_ROLE) => boolean; /** From fbddaff0d7f6ccea4bb0f82ed2e888a575def28b Mon Sep 17 00:00:00 2001 From: jmgasper Date: Wed, 14 Oct 2026 11:03:02 +1100 Subject: [PATCH 3/3] fix: match app drawer icons to Figma (PM-6555) --- public/assets/tools/README.md | 12 ++++++++++-- public/assets/tools/archive.svg | 7 ++++++- public/assets/tools/mm.svg | 7 ++++++- public/assets/tools/sprite.svg | 12 +++++++++--- public/assets/tools/work-app.svg | 7 ++++++- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/public/assets/tools/README.md b/public/assets/tools/README.md index c6e5a4c3..b59f1dcd 100644 --- a/public/assets/tools/README.md +++ b/public/assets/tools/README.md @@ -17,7 +17,7 @@ eg: ``` ## App drawer icons -The app drawer icons are 32x32 tiles: a tinted rounded square (`rx="4"`) with a 24x24 glyph centered on it, colored per drawer group: +The app drawer icons are 32x32 tiles: a tinted rounded square (`rx="4"`) with a centered glyph, colored per drawer group. Most glyphs use a 24x24 canvas; the Figma exports listed below use their original 20x20 canvas, centered with `translate(6 6)`: | Groups | Tile | Glyph | | --- | --- | --- | @@ -29,5 +29,13 @@ The app drawer icons are 32x32 tiles: a tinted rounded square (`rx="4"`) with a | Sales & Customers | `#FCF4D6` | `#8E6A00` | | Team & Platform | `#D9FBFB` | `#007D79` | -Glyphs come from [Material Symbols Rounded](https://github.com/google/material-design-icons) (weight 600, Apache-2.0), except `copilot-portal`, `mm`, `sales-pipeline` and `discord`, which use [Tabler Icons](https://tabler.io/icons) (MIT). +`mm`, `archive` and `work-app` use the exact glyph paths and colors exported from the [PM-6555 Figma design](https://www.figma.com/design/AMGGzlrbi8PaMw6aQbbxuL/Marketing-Design-System?node-id=270-7650): + +| Icon | Figma item node | Glyph | +| --- | --- | --- | +| `mm` | `270:7703` | `icon/shoe` | +| `archive` | `270:7715` | `icon/files` | +| `work-app` | `270:7729` | `icon/tools` | + +The remaining glyphs come from [Material Symbols Rounded](https://github.com/google/material-design-icons) (weight 600, Apache-2.0), except `copilot-portal`, `sales-pipeline` and `discord`, which use [Tabler Icons](https://tabler.io/icons) (MIT). Each icon also exists as `./{file-name}.svg`; keep its `` in `./sprite.svg` in sync, and avoid inner `id`s so symbols don't clash inside the shared sprite. diff --git a/public/assets/tools/archive.svg b/public/assets/tools/archive.svg index a17d5f2e..d3e1ba73 100644 --- a/public/assets/tools/archive.svg +++ b/public/assets/tools/archive.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/public/assets/tools/mm.svg b/public/assets/tools/mm.svg index 7061701f..83797efe 100644 --- a/public/assets/tools/mm.svg +++ b/public/assets/tools/mm.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/public/assets/tools/sprite.svg b/public/assets/tools/sprite.svg index e8f0613d..8d032645 100644 --- a/public/assets/tools/sprite.svg +++ b/public/assets/tools/sprite.svg @@ -5,7 +5,9 @@ - + + + @@ -129,11 +131,15 @@ - + + + - + + + diff --git a/public/assets/tools/work-app.svg b/public/assets/tools/work-app.svg index 47f28069..ad09b706 100644 --- a/public/assets/tools/work-app.svg +++ b/public/assets/tools/work-app.svg @@ -1 +1,6 @@ - + + + + + +