Skip to content

Commit 4a027b8

Browse files
committed
Allow creating and editing shared links with limited views
1 parent 2621219 commit 4a027b8

File tree

14 files changed

+173
-64
lines changed

14 files changed

+173
-64
lines changed

assets/js/dashboard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
SomethingWentWrongMessage
2020
} from './dashboard/error/something-went-wrong'
2121
import {
22+
parseLimitedToSegmentId,
2223
parsePreloadedSegments,
2324
SegmentsContextProvider
2425
} from './dashboard/filtering/segments-context'
@@ -83,6 +84,7 @@ if (container && container.dataset) {
8384
}
8485
>
8586
<SegmentsContextProvider
87+
limitedToSegmentId={parseLimitedToSegmentId(container.dataset)}
8688
preloadedSegments={parsePreloadedSegments(container.dataset)}
8789
>
8890
<RouterProvider router={router} />

assets/js/dashboard/filtering/segments-context.test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('SegmentsContext functions', () => {
3535
test('deleteOne works', () => {
3636
render(
3737
<SegmentsContextProvider
38+
limitedToSegmentId={null}
3839
preloadedSegments={[segmentOpenSource, segmentAPAC]}
3940
>
4041
<TestComponent />
@@ -51,7 +52,10 @@ describe('SegmentsContext functions', () => {
5152

5253
test('addOne adds to head of list', async () => {
5354
render(
54-
<SegmentsContextProvider preloadedSegments={[segmentAPAC]}>
55+
<SegmentsContextProvider
56+
limitedToSegmentId={null}
57+
preloadedSegments={[segmentAPAC]}
58+
>
5559
<TestComponent />
5660
</SegmentsContextProvider>
5761
)
@@ -68,6 +72,7 @@ describe('SegmentsContext functions', () => {
6872
test('updateOne works: updated segment is at head of list', () => {
6973
render(
7074
<SegmentsContextProvider
75+
limitedToSegmentId={null}
7176
preloadedSegments={[segmentOpenSource, segmentAPAC]}
7277
>
7378
<TestComponent />

assets/js/dashboard/filtering/segments-context.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,23 @@ export function parsePreloadedSegments(dataset: DOMStringMap): SavedSegments {
1717
return JSON.parse(dataset.segments!).map(handleSegmentResponse)
1818
}
1919

20+
export function parseLimitedToSegmentId(dataset: DOMStringMap): number | null {
21+
return JSON.parse(dataset.limitedToSegmentId!)
22+
}
23+
2024
type ChangeSegmentState = (
2125
segment: (SavedSegment | SavedSegmentPublic) & { segment_data: SegmentData }
2226
) => void
2327

2428
const initialValue: {
2529
segments: SavedSegments
30+
limitedToSegmentId: number | null
2631
updateOne: ChangeSegmentState
2732
addOne: ChangeSegmentState
2833
removeOne: ChangeSegmentState
2934
} = {
3035
segments: [],
36+
limitedToSegmentId: null,
3137
updateOne: () => {},
3238
addOne: () => {},
3339
removeOne: () => {}
@@ -41,9 +47,11 @@ export const useSegmentsContext = () => {
4147

4248
export const SegmentsContextProvider = ({
4349
preloadedSegments,
50+
limitedToSegmentId,
4451
children
4552
}: {
4653
preloadedSegments: SavedSegments
54+
limitedToSegmentId: number | null
4755
children: ReactNode
4856
}) => {
4957
const [segments, setSegments] = useState(preloadedSegments)
@@ -73,7 +81,13 @@ export const SegmentsContextProvider = ({
7381

7482
return (
7583
<SegmentsContext.Provider
76-
value={{ segments, removeOne, updateOne, addOne }}
84+
value={{
85+
segments,
86+
limitedToSegmentId: limitedToSegmentId ?? null,
87+
removeOne,
88+
updateOne,
89+
addOne
90+
}}
7791
>
7892
{children}
7993
</SegmentsContext.Provider>

assets/js/dashboard/nav-menu/filter-menu.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { popover, BlurMenuButtonOnEscape } from '../components/popover'
1111
import classNames from 'classnames'
1212
import { AppNavigationLink } from '../navigation/use-app-navigate'
1313
import { SearchableSegmentsSection } from './segments/searchable-segments-section'
14+
import { useSegmentsContext } from '../filtering/segments-context'
1415

1516
export function getFilterListItems({
1617
propsAvailable
@@ -49,6 +50,7 @@ const FilterMenuItems = ({ closeDropdown }: { closeDropdown: () => void }) => {
4950
const columns = useMemo(() => getFilterListItems(site), [site])
5051
const buttonRef = useRef<HTMLButtonElement>(null)
5152
const panelRef = useRef<HTMLDivElement>(null)
53+
const { limitedToSegmentId } = useSegmentsContext()
5254

5355
return (
5456
<>
@@ -107,10 +109,12 @@ const FilterMenuItems = ({ closeDropdown }: { closeDropdown: () => void }) => {
107109
</div>
108110
))}
109111
</div>
110-
<SearchableSegmentsSection
111-
closeList={closeDropdown}
112-
tooltipContainerRef={panelRef}
113-
/>
112+
{limitedToSegmentId === null && (
113+
<SearchableSegmentsSection
114+
closeList={closeDropdown}
115+
tooltipContainerRef={panelRef}
116+
/>
117+
)}
114118
</Popover.Panel>
115119
</Transition>
116120
</>

assets/test-utils/app-context-providers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const TestContextProviders = ({
7878
}
7979
}
8080
>
81-
<SegmentsContextProvider preloadedSegments={preloaded?.segments ?? []}>
81+
<SegmentsContextProvider limitedToSegmentId={null} preloadedSegments={preloaded?.segments ?? []}>
8282
<MemoryRouter
8383
basename={getRouterBasepath(site)}
8484
initialEntries={defaultInitialEntries}

lib/plausible/segments/segments.ex

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,63 @@ defmodule Plausible.Segments do
7070
{:ok, Repo.all(query)}
7171
end
7272

73+
def search_by_name(%Plausible.Site{} = site, name, opts) do
74+
type = Keyword.fetch!(opts, :type)
75+
fields = Keyword.get(opts, :fields, [:id, :name])
76+
77+
name_empty? = is_nil(name) or (is_binary(name) and String.trim(name) == "")
78+
79+
base_query =
80+
from(segment in Segment,
81+
where: segment.site_id == ^site.id,
82+
where: segment.type == ^type,
83+
limit: 20
84+
)
85+
86+
query =
87+
if name_empty? do
88+
from([segment] in base_query,
89+
select: ^fields,
90+
order_by: [desc: segment.updated_at]
91+
)
92+
else
93+
from([segment] in base_query,
94+
select: %{
95+
id: segment.id,
96+
name: segment.name,
97+
match_rank:
98+
fragment(
99+
"CASE
100+
WHEN lower(?) = lower(?) THEN 0 -- exact match
101+
WHEN lower(?) LIKE lower(?) || '%' THEN 1 -- starts with
102+
WHEN lower(?) LIKE '% ' || lower(?) || '%' THEN 2 -- after a space
103+
WHEN lower(?) LIKE ? THEN 3 -- anywhere
104+
END AS match_rank",
105+
segment.name,
106+
^name,
107+
segment.name,
108+
^name,
109+
segment.name,
110+
^name,
111+
segment.name,
112+
^"%name%"
113+
),
114+
pos:
115+
fragment(
116+
"position(lower(?) IN lower(?)) AS pos",
117+
segment.name,
118+
^name
119+
),
120+
len_diff: fragment("abs(length(?) - length(?)) AS len_diff", segment.name, ^name)
121+
},
122+
where: fragment("? ilike ?", segment.name, ^"%#{name}%"),
123+
order_by: fragment("match_rank asc, pos asc, len_diff asc, updated_at desc")
124+
)
125+
end
126+
127+
{:ok, Repo.all(query)}
128+
end
129+
73130
@spec get_one(pos_integer(), Plausible.Site.t(), atom(), pos_integer() | nil) ::
74131
{:ok, Segment.t()}
75132
| error_not_enough_permissions()

lib/plausible/site/shared_link.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ defmodule Plausible.Site.SharedLink do
1010
field :slug, :string
1111
field :password_hash, :string
1212
field :password, :string, virtual: true
13+
belongs_to :segment, Plausible.Segments.Segment
1314

1415
timestamps()
1516
end
1617

1718
def changeset(link, attrs \\ %{}, opts \\ []) do
1819
link
19-
|> cast(attrs, [:slug, :password, :name])
20+
|> cast(attrs, [:slug, :password, :name, :segment_id])
2021
|> validate_required([:slug, :name])
2122
|> validate_special_name(opts)
2223
|> unique_constraint(:slug)
2324
|> unique_constraint(:name, name: :shared_links_site_id_name_index)
25+
|> foreign_key_constraint(:segment_id)
2426
|> hash_password()
2527
end
2628

lib/plausible/sites.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ defmodule Plausible.Sites do
415415

416416
def create_shared_link(site, name, opts \\ []) do
417417
password = Keyword.get(opts, :password)
418+
segment_id = Keyword.get(opts, :segment_id)
419+
418420
site = Plausible.Repo.preload(site, :team)
419421
skip_feature_check? = Keyword.get(opts, :skip_feature_check?, false)
420422

@@ -423,7 +425,7 @@ defmodule Plausible.Sites do
423425
else
424426
%SharedLink{site_id: site.id, slug: Nanoid.generate()}
425427
|> SharedLink.changeset(
426-
%{name: name, password: password},
428+
%{name: name, password: password, segment_id: segment_id},
427429
Keyword.take(opts, [:skip_special_name_check?])
428430
)
429431
|> Repo.insert()

lib/plausible_web/components/generic.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,9 @@ defmodule PlausibleWeb.Components.Generic do
838838
<td
839839
class={[
840840
@height,
841-
"text-sm px-6 py-4 first:pl-0 last:pr-0 whitespace-nowrap overflow-visible",
841+
"text-sm px-6 py-4 first:pl-0 last:pr-0 whitespace-nowrap",
842+
# allow tooltips overflow cells vertically
843+
"overflow-visible",
842844
@truncate && "truncate",
843845
@max_width,
844846
@actions && "flex text-right justify-end",

lib/plausible_web/controllers/stats_controller.ex

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ defmodule PlausibleWeb.StatsController do
100100
hide_footer?: if(ce?() || demo, do: false, else: site_role != :public),
101101
consolidated_view?: consolidated_view?,
102102
consolidated_view_available?: consolidated_view_available?,
103-
team_identifier: team_identifier
103+
team_identifier: team_identifier,
104+
limited_to_segment_id: nil
104105
)
105106

106107
!stats_start_date && can_see_stats? ->
@@ -396,12 +397,28 @@ defmodule PlausibleWeb.StatsController do
396397
not Teams.locked?(shared_link.site.team) ->
397398
current_user = conn.assigns[:current_user]
398399
site_role = get_fallback_site_role(conn)
399-
shared_link = Plausible.Repo.preload(shared_link, site: :owners)
400+
shared_link = Plausible.Repo.preload(shared_link, site: [:owners], segment: [])
400401
stats_start_date = Plausible.Sites.stats_start_date(shared_link.site)
401402

402403
flags = get_flags(current_user, shared_link.site)
403-
404-
{:ok, segments} = Plausible.Segments.get_all_for_site(shared_link.site, site_role)
404+
limited_to_segment_id = shared_link.segment && shared_link.segment.id
405+
406+
segments =
407+
if is_nil(limited_to_segment_id) do
408+
{:ok, segments} = Plausible.Segments.get_all_for_site(shared_link.site, site_role)
409+
segments
410+
else
411+
shared_link.segment
412+
|> Map.take([
413+
:id,
414+
:name,
415+
:type,
416+
:inserted_at,
417+
:updated_at,
418+
:segment_data
419+
])
420+
|> List.wrap()
421+
end
405422

406423
embedded? = conn.params["embed"] == "true"
407424

@@ -435,7 +452,8 @@ defmodule PlausibleWeb.StatsController do
435452
# no shared links for consolidated views
436453
consolidated_view?: false,
437454
consolidated_view_available?: false,
438-
team_identifier: team_identifier
455+
team_identifier: team_identifier,
456+
limited_to_segment_id: limited_to_segment_id
439457
)
440458
end
441459
end

0 commit comments

Comments
 (0)