Skip to content

Conversation

@Ur-imazing
Copy link
Contributor

@Ur-imazing Ur-imazing commented Dec 7, 2025

Summary by CodeRabbit

  • New Features

    • Added local and global template creation options with separate "Make Template" and "Make Global Template" actions.
    • Introduced tabbed interface separating Team Projects and Team Templates for better organization.
    • Added status-based filtering (Active, Archived, Trashed) for journeys and templates with enhanced management workflows.
  • Improvements

    • Enhanced permission handling to distinguish between local and global template publishing.

✏️ Tip: You can customize this high-level summary in your review settings.

@Ur-imazing Ur-imazing self-assigned this Dec 7, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 7, 2025

Walkthrough

Introduces distinction between local and global templates via teamId filtering and permission checks. Local templates belong to user teams, while global templates belong to the 'jfp-team'. Updates query variables, permission logic, UI rendering, and refactors the journey list component structure with new JourneyListView and JourneyListContent components.

Changes

Cohort / File(s) Summary
Backend Template & Permission Logic
apis/api-journeys/src/app/modules/journey/journey.resolver.ts, journey.resolver.spec.ts
Distinguishes local vs global templates via isLocalTemplate and isGlobalTemplate checks. Updates permission enforcement to block template updates only for global templates. Adjusts duplicate journey logic to set template flag based on template origin. Adds comprehensive test coverage for local and global template scenarios.
Admin Pages & Routing
apps/journeys-admin/pages/index.tsx, publisher/[journeyId].tsx, publisher/index.tsx, templates/[journeyId].tsx, templates/index.tsx
Adds conditional onboarding panel rendering based on content type. Updates publisher template access logic to handle global vs non-global templates. Introduces teamId: 'jfp-team' filter to template queries. Changes query variables initialization to include template and team scoping.
Journey List Refactoring
apps/journeys-admin/src/components/JourneyList/JourneyList.tsx, JourneyList.spec.tsx, JourneyListMenu/*, JourneyListView/*
Replaces dynamic per-tab imports with new JourneyListView component and renderList callback. Adds JourneyListView to manage tab switching between journeys and templates with status filtering. Updates routing from query.tab to query.status and query.type. Introduces new content type and status abstractions.
Journey List Content
apps/journeys-admin/src/components/JourneyList/JourneyListContent/*
Introduces new JourneyListContent component with comprehensive GraphQL mutations for archive, trash, restore, and delete operations. Adds test utilities and mocks covering active, archived, and trashed states for both journeys and templates. Handles bulk actions with owner filtering and snackbar notifications.
Template Creation UI
apps/journeys-admin/src/components/Editor/Toolbar/Items/CreateTemplateItem/CreateTemplateItem.tsx, CreateTemplateItem.spec.tsx, Menu.tsx, Menu.spec.tsx
Adds globalPublish prop to distinguish between local and global template creation. Implements separate UI text and icons for "Make Template" vs "Make Global Template". Updates mutation variables to include appropriate teamId. Refactors tests to cover both creation modes.
Journey Card & Menu
apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DefaultMenu/DefaultMenu.tsx, DefaultMenu.spec.tsx
Adds conditional rendering of CreateTemplateItem for non-template journeys. Renders global variant only when user has publisher role. Updates tests to validate template menu item visibility based on user role.
Template List Components
apps/journeys-admin/src/components/TemplateList/ActiveTemplateList/*, ArchivedTemplateList/*, TrashedTemplateList/*
Adds teamId: 'jfp-team' filter to all template queries across active, archived, and trashed states to scope results to global templates.
GraphQL Query & Cache
apps/journeys-admin/src/libs/useAdminJourneysQuery/useAdminJourneysQuery.ts, cache.ts, useJourneyCreateMutation/useJourneyCreateMutation.spec.tsx
Extends GET_ADMIN_JOURNEYS query to select team.id, journeyCustomizationDescription, and journeyCustomizationFields. Adds cache key policy for adminJourneys with keyArgs including template and teamId. Updates cache update logic to conditionally skip adding template journeys.
Template UI Components
libs/journeys/ui/src/components/TemplateView/CreateJourneyButton/*, UseThisTemplateButton/*, TemplateActionButton/*, TemplateView/*
Adds variant prop ('button' | 'menu-item') to template-related components. Implements conditional rendering and behavior for menu-item variant. Updates sign-in logic and prefetch behavior based on variant. Propagates variant through component hierarchy.
Template Query Filters
libs/journeys/ui/src/components/TemplateSections/TemplateSections.tsx, TemplateView.tsx, and specs
Adds teamId: 'jfp-team' to journey queries in template-related components to scope results to global templates.
Journey Duplication
libs/journeys/ui/src/libs/useJourneyDuplicateMutation/useJourneyDuplicateMutation.ts
Extends JOURNEY_DUPLICATE mutation to return template field. Updates cache modify logic to conditionally exclude template journeys from adminJourneys cache entries.
Icons
libs/shared/ui/src/components/icons/Layout1.tsx, LayoutTop.tsx, Icon.tsx, icon.stories.tsx
Introduces two new SVG icon components (Layout1 and LayoutTop). Registers icons in Icon component's name union and icon map.
Localization
libs/locales/en/apps-journeys-admin.json
Renames "Create Template" to distinguish "Make Template" and "Make Global Template". Adds team-based terminology ("Team Projects", "Team Templates"). Introduces extensive status/action messaging for template and journey lifecycle operations (archive, trash, restore, delete). Adds contextual empty state and helper messages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring special attention:

  • Backend template permission logic (journey.resolver.ts): Review the isGlobalTemplate and isLocalTemplate checks to ensure they correctly enforce permissions and handle edge cases around teamId validation.
  • JourneyListView/JourneyListContent refactoring: Verify the new component hierarchy, renderList callback pattern, and state synchronization with router queries (status, type) function correctly across tabs.
  • Variant prop propagation: Trace the variant prop through CreateJourneyButton, UseThisTemplateButton, TemplateActionButton, and related components to ensure consistent behavior and proper conditional rendering.
  • Query variable and cache key changes: Confirm that adding teamId and template filters to queries and cache keyArgs produces expected caching behavior and doesn't cause stale data issues.
  • CreateTemplateItem globalPublish logic: Validate that teamId is correctly set based on the globalPublish prop and that mutation variables are properly passed through the Apollo update function.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: regenerate interface files for local template changes' accurately describes the main change—regenerating interface files (likely GraphQL-generated types) as a result of modifications enabling local template functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 25-08-UC-fix-run-codegen-for-local-template-resolver-edit

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

Fails
🚫 Please request a reviewer for this PR.
Warnings
⚠️ ❗ Big PR (3856 changes)

(change count - 3856): Pull Request size seems relatively large. If Pull Request contains multiple changes, split each into separate PR will helps faster, easier review.

Generated by 🚫 dangerJS against a686243

@nx-cloud
Copy link

nx-cloud bot commented Dec 7, 2025

View your CI Pipeline Execution ↗ for commit a686243

Command Status Duration Result
nx run watch-modern-e2e:e2e ✅ Succeeded 4s View ↗
nx run journeys-admin-e2e:e2e ✅ Succeeded 1m 17s View ↗
nx run journeys-e2e:e2e ✅ Succeeded 27s View ↗
nx run resources-e2e:e2e ✅ Succeeded 13s View ↗
nx run watch-e2e:e2e ✅ Succeeded 13s View ↗
nx run videos-admin-e2e:e2e ✅ Succeeded 4s View ↗
nx run-many --target=vercel-alias --projects=jo... ✅ Succeeded 2s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 11s View ↗
Additional runs (16) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2025-12-07 22:39:14 UTC

@github-actions github-actions bot temporarily deployed to Preview - watch-modern December 7, 2025 22:28 Inactive
@github-actions github-actions bot temporarily deployed to Preview - journeys-admin December 7, 2025 22:28 Inactive
@github-actions github-actions bot temporarily deployed to Preview - resources December 7, 2025 22:28 Inactive
@github-actions github-actions bot temporarily deployed to Preview - journeys December 7, 2025 22:28 Inactive
@github-actions github-actions bot temporarily deployed to Preview - videos-admin December 7, 2025 22:28 Inactive
@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch-modern ✅ Ready watch-modern preview Mon Dec 8 11:30:18 NZDT 2025

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys ✅ Ready journeys preview Mon Dec 8 11:30:49 NZDT 2025

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
videos-admin ✅ Ready videos-admin preview Mon Dec 8 11:31:34 NZDT 2025

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch ✅ Ready watch preview Mon Dec 8 11:31:58 NZDT 2025

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
resources ✅ Ready resources preview Mon Dec 8 11:32:09 NZDT 2025

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys-admin ✅ Ready journeys-admin preview Mon Dec 8 11:32:31 NZDT 2025

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/journeys/ui/src/components/TemplateView/UseThisTemplateButton/UseThisTemplateButton.tsx (1)

37-45: Enforce sign-in check logic for menu-item variant.

The condition on line 38 assumes variant='menu-item' always indicates a signed-in user, but this assumption is not enforced. The logic if (variant === 'menu-item' || signedIn) bypasses the account dialog based on variant alone, which could cause issues if menu-item is used in other contexts in the future.

Either make the signedIn prop required when variant='menu-item' (via TypeScript constraints or explicit validation), or require callers to pass signedIn explicitly rather than relying on the variant to determine sign-in status.

♻️ Duplicate comments (3)
apps/journeys-admin/src/components/TemplateList/TrashedTemplateList/TrashedTemplateList.tsx (1)

42-43: Same hardcoded team ID issue.

The 'jfp-team' string literal is repeated here. See the previous comment about extracting this into a shared constant.

apps/journeys-admin/pages/templates/[journeyId].tsx (1)

159-160: Same hardcoded team ID in SSR context.

The 'jfp-team' string is hardcoded here in the server-side props generation. Extracting this to a shared constant would be beneficial, especially since this needs to stay in sync with client-side queries.

apps/journeys-admin/src/components/TemplateList/ArchivedTemplateList/ArchivedTemplateList.tsx (1)

42-43: Same hardcoded team ID issue.

The 'jfp-team' string literal is repeated here. See earlier comments about extracting this into a shared constant.

🧹 Nitpick comments (24)
apps/journeys-admin/pages/index.tsx (2)

37-41: Normalize router.query.type instead of asserting, and extract a type alias

The logic is correct, but the type assertion masks the actual Next.js router type (string | string[] | undefined):

const currentContentType =
  (router?.query?.type as 'journeys' | 'templates') ?? 'journeys'

Two suggestions:

  1. Introduce an explicit type alias (per TS guidelines) and reuse it:
type ContentType = 'journeys' | 'templates'
  1. Normalize the query param instead of asserting, so unexpected values or arrays fall back safely:
type ContentType = 'journeys' | 'templates'

const typeParam = router.query.type
const currentContentType: ContentType =
  typeParam === 'templates' ? 'templates' : 'journeys'

const showSidePanel = currentContentType === 'journeys'

This keeps runtime behavior the same for valid values, handles string[]/invalid cases, and removes the unchecked cast. As per coding guidelines, defining ContentType tightens typing here and for any related components.


60-75: Conditional side panel rendering looks good; consider minor React/TS idioms

The conditional rendering of the side panel and title is clear and matches the intent to only show onboarding on the journeys tab:

sidePanelChildren={showSidePanel ? <OnboardingPanel /> : undefined}
sidePanelTitle={
  showSidePanel ? (
    <>
      <Typography variant="subtitle1">{t('Create a New Journey')}</Typography>
      <HelpScoutBeacon userInfo={{ name: user?.displayName ?? '', email: user?.email ?? '' }} />
    </>
  ) : undefined
}

Two optional refinements:

  1. Use null instead of undefined for JSX conditionals, which is a bit more idiomatic for ReactNode props:
sidePanelChildren={showSidePanel ? <OnboardingPanel /> : null}
sidePanelTitle={
  showSidePanel ? (
    <>
      {/* … */}
    </>
  ) : null
}
  1. Optionally hoist userInfo into a const to avoid recreating the object inline every render and make the prop clearer, e.g.:
const userInfo = {
  name: user?.displayName ?? '',
  email: user?.email ?? ''
}

// …

<HelpScoutBeacon userInfo={userInfo} />

These are style/readability tweaks; behavior is already correct.

libs/journeys/ui/src/components/TemplateSections/TemplateSections.tsx (1)

32-45: Team scoping looks correct; please confirm this component is only used for global templates

Adding teamId: 'jfp-team' to the where filter aligns this component with the new team-scoped template behavior and matches the updated mocks/type policies elsewhere, so the query shape looks consistent.

Two small follow‑ups to consider:

  1. If TemplateSections is ever used outside the “global templates” context, this will now silently filter out non‑jfp-team templates. It’s worth double‑checking call sites to ensure that’s intended for all usages, or that local‑template flows use a different component/path.
  2. Since 'jfp-team' is now used across multiple queries in the PR, consider centralizing it in a shared constant (e.g., GLOBAL_TEMPLATES_TEAM_ID) to avoid future drift if the value changes. Optional but improves maintainability.

If you want, I can provide a small rg/ast-grep script to enumerate all TemplateSections usages and verify they’re only in global-template contexts.

libs/journeys/ui/src/components/TemplateSections/TemplateSections.spec.tsx (1)

168-249: Mocks correctly updated to match new team‑scoped query variables

The three mocks now include teamId: 'jfp-team' in where, which keeps them aligned with the updated TemplateSections query and ensures tests will fail if the component ever drops or changes that argument. This is a good way to implicitly enforce the new contract.

As a minor follow‑up, you might consider sharing a common GLOBAL_TEMPLATES_TEAM_ID constant between the component and these specs to avoid hard‑coding the same literal in multiple places, but that’s optional.

apps/journeys-admin/src/components/JourneyList/JourneyListMenu/JourneyListMenu.tsx (1)

43-51: Add aria-label for accessibility.

Per coding guidelines, interactive elements should include accessibility attributes. The IconButton is missing an aria-label to describe its purpose for screen readers.

          <IconButton
            edge="end"
            color="inherit"
            sx={{ mx: 3 }}
            onClick={handleShowMenu}
            data-testid="JourneyListMenuButton"
+           aria-label={t('Journey list menu')}
          >
            <MoreIcon data-testid="MoreIcon" />
          </IconButton>
apps/journeys-admin/src/components/JourneyList/JourneyListMenu/JourneyListMenu.spec.tsx (1)

112-120: Misleading test names in 'trashed' describe block.

The test names don't match the functionality being tested:

  • Line 112: 'should call archiveAllActive' tests restoreAllTrashed
  • Line 122: 'should call trashAllArchived' tests deleteAllTrashed

Consider renaming for clarity:

-    it('should call archiveAllActive', () => {
+    it('should call restoreAllTrashed', () => {
-    it('should call trashAllArchived', () => {
+    it('should call deleteAllTrashed', () => {

Also applies to: 122-130

apps/journeys-admin/src/libs/useJourneyCreateMutation/useJourneyCreateMutation.spec.tsx (1)

145-160: Cache assertion handles dynamic keyArgs correctly, but fallback is weak.

The prefix-based search for adminJourneys entries correctly accommodates the dynamic cache keys generated by keyArgs. However, the fallback assertion (lines 156-158) only verifies the journey exists in cache, not that it was properly added to an adminJourneys entry.

If the fallback branch executes, it may mask a cache update failure. Consider whether this should fail explicitly:

      } else {
-       // Fallback: check if journey exists in cache (cache.modify updates all entries)
-       expect(cache.extract()?.['Journey:createdJourneyId']).toBeDefined()
+       // If no adminJourneys entry exists, the cache update didn't work as expected
+       fail('Expected adminJourneys cache entry to exist')
      }

Alternatively, if the fallback is intentional for certain test scenarios, add a comment explaining when it's expected to trigger.

apis/api-journeys/src/app/modules/journey/journey.resolver.ts (2)

1038-1046: Consider reordering the null check before accessing journey properties.

The isGlobalTemplate check on line 1038 accesses journey?.team?.id before the null check for journey on line 1039. While optional chaining prevents a runtime error, it's cleaner to check for existence first:

+    if (journey == null)
+      throw new GraphQLError('journey not found', {
+        extensions: { code: 'NOT_FOUND' }
+      })
     const isGlobalTemplate = journey?.team?.id === 'jfp-team'
-    if (journey == null)
-      throw new GraphQLError('journey not found', {
-        extensions: { code: 'NOT_FOUND' }
-      })
     if (
       isGlobalTemplate &&
       ability.cannot(Action.Manage, subject('Journey', journey), 'template')
     )

584-584: Consider extracting 'jfp-team' to a constant.

The magic string 'jfp-team' is used here and in journeyTemplate (line 1038). Extracting it to a shared constant would improve maintainability and reduce the risk of typos.

+const GLOBAL_TEMPLATE_TEAM_ID = 'jfp-team'
+
 // In journeyDuplicate:
-    const isLocalTemplate = journey.teamId !== 'jfp-team' && journey.template
+    const isLocalTemplate = journey.teamId !== GLOBAL_TEMPLATE_TEAM_ID && journey.template

 // In journeyTemplate:
-    const isGlobalTemplate = journey?.team?.id === 'jfp-team'
+    const isGlobalTemplate = journey?.team?.id === GLOBAL_TEMPLATE_TEAM_ID
apps/journeys-admin/src/components/Editor/Toolbar/Items/CreateTemplateItem/CreateTemplateItem.tsx (1)

57-57: Consider extracting the hardcoded team ID.

The 'jfp-team' string is hardcoded here and in the resolver. Consider importing from a shared constants file to ensure consistency:

// In a shared constants file:
export const GLOBAL_TEMPLATE_TEAM_ID = 'jfp-team'
apps/journeys-admin/src/components/TemplateList/ActiveTemplateList/ActiveTemplateList.tsx (1)

42-43: Same hardcoded team ID concern.

Consider using a shared constant for 'jfp-team' to maintain consistency with the resolver and CreateTemplateItem component.

libs/journeys/ui/src/components/TemplateView/TemplateView.tsx (1)

46-47: Consider extracting the hardcoded team ID into a named constant.

The string literal 'jfp-team' is repeated across multiple files in this PR. Extracting it into a shared constant (e.g., GLOBAL_TEMPLATES_TEAM_ID) would improve maintainability and make the intent clearer.

Example:

// In a shared constants file
export const GLOBAL_TEMPLATES_TEAM_ID = 'jfp-team'

// In this file
teamId: GLOBAL_TEMPLATES_TEAM_ID
apps/journeys-admin/pages/templates/index.tsx (1)

143-144: Consider extracting magic constants.

Both '529' (English language ID) and 'jfp-team' are hardcoded string literals. Consider extracting these to named constants for better maintainability and clarity.

Example:

const ENGLISH_LANGUAGE_ID = '529'
const GLOBAL_TEMPLATES_TEAM_ID = 'jfp-team'

// In the query
languageIds: [ENGLISH_LANGUAGE_ID],
teamId: GLOBAL_TEMPLATES_TEAM_ID
apps/journeys-admin/src/libs/useAdminJourneysQuery/useAdminJourneysQuery.spec.tsx (1)

110-126: Avoid as any casting in test assertions.

The test assertions use (journey as any) to access the new fields, which bypasses TypeScript's type checking. This can hide type mismatches and make refactoring more difficult.

Consider one of these approaches:

Option 1: Update the GraphQL type definitions

expect(hookResult.current.data?.journeys).toBeDefined()
expect(hookResult.current.data?.journeys?.length).toBeGreaterThan(0)
hookResult.current.data?.journeys?.forEach((journey) => {
  expect(journey.team?.id).toBe('team-id')
  expect(journey.journeyCustomizationDescription).toBe(
    'Customize this journey'
  )
  expect(journey.journeyCustomizationFields).toEqual([
    {
      id: 'journey-customization-field-id',
      journeyId: 'journey.id',
      key: 'key',
      value: 'value',
      defaultValue: 'defaultValue'
    }
  ])
})

Option 2: If the types are intentionally not in the query, add proper type guards or define an explicit test type

type JourneyWithCustomization = GetAdminJourneys['journeys'][0] & {
  team: { id: string }
  journeyCustomizationDescription: string
  journeyCustomizationFields: Array<{...}>
}

const journey = hookResult.current.data?.journeys?.[0] as JourneyWithCustomization

This makes the type extension explicit and documents the additional fields being tested.

apps/journeys-admin/pages/publisher/[journeyId].tsx (1)

48-86: Template gating logic looks correct; consider load-state handling as an optional polish

The isGlobalTemplate calculation and the new (isPublisher || !isGlobalTemplate) / (isGlobalTemplate && !isPublisher) conditions match the local vs global template behavior you’re introducing and look logically sound.

One minor side effect: before the queries resolve, both isPublisher and isGlobalTemplate are undefined, so the editor branch evaluates truthy (!isGlobalTemplate) and briefly renders until data arrives (at which point global, non‑publisher users will flip to the access‑denied view). If that flicker is undesirable, you could gate rendering on the query loading states or on data?.publisherTemplate / publisherData being defined.

Otherwise, this change aligns with the new global/local template semantics.

apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DefaultMenu/DefaultMenu.tsx (1)

8-9: Menu integrations for template actions look good; watch for double Divider when no active team

The new wiring for template actions makes sense:

  • Non‑template journeys: Duplicate/Translate, then a dedicated section for “Make Template” plus “Make Global Template” when isPublisher === true via CreateTemplateItem with globalPublish set appropriately.
  • Template journeys: TemplateActionButton rendered as a menu-item, consistent with the TemplateView behavior.

One minor UI nit: when template !== true and activeTeam == null, the Divider at line 217 still renders, the Duplicate/Translate block is skipped, and the non‑template block at line 231 adds another Divider, so you end up with two separators back‑to‑back. If there’s a realistic path where activeTeam is unset, you may want to conditionally render one of those Dividers or fold them into the same block.

If that edge case doesn’t occur in practice, the current implementation is fine.

Also applies to: 34-35, 218-241

libs/journeys/ui/src/components/TemplateView/TemplateViewHeader/TemplateActionButton/TemplateActionButton.spec.tsx (1)

2-5: Expanded TemplateActionButton tests are solid and map well to behavior

The router mocking, SnackbarProvider/JourneyProvider wrapping, and the new scenarios (menu-item vs button, signed in/out, customizable vs non-customizable, navigation and dialog flows) give this component excellent coverage and closely match the implementation semantics.

If you find yourself adding more cases later, you might consider a small renderWithProviders helper to DRY up the repeated render tree, but that’s purely optional.

Also applies to: 16-22, 30-38, 44-143, 145-272

apps/journeys-admin/src/components/JourneyList/JourneyList.tsx (1)

66-73: Consider simplifying nested ternary for readability.

The nested ternary expression for activeTab is hard to follow. Consider using a helper function or a more readable approach.

-  // Determine active tab from router query (support both old 'tab' and new 'status' params)
-  const activeTab =
-    (router?.query?.status as JourneyStatusFilter) ??
-    (router?.query?.tab?.toString() === 'archived'
-      ? 'archived'
-      : router?.query?.tab?.toString() === 'trashed'
-        ? 'trashed'
-        : 'active')
+  // Determine active tab from router query (support both old 'tab' and new 'status' params)
+  const getActiveTab = (): JourneyStatusFilter => {
+    if (router?.query?.status) {
+      return router.query.status as JourneyStatusFilter
+    }
+    const tab = router?.query?.tab?.toString()
+    if (tab === 'archived') return 'archived'
+    if (tab === 'trashed') return 'trashed'
+    return 'active'
+  }
+  const activeTab = getActiveTab()
apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx (3)

25-26: Consider renaming local JourneyStatus type to avoid shadowing.

The local type JourneyStatus could be confused with the GraphQL JourneyStatus enum from globalTypes. The index barrel already re-exports it as JourneyStatusFilter, so consider using that name here as well for consistency.

 export type ContentType = 'journeys' | 'templates'
-export type JourneyStatus = 'active' | 'archived' | 'trashed'
+export type JourneyStatusFilter = 'active' | 'archived' | 'trashed'

Then update all internal usages of JourneyStatus to JourneyStatusFilter within this file.


114-139: Move contentTypeOptions outside component or memoize to prevent unnecessary effect runs.

contentTypeOptions is recreated on every render and is included in the dependency array, causing the useEffect to run more frequently than necessary.

Move the options outside the component (they only depend on t for translations, but the values are static):

+const CONTENT_TYPE_OPTIONS: Omit<ContentTypeOption, 'displayValue'>[] = [
+  { queryParam: 'journeys', tabIndex: 0 },
+  { queryParam: 'templates', tabIndex: 1 }
+]
+
 export function JourneyListView({
   ...
 }: JourneyListViewProps): ReactElement {
   const { t } = useTranslation('apps-journeys-admin')
   ...
-  // Content type options (Journeys, Templates)
-  const contentTypeOptions: ContentTypeOption[] = [
-    {
-      queryParam: 'journeys',
-      displayValue: t('Team Projects'),
-      tabIndex: 0
-    },
-    {
-      queryParam: 'templates',
-      displayValue: t('Team Templates'),
-      tabIndex: 1
-    }
-  ]
+  const contentTypeOptions = useMemo(
+    () =>
+      CONTENT_TYPE_OPTIONS.map((opt) => ({
+        ...opt,
+        displayValue:
+          opt.queryParam === 'journeys' ? t('Team Projects') : t('Team Templates')
+      })),
+    [t]
+  )

Then update the useEffect dependency array to only include contentTypeOptions (now stable).


227-273: Consider moving controls outside of Tabs for better semantics.

Placing Box elements with FormControl, JourneySort, and JourneyListMenu as direct children of Tabs is unconventional. While MUI allows this, it may affect accessibility and tab navigation. Consider wrapping the tabs row in a Box with display: flex and placing controls alongside the Tabs component.

-      <Tabs
-        value={activeContentTypeTab}
-        onChange={handleContentTypeChange}
-        aria-label="journey content type tabs"
-        ...
-      >
-        <Tab ... />
-        <Tab ... />
-        {/* Status filter dropdown - visible for both tabs */}
-        <Box ...>
-          <FormControl ...>...</FormControl>
-        </Box>
-        ...
-      </Tabs>
+      <Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
+        <Tabs
+          value={activeContentTypeTab}
+          onChange={handleContentTypeChange}
+          aria-label="journey content type tabs"
+          ...
+        >
+          <Tab ... />
+          <Tab ... />
+        </Tabs>
+        <Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 2 }}>
+          <FormControl ...>...</FormControl>
+          <JourneySort ... />
+          <JourneyListMenu ... />
+        </Box>
+      </Box>
libs/journeys/ui/src/components/TemplateView/CreateJourneyButton/CreateJourneyButton.tsx (1)

237-263: Consider adding aria-label to MenuItem for accessibility.

The Button variant has accessibility attributes, but the MenuItem could benefit from an explicit aria-label for screen readers, per coding guidelines.

         <MenuItem
           onClick={handleCheckSignIn}
           data-testid="CreateJourneyMenuItem"
+          aria-label={t('Use This Template')}
         >
apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx (2)

110-143: Memoize getQueryParams to avoid recalculation on each render.

getQueryParams() is called in multiple places (line 145, 255) and recalculates the same values. Since it depends only on contentType and status, consider using useMemo.

-  // Determine query parameters based on contentType and status
-  const getQueryParams = () => {
+  // Determine query parameters based on contentType and status
+  const queryParams = useMemo(() => {
     const isTemplate = contentType === 'templates'
     const baseParams: {
       status: JourneyStatus[]
       template?: boolean
       useLastActiveTeamId?: boolean
     } = {
       status: []
     }
     // ... rest of switch logic
     return baseParams
-  }
+  }, [contentType, status])

-  const { data, refetch } = useAdminJourneysQuery(getQueryParams())
+  const { data, refetch } = useAdminJourneysQuery(queryParams)

Then update getOwnerFilteredIds to use queryParams.useLastActiveTeamId instead of getQueryParams().useLastActiveTeamId.


576-580: Remove redundant key prop on JourneyCard.

The parent Grid already has key={journey.id}. The key on JourneyCard is redundant.

                     <JourneyCard
-                      key={journey.id}
                       journey={journey}
                       refetch={refetch}
                     />

Comment on lines +586 to +596
<Box
sx={{
display: 'flex',
flexDirection: 'column',
pt: status === 'active' ? 30 : 30
}}
>
<Typography variant="subtitle1" align="center" gutterBottom>
{getEmptyStateMessage()}
</Typography>
</Box>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant ternary expression.

The ternary status === 'active' ? 30 : 30 always evaluates to 30. Simplify to just 30.

                 <Box
                   sx={{
                     display: 'flex',
                     flexDirection: 'column',
-                    pt: status === 'active' ? 30 : 30
+                    pt: 30
                   }}
                 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Box
sx={{
display: 'flex',
flexDirection: 'column',
pt: status === 'active' ? 30 : 30
}}
>
<Typography variant="subtitle1" align="center" gutterBottom>
{getEmptyStateMessage()}
</Typography>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
pt: 30
}}
>
<Typography variant="subtitle1" align="center" gutterBottom>
{getEmptyStateMessage()}
</Typography>
</Box>
🤖 Prompt for AI Agents
In
apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx
around lines 586 to 596, the Box sx prop uses a redundant ternary pt: status ===
'active' ? 30 : 30; replace the ternary with the constant value by setting pt:
30 directly to simplify the code and remove the unnecessary conditional.

onClose={() => setOpenAccountDialog(false)}
/>
)}
{openTeamDialog != null && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant null check on boolean state.

openTeamDialog is a boolean state initialized to false, so openTeamDialog != null is always true. This condition should likely just check if the dialog is open, or the condition may be a remnant from previous code.

-      {openTeamDialog != null && (
+      {openTeamDialog && (
         <DynamicCopyToTeamDialog
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{openTeamDialog != null && (
{openTeamDialog && (
<DynamicCopyToTeamDialog
🤖 Prompt for AI Agents
In
libs/journeys/ui/src/components/TemplateView/CreateJourneyButton/CreateJourneyButton.tsx
around line 275, the condition "openTeamDialog != null" is redundant because
openTeamDialog is a boolean initialized to false; replace the null-check with a
boolean check (e.g., "openTeamDialog && ( ... )") so the dialog is only rendered
when openTeamDialog is true, or remove the conditional entirely if the intent
was to always render the element.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 5, 2026

This pull request has been marked stale due to 28 days without activity. It will be closed in 14 days unless updated.

@github-actions github-actions bot added the stale label Jan 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants