Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7f161e1
Add :limited_to_segment_id field to shared_links schema
apata Dec 1, 2025
924e77e
Refactor column name, add FK, index, and on delete cascade
apata Dec 2, 2025
0fb3035
Format
apata Dec 2, 2025
2621219
Add static UI for adding a segment to a shared link
sanne-san Nov 4, 2025
4a027b8
Allow creating and editing shared links with limited views
apata Dec 2, 2025
1adcfd6
Clarify search function arg names
apata Dec 3, 2025
32652d5
Unify function to set segment filter
apata Dec 3, 2025
befc3b7
Enforce limited view segment on dashboard
apata Dec 3, 2025
d2e406b
Clarify type
apata Dec 3, 2025
973dd76
WIP
apata Dec 4, 2025
426a571
Merge remote-tracking branch 'origin/master' into limited-view/base-ui
apata Dec 4, 2025
cb66768
Disable clear filter button for limited view segment
apata Dec 4, 2025
5ec4cac
Load related shared links WIP
apata Dec 4, 2025
8ba440c
Add warning about shared links WIP
apata Dec 4, 2025
2bb3dea
Merge
apata Dec 8, 2025
2f36339
Finalize delete modal
apata Dec 8, 2025
889476d
Unify shared link password protection checks
apata Dec 8, 2025
aa55aaf
WIP
apata Dec 8, 2025
c9cfd00
Give random name for shared links in tests
apata Dec 8, 2025
e2a8144
Add tests for related shared links endpoint
apata Dec 8, 2025
21591f4
Split components and remove nesting
apata Dec 9, 2025
b537bb2
Change limited view copy
apata Dec 9, 2025
283b6b6
Fix search ranking, make segment search input DRYer, add search test
apata Dec 9, 2025
219d8f6
Fix url search params test
apata Dec 9, 2025
aee220e
Update changelog
apata Dec 9, 2025
2f68df8
Fix invalid accessor
apata Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.

### Added

- Shared Links can now be limited to a particular segment of the data

### Removed

### Changed
Expand Down
16 changes: 13 additions & 3 deletions assets/js/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createAppRouter } from './dashboard/router'
import ErrorBoundary from './dashboard/error/error-boundary'
import * as api from './dashboard/api'
import * as timer from './dashboard/util/realtime-update-timer'
import { redirectForLegacyParams } from './dashboard/util/url-search-params'
import { maybeDoFERedirect } from './dashboard/util/url-search-params'
import SiteContextProvider, {
parseSiteFromDataset
} from './dashboard/site-context'
Expand All @@ -19,6 +19,8 @@ import {
SomethingWentWrongMessage
} from './dashboard/error/something-went-wrong'
import {
getLimitedToSegment,
parseLimitedToSegmentId,
parsePreloadedSegments,
SegmentsContextProvider
} from './dashboard/filtering/segments-context'
Expand All @@ -39,8 +41,15 @@ if (container && container.dataset) {
api.setSharedLinkAuth(sharedLinkAuth)
}

const limitedToSegmentId = parseLimitedToSegmentId(container.dataset)
const preloadedSegments = parsePreloadedSegments(container.dataset)
const limitedToSegment = getLimitedToSegment(
limitedToSegmentId,
preloadedSegments
)

try {
redirectForLegacyParams(window.location, window.history)
maybeDoFERedirect(window.location, window.history, limitedToSegment)
} catch (e) {
console.error('Error redirecting in a backwards compatible way', e)
}
Expand Down Expand Up @@ -83,7 +92,8 @@ if (container && container.dataset) {
}
>
<SegmentsContextProvider
preloadedSegments={parsePreloadedSegments(container.dataset)}
limitedToSegment={limitedToSegment}
preloadedSegments={preloadedSegments}
>
<RouterProvider router={router} />
</SegmentsContextProvider>
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/components/error-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { ReactNode } from 'react'
import classNames from 'classnames'
import {
ArrowPathIcon,
Expand All @@ -12,7 +12,7 @@ export const ErrorPanel = ({
onClose,
onRetry
}: {
errorMessage: string
errorMessage: ReactNode
className?: string
onClose?: () => void
onRetry?: () => void
Expand Down
7 changes: 6 additions & 1 deletion assets/js/dashboard/filtering/segments-context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('SegmentsContext functions', () => {
test('deleteOne works', () => {
render(
<SegmentsContextProvider
limitedToSegment={null}
preloadedSegments={[segmentOpenSource, segmentAPAC]}
>
<TestComponent />
Expand All @@ -51,7 +52,10 @@ describe('SegmentsContext functions', () => {

test('addOne adds to head of list', async () => {
render(
<SegmentsContextProvider preloadedSegments={[segmentAPAC]}>
<SegmentsContextProvider
limitedToSegment={null}
preloadedSegments={[segmentAPAC]}
>
<TestComponent />
</SegmentsContextProvider>
)
Expand All @@ -68,6 +72,7 @@ describe('SegmentsContext functions', () => {
test('updateOne works: updated segment is at head of list', () => {
render(
<SegmentsContextProvider
limitedToSegment={null}
preloadedSegments={[segmentOpenSource, segmentAPAC]}
>
<TestComponent />
Expand Down
26 changes: 25 additions & 1 deletion assets/js/dashboard/filtering/segments-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,33 @@ export function parsePreloadedSegments(dataset: DOMStringMap): SavedSegments {
return JSON.parse(dataset.segments!).map(handleSegmentResponse)
}

export function parseLimitedToSegmentId(dataset: DOMStringMap): number | null {
return JSON.parse(dataset.limitedToSegmentId!)
}

export function getLimitedToSegment(
limitedToSegmentId: number | null,
preloadedSegments: SavedSegments
): Pick<SavedSegment, 'id' | 'name'> | null {
if (limitedToSegmentId !== null) {
return preloadedSegments.find((s) => s.id === limitedToSegmentId) ?? null
}
return null
}

type ChangeSegmentState = (
segment: (SavedSegment | SavedSegmentPublic) & { segment_data: SegmentData }
) => void

const initialValue: {
segments: SavedSegments
limitedToSegment: Pick<SavedSegment, 'id' | 'name'> | null
updateOne: ChangeSegmentState
addOne: ChangeSegmentState
removeOne: ChangeSegmentState
} = {
segments: [],
limitedToSegment: null,
updateOne: () => {},
addOne: () => {},
removeOne: () => {}
Expand All @@ -41,9 +57,11 @@ export const useSegmentsContext = () => {

export const SegmentsContextProvider = ({
preloadedSegments,
limitedToSegment,
children
}: {
preloadedSegments: SavedSegments
limitedToSegment: Pick<SavedSegment, 'id' | 'name'> | null
children: ReactNode
}) => {
const [segments, setSegments] = useState(preloadedSegments)
Expand Down Expand Up @@ -73,7 +91,13 @@ export const SegmentsContextProvider = ({

return (
<SegmentsContext.Provider
value={{ segments, removeOne, updateOne, addOne }}
value={{
segments,
limitedToSegment,
removeOne,
updateOne,
addOne
}}
>
{children}
</SegmentsContext.Provider>
Expand Down
64 changes: 58 additions & 6 deletions assets/js/dashboard/filtering/segments.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { remapToApiFilters } from '../util/filters'
import {
formatSegmentIdAsLabelKey,
getSearchToApplySingleSegmentFilter,
getSearchToSetSegmentFilter,
getSegmentNamePlaceholder,
isSegmentIdLabelKey,
parseApiSegmentData,
Expand Down Expand Up @@ -64,9 +64,57 @@ describe(`${parseApiSegmentData.name}`, () => {
})
})

describe(`${getSearchToApplySingleSegmentFilter.name}`, () => {
test('generated search function applies single segment correctly', () => {
const searchFunction = getSearchToApplySingleSegmentFilter({
describe(`${getSearchToSetSegmentFilter.name}`, () => {
test('generated search function omits other filters segment correctly', () => {
const searchFunction = getSearchToSetSegmentFilter(
{
name: 'APAC',
id: 500
},
{ omitAllOtherFilters: true }
)
const existingSearch = {
date: '2025-02-10',
filters: [
['is', 'country', ['US']],
['is', 'page', ['/blog']]
],
labels: { US: 'United States' }
}
expect(searchFunction(existingSearch)).toEqual({
date: '2025-02-10',
filters: [['is', 'segment', [500]]],
labels: { 'segment-500': 'APAC' }
})
})

test('generated search function replaces existing segment filter correctly', () => {
const searchFunction = getSearchToSetSegmentFilter({
name: 'APAC',
id: 500
})
const existingSearch = {
date: '2025-02-10',
filters: [
['is', 'segment', [100]],
['is', 'country', ['US']],
['is', 'page', ['/blog']]
],
labels: { US: 'United States', 'segment-100': 'Scandinavia' }
}
expect(searchFunction(existingSearch)).toEqual({
date: '2025-02-10',
filters: [
['is', 'segment', [500]],
['is', 'country', ['US']],
['is', 'page', ['/blog']]
],
labels: { US: 'United States', 'segment-500': 'APAC' }
})
})

test('generated search function sets new segment filter correctly', () => {
const searchFunction = getSearchToSetSegmentFilter({
name: 'APAC',
id: 500
})
Expand All @@ -80,8 +128,12 @@ describe(`${getSearchToApplySingleSegmentFilter.name}`, () => {
}
expect(searchFunction(existingSearch)).toEqual({
date: '2025-02-10',
filters: [['is', 'segment', [500]]],
labels: { 'segment-500': 'APAC' }
filters: [
['is', 'segment', [500]],
['is', 'country', ['US']],
['is', 'page', ['/blog']]
],
labels: { US: 'United States', 'segment-500': 'APAC' }
})
})
})
Expand Down
53 changes: 47 additions & 6 deletions assets/js/dashboard/filtering/segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,44 @@ export const parseApiSegmentData = ({
...rest
})

export function getSearchToApplySingleSegmentFilter(
segment: Pick<SavedSegment, 'id' | 'name'>
export function getSearchToRemoveSegmentFilter(): Required<AppNavigationTarget>['search'] {
return (searchRecord) => {
const updatedFilters = (
(Array.isArray(searchRecord.filters)
? searchRecord.filters
: []) as Filter[]
).filter((f) => !isSegmentFilter(f))
const currentLabels = searchRecord.labels ?? {}
return {
...searchRecord,
filters: updatedFilters,
labels: cleanLabels(updatedFilters, currentLabels)
}
}
}

export function getSearchToSetSegmentFilter(
segment: Pick<SavedSegment, 'id' | 'name'>,
options: { omitAllOtherFilters?: boolean } = {}
): Required<AppNavigationTarget>['search'] {
return (search) => {
const filters = [['is', 'segment', [segment.id]]]
const labels = cleanLabels(filters, {}, 'segment', {
return (searchRecord) => {
const otherFilters = (
(Array.isArray(searchRecord.filters)
? searchRecord.filters
: []) as Filter[]
).filter((f) => !isSegmentFilter(f))
const currentLabels = searchRecord.labels ?? {}

const filters = [
['is', 'segment', [segment.id]],
...(options.omitAllOtherFilters ? [] : otherFilters)
]

const labels = cleanLabels(filters, currentLabels, 'segment', {
[formatSegmentIdAsLabelKey(segment.id)]: segment.name
})
return {
...search,
...searchRecord,
filters,
labels
}
Expand Down Expand Up @@ -177,6 +205,19 @@ export function canSeeSegmentDetails({ user }: { user: UserContextValue }) {
return user.loggedIn && user.role !== Role.public
}

export function canRemoveFilter(
filter: Filter,
limitedToSegment: Pick<SavedSegment, 'id' | 'name'> | null
) {
if (isSegmentFilter(filter) && limitedToSegment) {
const [_operation, _dimension, clauses] = filter
return (
clauses.length === 1 && String(limitedToSegment.id) === String(clauses[1])
)
}
return true
}

export function findAppliedSegmentFilter({ filters }: { filters: Filter[] }) {
const segmentFilter = filters.find(isSegmentFilter)
if (!segmentFilter) {
Expand Down
12 changes: 8 additions & 4 deletions assets/js/dashboard/nav-menu/filter-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { popover, BlurMenuButtonOnEscape } from '../components/popover'
import classNames from 'classnames'
import { AppNavigationLink } from '../navigation/use-app-navigate'
import { SearchableSegmentsSection } from './segments/searchable-segments-section'
import { useSegmentsContext } from '../filtering/segments-context'

export function getFilterListItems({
propsAvailable
Expand Down Expand Up @@ -49,6 +50,7 @@ const FilterMenuItems = ({ closeDropdown }: { closeDropdown: () => void }) => {
const columns = useMemo(() => getFilterListItems(site), [site])
const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const { limitedToSegment } = useSegmentsContext()

return (
<>
Expand Down Expand Up @@ -107,10 +109,12 @@ const FilterMenuItems = ({ closeDropdown }: { closeDropdown: () => void }) => {
</div>
))}
</div>
<SearchableSegmentsSection
closeList={closeDropdown}
tooltipContainerRef={panelRef}
/>
{limitedToSegment === null && (
<SearchableSegmentsSection
closeList={closeDropdown}
tooltipContainerRef={panelRef}
/>
)}
</Popover.Panel>
</Transition>
</>
Expand Down
29 changes: 17 additions & 12 deletions assets/js/dashboard/nav-menu/filter-pills-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { styledFilterText, plainFilterText } from '../util/filter-text'
import { useAppNavigate } from '../navigation/use-app-navigate'
import classNames from 'classnames'
import { filterRoute } from '../router'
import { canRemoveFilter } from '../filtering/segments'
import { useSegmentsContext } from '../filtering/segments-context'

export const PILL_X_GAP_PX = 16
export const PILL_Y_GAP_PX = 8
Expand Down Expand Up @@ -48,6 +50,7 @@ export const AppliedFilterPillsList = React.forwardRef<
AppliedFilterPillsListProps
>(({ className, style, slice, direction, pillClassName }, ref) => {
const { query } = useQueryContext()
const { limitedToSegment } = useSegmentsContext()
const navigate = useAppNavigate()

const renderableFilters =
Expand Down Expand Up @@ -82,19 +85,21 @@ export const AppliedFilterPillsList = React.forwardRef<
]
}
},
onRemoveClick: () => {
const newFilters = query.filters.filter(
(_, i) => i !== index + indexAdjustment
)
onRemoveClick: canRemoveFilter(filter, limitedToSegment)
? () => {
const newFilters = query.filters.filter(
(_, i) => i !== index + indexAdjustment
)

navigate({
search: (search) => ({
...search,
filters: newFilters,
labels: cleanLabels(newFilters, query.labels)
})
})
}
navigate({
search: (searchRecord) => ({
...searchRecord,
filters: newFilters,
labels: cleanLabels(newFilters, query.labels)
})
})
}
: undefined
}
}))}
className={className}
Expand Down
Loading
Loading