Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Saved Segments UI (variant D) #4891

Merged
merged 26 commits into from
Feb 26, 2025
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a75939c
WIP
apata Feb 12, 2025
46f4540
Load members
apata Feb 12, 2025
0a192cf
Assert that we know has_not_done will not work without changes
apata Feb 13, 2025
8b5c9bd
Add tests
apata Feb 13, 2025
60e654b
Assert that dates are in the expected format
apata Feb 13, 2025
7dd78ca
Add tests, better authorship dates, api.js -> api.ts
apata Feb 13, 2025
12099fe
Add error panels
apata Feb 13, 2025
ed1a59a
Flatten errors on the API side
apata Feb 13, 2025
44fb20f
Stop name copy from getting too long
apata Feb 17, 2025
317403b
Make comparison mode and edit segment modes exclusive
apata Feb 18, 2025
039dcc2
Fix flicker calculating space
apata Feb 18, 2025
b009a9a
Fix issue with definite state not persisting
apata Feb 18, 2025
a4e0b51
Unhitch modals from query-context
apata Feb 19, 2025
dfa441f
Separate API format and dashboard format of segment_data
apata Feb 19, 2025
180a16b
Clarify purpose of useDefiniteLocationState
apata Feb 19, 2025
99ebd02
Tweak UI: site switcher, save as segment
apata Feb 19, 2025
9955969
Fix issues with modals
apata Feb 19, 2025
1bb067e
Remove commented and unnecessary code, better query context
apata Feb 20, 2025
e619595
Fix too permissive site members dataset
apata Feb 20, 2025
2c3e8e6
Make sure Segment doesn't show up as an option to customer without th…
apata Feb 20, 2025
18c420f
Fix issue with 'See more' menu being present when it should not be
apata Feb 25, 2025
672ef5f
Permit :has_not_done filter in segments
apata Feb 25, 2025
8db3ce9
Refactor to matching on filter list structure
apata Feb 25, 2025
59c99f8
Flatten :and stemming from segment filters on first level
apata Feb 25, 2025
da74227
Update test
apata Feb 25, 2025
31d5574
Merge branch 'master' into saved-segments/variant-d
apata Feb 26, 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
11 changes: 9 additions & 2 deletions assets/js/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -57,8 +57,15 @@ if (container && container.dataset) {
<ThemeContextProvider>
<SiteContextProvider site={site}>
<UserContextProvider
role={container.dataset.currentUserRole as Role}
loggedIn={container.dataset.loggedIn === 'true'}
user={
container.dataset.loggedIn === 'true'
? {
loggedIn: true,
role: container.dataset.currentUserRole! as Role,
id: parseInt(container.dataset.currentUserId!, 10)
}
: { loggedIn: false, role: null, id: null }
}
>
<RouterProvider router={router} />
</UserContextProvider>
76 changes: 0 additions & 76 deletions assets/js/dashboard/api.js

This file was deleted.

134 changes: 134 additions & 0 deletions assets/js/dashboard/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/** @format */
import { DashboardQuery } from './query'
import { formatISO } from './util/date'
import { serializeApiFilters } from './util/filters'

let abortController = new AbortController()
let SHARED_LINK_AUTH: null | string = null

export class ApiError extends Error {
payload: unknown
constructor(message: string, payload: unknown) {
super(message)
this.name = 'ApiError'
this.payload = payload
}
}

function serialize(obj: Record<string, string | boolean | number>) {
const str: string[] = []
/* eslint-disable-next-line no-prototype-builtins */
for (const p in obj)
if (obj.hasOwnProperty(p)) {
str.push(`${encodeURIComponent(p)}=${encodeURIComponent(obj[p])}`)
}
return str.join('&')
}

export function setSharedLinkAuth(auth: string) {
SHARED_LINK_AUTH = auth
}

export function cancelAll() {
abortController.abort()
abortController = new AbortController()
}

export function serializeQuery(
query: DashboardQuery,
extraQuery: unknown[] = []
) {
const queryObj: Record<string, string> = {}
if (query.period) {
queryObj.period = query.period
}
if (query.date) {
queryObj.date = formatISO(query.date)
}
if (query.from) {
queryObj.from = formatISO(query.from)
}
if (query.to) {
queryObj.to = formatISO(query.to)
}
if (query.filters) {
queryObj.filters = serializeApiFilters(query.filters)
}
if (query.with_imported) {
queryObj.with_imported = String(query.with_imported)
}
if (SHARED_LINK_AUTH) {
queryObj.auth = SHARED_LINK_AUTH
}

if (query.comparison) {
queryObj.comparison = query.comparison
queryObj.compare_from = query.compare_from
? formatISO(query.compare_from)
: undefined
queryObj.compare_to = query.compare_to
? formatISO(query.compare_to)
: undefined
queryObj.match_day_of_week = String(query.match_day_of_week)
}

Object.assign(queryObj, ...extraQuery)

return '?' + serialize(queryObj)
}

function getHeaders(): Record<string, string> {
return SHARED_LINK_AUTH ? { 'X-Shared-Link-Auth': SHARED_LINK_AUTH } : {}
}

async function handleApiResponse(response: Response) {
const payload = await response.json()
if (!response.ok) {
throw new ApiError(payload.error, payload)
}

return payload
}

export async function get(
url: string,
query?: DashboardQuery,
...extraQuery: unknown[]
) {
const response = await fetch(
query ? url + serializeQuery(query, extraQuery) : url,
{
signal: abortController.signal,
headers: { ...getHeaders(), Accept: 'application/json' }
}
)
return handleApiResponse(response)
}

export const mutation = async <
TBody extends Record<string, unknown> = Record<string, unknown>
>(
url: string,
options:
| { body: TBody; method: 'PATCH' | 'PUT' | 'POST' }
| { method: 'DELETE' }
) => {
const fetchOptions =
options.method === 'DELETE'
? {}
: {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(options.body)
}
const response = await fetch(url, {
method: options.method,
headers: {
...getHeaders(),
...fetchOptions.headers,
Accept: 'application/json'
},
body: fetchOptions.body,
signal: abortController.signal
})
return handleApiResponse(response)
}
51 changes: 51 additions & 0 deletions assets/js/dashboard/components/error-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** @format */

import React from 'react'
import classNames from 'classnames'
import {
ArrowPathIcon,
ExclamationTriangleIcon,
XMarkIcon
} from '@heroicons/react/24/outline'

export const ErrorPanel = ({
errorMessage,
className,
onClose,
onRetry
}: {
errorMessage: string
className?: string
onClose?: () => void
onRetry?: () => void
}) => (
<div
className={classNames(
'flex gap-x-1 rounded bg-red-100 text-red-600 dark:bg-red-200 dark:text-red-800 p-4',
className
)}
>
<div className="mt-1 flex">
<ExclamationTriangleIcon className="block w-4 h-4 shrink-0" />
</div>
<div className="break-all text-sm/5">{errorMessage}</div>
{typeof onClose === 'function' && (
<button
className="flex ml-auto w-5 h-5 items-center justify-center hover:text-red-700 dark:hover:text-red-900"
onClick={onClose}
title="Close notice"
>
<XMarkIcon className="block w-4 h-4 shrink-0" />
</button>
)}
{typeof onRetry === 'function' && (
<button
className="flex ml-auto w-5 h-5 items-center justify-center hover:text-red-700 dark:hover:text-red-900"
onClick={onRetry}
title="Retry"
>
<ArrowPathIcon className="block w-4 h-4 shrink-0" />
</button>
)}
</div>
)
8 changes: 4 additions & 4 deletions assets/js/dashboard/components/notice.js
Original file line number Diff line number Diff line change
@@ -9,10 +9,10 @@ export function FeatureSetupNotice({ feature, title, info, callToAction, onHideA

const requestHideSection = () => {
if (window.confirm(`Are you sure you want to hide ${sectionTitle}? You can make it visible again in your site settings later.`)) {
api.put(`/api/${encodeURIComponent(site.domain)}/disable-feature`, { feature: feature })
.then(response => {
if (response.ok) { onHideAction() }
})
api.mutation(`/api/${encodeURIComponent(site.domain)}/disable-feature`, { method: 'PUT', body: { feature: feature } })
.then(() => onHideAction())
.catch((error) => {if (!(error instanceof api.ApiError)) {throw error}})

}
}

4 changes: 4 additions & 0 deletions assets/js/dashboard/components/popover.tsx
Original file line number Diff line number Diff line change
@@ -55,6 +55,10 @@ const items = {
roundedStartEnd: classNames(
'first-of-type:rounded-t-md',
'last-of-type:rounded-b-md'
),
groupRoundedStartEnd: classNames(
'group-first-of-type:rounded-t-md',
'group-last-of-type:rounded-b-md'
)
}
}
2 changes: 1 addition & 1 deletion assets/js/dashboard/components/search-input.tsx
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ export const SearchInput = ({
type="text"
placeholder={isFocused ? placeholderFocused : placeholderUnfocused}
className={classNames(
'shadow-sm dark:bg-gray-900 dark:text-gray-100 focus:ring-indigo-500 focus:border-indigo-500 block sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800 w-48',
'shadow-sm dark:bg-gray-900 dark:text-gray-100 focus:ring-indigo-500 focus:border-indigo-500 block border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800 w-48',
className
)}
onChange={debouncedOnSearchInputChange}
8 changes: 3 additions & 5 deletions assets/js/dashboard/dashboard-keybinds.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* @format */
import React from 'react'
import { NavigateKeybind } from './keybinding'
import { useRoutelessModalsContext } from './navigation/routeless-modals-context'

const ClearFiltersKeybind = () => (
<NavigateKeybind
@@ -21,9 +22,6 @@ const ClearFiltersKeybind = () => (
)

export function DashboardKeybinds() {
return (
<>
<ClearFiltersKeybind />
</>
)
const { modal } = useRoutelessModalsContext()
return modal === null && <ClearFiltersKeybind />
}
Loading

Unchanged files with check annotations Beta

if (currentTab == 'all' && isRemovingFilter('channel')) {
setTab('channels')()
}
}, [query, currentTab])

Check warning on line 185 in assets/js/dashboard/stats/sources/source-list.js

GitHub Actions / Build and test

React Hook useEffect has missing dependencies: 'previousQuery' and 'setTab'. Either include them or remove the dependency array
function setTab(tab) {
return () => {