Skip to content

Commit 9383307

Browse files
committed
Allow creating and editing shared links with limited views
1 parent 50f9771 commit 9383307

File tree

11 files changed

+160
-61
lines changed

11 files changed

+160
-61
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ defmodule Plausible.Site.SharedLink do
1010
field :slug, :string
1111
field :password_hash, :string
1212
field :password, :string, virtual: true
13-
field :limited_to_segment_id, :integer
13+
field :limited_to_segment_id, :integer, default: nil
14+
has_one :limited_to_segment, Plausible.Segments.Segment
1415

1516
timestamps()
1617
end
1718

1819
def changeset(link, attrs \\ %{}, opts \\ []) do
1920
link
20-
|> cast(attrs, [:slug, :password, :name])
21+
|> cast(attrs, [:slug, :password, :name, :limited_to_segment_id])
2122
|> validate_required([:slug, :name])
2223
|> validate_special_name(opts)
2324
|> unique_constraint(:slug)

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+
limited_to_segment_id = Keyword.get(opts, :limited_to_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, limited_to_segment_id: limited_to_segment_id},
427429
Keyword.take(opts, [:skip_special_name_check?])
428430
)
429431
|> Repo.insert()

lib/plausible_web/controllers/stats_controller.ex

Lines changed: 2 additions & 1 deletion
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? ->

lib/plausible_web/live/shared_link_settings.ex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ defmodule PlausibleWeb.Live.SharedLinkSettings do
133133
</:tooltip_content>
134134
<Heroicons.lock_open class="feather ml-2 mb-0.5" />
135135
</.tooltip>
136-
<.tooltip enabled?={true} centered?={true}>
136+
<.tooltip :if={link.limited_to_segment_id} enabled?={true} centered?={true}>
137137
<:tooltip_content>
138138
Limited view
139139
</:tooltip_content>
@@ -177,6 +177,21 @@ defmodule PlausibleWeb.Live.SharedLinkSettings do
177177

178178
def handle_event("edit-shared-link", %{"slug" => slug}, socket) do
179179
shared_link = Plausible.Repo.get_by(Plausible.Site.SharedLink, slug: slug)
180+
segment_id = shared_link.limited_to_segment_id
181+
182+
shared_link =
183+
if is_nil(segment_id) do
184+
shared_link
185+
else
186+
{:ok, segments} =
187+
Plausible.Segments.get_many(socket.assigns.site, [segment_id], fields: [:id, :name])
188+
189+
segment =
190+
segments
191+
|> Enum.at(0, %{id: segment_id, name: "Unknown segment"})
192+
193+
Map.put(shared_link, :limited_to_segment, segment)
194+
end
180195

181196
socket =
182197
socket |> assign(form_shared_link: shared_link) |> Modal.open("shared-links-form-modal")

0 commit comments

Comments
 (0)