import React, { ReactNode, useCallback, useState } from 'react' import ModalWithRouting from '../stats/modals/modal' import { canSeeSegmentDetails, isListableSegment, isSegmentFilter, SavedSegment, SEGMENT_TYPE_LABELS, SegmentData, SegmentType } from '../filtering/segments' import { useQueryContext } from '../query-context' import { AppNavigationLink } from '../navigation/use-app-navigate' import { cleanLabels } from '../util/filters' import { plainFilterText, styledFilterText } from '../util/filter-text' import { rootRoute } from '../router' import { FilterPillsList } from '../nav-menu/filter-pills-list' import classNames from 'classnames' import { SegmentAuthorship } from './segment-authorship' import { ExclamationTriangleIcon } 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 { Role, UserContextValue, useUserContext } from '../user-context' import { removeFilterButtonClassname } from '../components/remove-filter-button' interface ApiRequestProps { status: MutationStatus error?: unknown reset: () => void } interface SegmentModalProps { user: UserContextValue siteSegmentsAvailable: boolean onClose: () => void namePlaceholder: string } const primaryNeutralButtonClassName = 'button !px-3' const primaryNegativeButtonClassName = classNames( 'button !px-3.5', 'items-center !bg-red-500 dark:!bg-red-500 hover:!bg-red-600 dark:hover:!bg-red-700 whitespace-nowrap' ) const secondaryButtonClassName = classNames( 'button !px-3.5', 'border !border-gray-300 dark:!border-gray-700 !bg-white dark:!bg-gray-700 !text-gray-800 dark:!text-gray-100 hover:!text-gray-900 hover:!shadow-sm dark:hover:!bg-gray-600 dark:hover:!text-white' ) const SegmentActionModal = ({ children, onClose }: { children: ReactNode onClose: () => void }) => { return (
{children}
) } export const CreateSegmentModal = ({ segment, onClose, onSave, siteSegmentsAvailable: siteSegmentsAvailable, user, namePlaceholder, error, reset, status }: SegmentModalProps & ApiRequestProps & { segment?: SavedSegment onSave: (input: Pick) => void }) => { const defaultName = segment?.name ? `Copy of ${segment.name}`.slice(0, 255) : '' const [name, setName] = useState(defaultName) const defaultType = segment?.type === SegmentType.site && siteSegmentsAvailable && hasSiteSegmentPermission(user) ? SegmentType.site : SegmentType.personal const [type, setType] = useState(defaultType) const { disabled, disabledMessage, onSegmentTypeChange } = useSegmentTypeDisabledState({ siteSegmentsAvailable, user, setType }) return ( Create segment {disabled && } { const trimmedName = name.trim() const saveableName = trimmedName.length ? trimmedName : namePlaceholder onSave({ name: saveableName, type }) }} /> {error !== null && ( )} ) } export const DeleteSegmentModal = ({ onClose, onSave, segment, status, error, reset }: { onClose: () => void onSave: (input: Pick) => void segment: SavedSegment & { segment_data?: SegmentData } } & ApiRequestProps) => { return ( Delete {SEGMENT_TYPE_LABELS[segment.type].toLowerCase()} {` "${segment.name}"?`} {!!segment.segment_data && ( )} {error !== null && ( )} ) } const FormTitle = ({ children }: { children?: ReactNode }) => (

{children}

) const ButtonsRow = ({ className, children }: { className?: string children?: ReactNode }) => (
{children}
) const SegmentNameInput = ({ namePlaceholder, value, onChange }: { namePlaceholder: string value: string onChange: (value: string) => void }) => { return ( <> onChange(e.target.value)} placeholder={namePlaceholder} id="name" className="block px-3.5 py-2.5 w-full text-sm dark:text-gray-300 rounded-md border border-gray-300 dark:border-gray-750 dark:bg-gray-750 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500" /> ) } const SegmentTypeSelector = ({ value, onChange }: { value: SegmentType onChange: (value: SegmentType) => void }) => { const options = [ { type: SegmentType.personal, name: SEGMENT_TYPE_LABELS[SegmentType.personal], description: 'Visible only to you' }, { type: SegmentType.site, name: SEGMENT_TYPE_LABELS[SegmentType.site], description: 'Visible to others on the site' } ] return (
{options.map(({ type, name, description }) => (
onChange(type)} className="mt-px size-4.5 cursor-pointer text-indigo-600 dark:bg-transparent border-gray-400 dark:border-gray-600 checked:border-indigo-600 dark:checked:border-white" />
))}
) } const useSegmentTypeDisabledState = ({ siteSegmentsAvailable, user, setType }: { siteSegmentsAvailable: boolean user: UserContextValue setType: (type: SegmentType) => void }) => { const [disabled, setDisabled] = useState(false) const [disabledMessage, setDisabledMessage] = useState(null) const userIsOwner = user.role === Role.owner const canSelectSiteSegment = hasSiteSegmentPermission(user) const onSegmentTypeChange = useCallback( (type: SegmentType) => { setType(type) if (type === SegmentType.site && !canSelectSiteSegment) { setDisabled(true) setDisabledMessage( <> {"You don't have enough permissions to change segment to this type"} ) } else if (type === SegmentType.site && !siteSegmentsAvailable) { setDisabled(true) setDisabledMessage( <> To use this segment type, {userIsOwner ? ( please upgrade your subscription ) : ( <> please reach out to a team owner to upgrade their subscription. )} ) } else { setDisabled(false) setDisabledMessage(null) } }, [setType, siteSegmentsAvailable, userIsOwner, canSelectSiteSegment] ) return { disabled, disabledMessage, onSegmentTypeChange } } const SaveSegmentButton = ({ disabled, onSave }: { disabled: boolean onSave: () => void }) => { return ( ) } const SegmentTypeDisabledMessage = ({ message }: { message: ReactNode | null }) => { if (!message) return null return (
{message}
) } export const UpdateSegmentModal = ({ onClose, onSave, segment, siteSegmentsAvailable, user, namePlaceholder, status, error, reset }: SegmentModalProps & ApiRequestProps & { onSave: (input: Pick) => void segment: SavedSegment }) => { const [name, setName] = useState(segment.name) const [type, setType] = useState(segment.type) const { disabled, disabledMessage, onSegmentTypeChange } = useSegmentTypeDisabledState({ siteSegmentsAvailable, user, setType }) return ( Update segment {disabled && } { const trimmedName = name.trim() const saveableName = trimmedName.length ? trimmedName : namePlaceholder onSave({ id: segment.id, name: saveableName, type }) }} /> {error !== null && ( )} ) } const FiltersInSegment = ({ segment_data }: { segment_data: SegmentData }) => { return ( <>

Filters in segment

({ className: 'dark:!shadow-gray-950/60', plainText: plainFilterText({ labels: segment_data.labels }, filter), children: styledFilterText({ labels: segment_data.labels }, filter), interactive: false }))} />
) } const Placeholder = ({ children, placeholder }: { children: ReactNode | false placeholder: ReactNode }) => ( {children === false ? placeholder : children} ) const hasSiteSegmentPermission = (user: UserContextValue) => { return [Role.admin, Role.owner, Role.editor, 'super_admin'].includes( user.role ) } export const SegmentModal = ({ id }: { id: SavedSegment['id'] }) => { const site = useSiteContext() const user = useUserContext() const { query } = useQueryContext() const { segments } = useSegmentsContext() const segment = segments .filter((s) => isListableSegment({ segment: s, site, user })) .find((s) => String(s.id) === String(id)) 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 (

{data ? data.name : 'Segment details'}

{data?.segment_data ? SEGMENT_TYPE_LABELS[data.type] : false}
{!!data?.segment_data && ( <>
({ ...s, filters: data.segment_data.filters, labels: data.segment_data.labels })} state={{ expandedSegment: data }} > Edit segment { const nonSegmentFilters = query.filters.filter( (f) => !isSegmentFilter(f) ) return { ...s, filters: nonSegmentFilters, labels: cleanLabels( nonSegmentFilters, query.labels, 'segment', {} ) } }} > Remove filter
)} {error !== null && ( window.location.reload()} /> )}
) }