- {`Created at ${formatDayShort(parseNaiveDate(inserted_at))}`}
+ {`Created at ${formatDayShort(dateForSite(inserted_at, site))}`}
{!showUpdatedAt && !!authorLabel && ` by ${authorLabel}`}
{showUpdatedAt && (
- {`Last updated at ${formatDayShort(parseNaiveDate(updated_at))}`}
+ {`Last updated at ${formatDayShort(dateForSite(updated_at, site))}`}
{!!authorLabel && ` by ${authorLabel}`}
)}
diff --git a/assets/js/dashboard/segments/segment-modals.test.tsx b/assets/js/dashboard/segments/segment-modals.test.tsx
new file mode 100644
index 0000000000..030f525556
--- /dev/null
+++ b/assets/js/dashboard/segments/segment-modals.test.tsx
@@ -0,0 +1,148 @@
+/** @format */
+
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import { SegmentModal } from './segment-modals'
+import { TestContextProviders } from '../../../test-utils/app-context-providers'
+import {
+ SavedSegment,
+ SavedSegments,
+ SegmentData,
+ SegmentType
+} from '../filtering/segments'
+import { Role, UserContextValue } from '../user-context'
+import { PlausibleSite } from '../site-context'
+
+beforeEach(() => {
+ const modalRoot = document.createElement('div')
+ modalRoot.id = 'modal_root'
+ document.body.appendChild(modalRoot)
+})
+
+const flags = { saved_segments: true, saved_segments_fe: true }
+
+describe('Segment details modal - errors', () => {
+ const anySiteSegment: SavedSegment & { segment_data: SegmentData } = {
+ id: 1,
+ type: SegmentType.site,
+ owner_id: 1,
+ owner_name: 'Test User',
+ name: 'Blog or About',
+ segment_data: {
+ filters: [['is', 'page', ['/blog', '/about']]],
+ labels: {}
+ },
+ inserted_at: '2025-03-13T13:00:00',
+ updated_at: '2025-03-13T16:00:00'
+ }
+
+ const anyPersonalSegment: SavedSegment & { segment_data: SegmentData } = {
+ ...anySiteSegment,
+ id: 2,
+ type: SegmentType.personal
+ }
+
+ const cases: {
+ case: string
+ segments: SavedSegments
+ segmentId: number
+ user: UserContextValue
+ message: string
+ siteOptions: Partial
+ }[] = [
+ {
+ case: 'segment is not in list',
+ segments: [anyPersonalSegment, anySiteSegment],
+ segmentId: 202020,
+ user: { loggedIn: true, id: 1, role: Role.owner },
+ message: `Segment not found with with ID "202020"`,
+ siteOptions: { flags, siteSegmentsAvailable: true }
+ },
+ {
+ case: 'site segment is in list but not listable because site segments are not available',
+ segments: [anyPersonalSegment, anySiteSegment],
+ segmentId: anySiteSegment.id,
+ user: { loggedIn: true, id: 1, role: Role.owner },
+ message: `Segment not found with with ID "${anySiteSegment.id}"`,
+ siteOptions: { flags, siteSegmentsAvailable: false }
+ },
+ {
+ case: 'personal segment is in list but not listable because it is a public dashboard',
+ segments: [{ ...anyPersonalSegment, owner_id: null, owner_name: null }],
+ segmentId: anyPersonalSegment.id,
+ user: { loggedIn: false, id: null, role: Role.public },
+ message: `Segment not found with with ID "${anyPersonalSegment.id}"`,
+ siteOptions: { flags, siteSegmentsAvailable: true }
+ },
+ {
+ case: 'segment is in list and listable, but detailed view is not available because user is not logged in',
+ segments: [{ ...anySiteSegment, owner_id: null, owner_name: null }],
+ segmentId: anySiteSegment.id,
+ user: { loggedIn: false, id: null, role: Role.public },
+ message: 'Not enough permissions to see segment details',
+ siteOptions: { flags, siteSegmentsAvailable: true }
+ }
+ ]
+ it.each(cases)(
+ 'shows error `$message` when $case',
+ ({ user, segments, segmentId, message, siteOptions }) => {
+ render(, {
+ wrapper: (props) => (
+
+ )
+ })
+
+ expect(screen.getByText(message)).toBeVisible()
+ expect(screen.queryByText(`Edit segment`)).not.toBeInTheDocument()
+ }
+ )
+})
+
+describe('Segment details modal - other cases', () => {
+ it('displays site segment correctly', () => {
+ const anySiteSegment: SavedSegment & { segment_data: SegmentData } = {
+ id: 100,
+ type: SegmentType.site,
+ owner_id: 100100,
+ owner_name: 'Test User',
+ name: 'Blog or About',
+ segment_data: {
+ filters: [['is', 'page', ['/blog', '/about']]],
+ labels: {}
+ },
+ inserted_at: '2025-03-13T13:00:00',
+ updated_at: '2025-03-13T16:00:00'
+ }
+
+ render(, {
+ wrapper: (props) => (
+
+ )
+ })
+ expect(screen.getByText(anySiteSegment.name)).toBeVisible()
+ expect(screen.getByText('Site segment')).toBeVisible()
+
+ expect(screen.getByText('Filters in segment')).toBeVisible()
+ expect(screen.getByTitle('Page is /blog or /about')).toBeVisible()
+
+ expect(
+ screen.getByText(`Last updated at 13 Mar by ${anySiteSegment.owner_name}`)
+ ).toBeVisible()
+ expect(screen.getByText(`Created at 13 Mar`)).toBeVisible()
+
+ expect(screen.getByText('Edit segment')).toBeVisible()
+ expect(screen.getByText('Remove filter')).toBeVisible()
+ })
+})
diff --git a/assets/js/dashboard/segments/segment-modals.tsx b/assets/js/dashboard/segments/segment-modals.tsx
index 822bf8ee6e..a1c372fb82 100644
--- a/assets/js/dashboard/segments/segment-modals.tsx
+++ b/assets/js/dashboard/segments/segment-modals.tsx
@@ -1,15 +1,16 @@
/** @format */
-import React, { ReactNode, useEffect, useState } from 'react'
+import React, { ReactNode, useState } from 'react'
import ModalWithRouting from '../stats/modals/modal'
import {
+ canSeeSegmentDetails,
+ isListableSegment,
isSegmentFilter,
SavedSegment,
SEGMENT_TYPE_LABELS,
SegmentData,
SegmentType
} from '../filtering/segments'
-import { useSegmentPrefetch } from '../nav-menu/segments/searchable-segments-section'
import { useQueryContext } from '../query-context'
import { AppNavigationLink } from '../navigation/use-app-navigate'
import { cleanLabels } from '../util/filters'
@@ -22,6 +23,9 @@ import { ExclamationTriangleIcon, TrashIcon } from '@heroicons/react/24/outline'
import { MutationStatus } from '@tanstack/react-query'
import { ApiError } from '../api'
import { ErrorPanel } from '../components/error-panel'
+import { useSegmentsContext } from '../filtering/segments-context'
+import { useSiteContext } from '../site-context'
+import { useUserContext } from '../user-context'
interface ApiRequestProps {
status: MutationStatus
@@ -427,15 +431,28 @@ const Placeholder = ({
)
export const SegmentModal = ({ id }: { id: SavedSegment['id'] }) => {
+ const site = useSiteContext()
+ const user = useUserContext()
const { query } = useQueryContext()
+ const { segments } = useSegmentsContext()
- const { data, fetchSegment, status, error } = useSegmentPrefetch({
- id: String(id)
- })
+ const segment = segments
+ .filter((s) => isListableSegment({ segment: s, site, user }))
+ .find((s) => String(s.id) === String(id))
- useEffect(() => {
- fetchSegment()
- }, [fetchSegment])
+ let error: ApiError | null = null
+
+ if (!segment) {
+ error = new ApiError(`Segment not found with with ID "${id}"`, {
+ error: `Segment not found with with ID "${id}"`
+ })
+ } else if (!canSeeSegmentDetails({ user })) {
+ error = new ApiError('Not enough permissions to see segment details', {
+ error: `Not enough permissions to see segment details`
+ })
+ }
+
+ const data = !error ? segment : null
return (
@@ -506,13 +523,6 @@ export const SegmentModal = ({ id }: { id: SavedSegment['id'] }) => {
>
)}
- {status === 'pending' && (
-