diff --git a/assets/js/dashboard/components/form-elements.tsx b/assets/js/dashboard/components/form-elements.tsx new file mode 100644 index 000000000000..99dd69528fe3 --- /dev/null +++ b/assets/js/dashboard/components/form-elements.tsx @@ -0,0 +1,117 @@ +import React, { ReactNode } from 'react' +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' + +export const LabeledTextInput = ({ + label, + id, + value, + onChange, + placeholder +}: { + label: string + id: string + value: string + onChange: (value: string) => void + placeholder: string +}) => { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + id={id} + 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" + /> +
+ ) +} + +export const TypeSelector = ({ + value, + onChange, + options, + idPrefix +}: { + value: T + onChange: (value: T) => void + options: { type: T; name: string; description: string }[] + idPrefix: string +}) => { + 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" + /> + +
+
+ ))} +
+ ) +} + +export const TypeDisabledMessage = ({ + message +}: { + message: ReactNode | null +}) => { + if (!message) return null + + return ( +
+ +
{message}
+
+ ) +} + +/** Keep this component styled the same as checkboxes in PlausibleWeb.Live.Installation.Instructions */ +export const Checkbox = ({ + id, + checked, + onChange, + children +}: React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement +>) => { + return ( + + ) +} diff --git a/assets/js/dashboard/components/modal-layout.tsx b/assets/js/dashboard/components/modal-layout.tsx index 107d4ae04a71..8a321b2265f6 100644 --- a/assets/js/dashboard/components/modal-layout.tsx +++ b/assets/js/dashboard/components/modal-layout.tsx @@ -1,6 +1,7 @@ import React, { ReactNode } from 'react' import { XMarkIcon } from '@heroicons/react/20/solid' import ModalWithRouting from '../stats/modals/modal' +import { Button } from './button' export function ModalLayout({ title, @@ -44,3 +45,21 @@ export function ModalLayout({ export function ModalFooter({ children }: { children: ReactNode }) { return
{children}
} + +export function SaveButton({ + disabled, + onSave +}: { + disabled: boolean + onSave: () => void +}) { + return ( + + ) +} diff --git a/assets/js/dashboard/components/placeholder.tsx b/assets/js/dashboard/components/placeholder.tsx new file mode 100644 index 000000000000..89a409d15c6b --- /dev/null +++ b/assets/js/dashboard/components/placeholder.tsx @@ -0,0 +1,20 @@ +import classNames from 'classnames' +import React, { ReactNode } from 'react' + +export const Placeholder = ({ + children, + placeholder +}: { + children: ReactNode | false + placeholder: ReactNode +}) => ( + + {children === false ? placeholder : children} + +) diff --git a/assets/js/dashboard/segments/segment-modals.tsx b/assets/js/dashboard/segments/segment-modals.tsx index 424e7e4145b2..14caccd83b03 100644 --- a/assets/js/dashboard/segments/segment-modals.tsx +++ b/assets/js/dashboard/segments/segment-modals.tsx @@ -1,5 +1,9 @@ import React, { ReactNode, useCallback, useState } from 'react' -import { ModalLayout, ModalFooter } from '../components/modal-layout' +import { + ModalLayout, + ModalFooter, + SaveButton +} from '../components/modal-layout' import { canRemoveFilter, getSearchToRemoveSegmentFilter, @@ -18,7 +22,6 @@ 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, useQuery } from '@tanstack/react-query' import { ApiError, get } from '../api' import { ErrorPanel } from '../components/error-panel' @@ -26,9 +29,34 @@ import { useSegmentsContext } from '../filtering/segments-context' import { Role, UserContextValue, useUserContext } from '../user-context' import { useSiteContext } from '../site-context' import { Button, buttonClassName } from '../components/button' +import { + Checkbox, + LabeledTextInput, + TypeDisabledMessage, + TypeSelector +} from '../components/form-elements' +import { Placeholder } from '../components/placeholder' const inModalSectionLabelClassName = 'text-sm font-semibold dark:text-gray-100' +const nameInputProps = { id: 'name', label: 'Segment name' } + +const segmentTypeSelectorProps = { + idPrefix: 'segment-type', + 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' + } + ] +} + interface ApiRequestProps { status: MutationStatus error?: unknown @@ -79,18 +107,23 @@ export const CreateSegmentModal = ({ return ( - + + {...segmentTypeSelectorProps} + value={type} + onChange={onSegmentTypeChange} /> - - {disabled && } + {disabled && } - { const trimmedName = name.trim() @@ -254,84 +287,6 @@ const RelatedSharedLinks = ({ sharedLinks }: { sharedLinks: string[] }) => { ) } -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, @@ -389,39 +344,6 @@ const useSegmentTypeDisabledState = ({ } } -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, @@ -449,18 +371,23 @@ export const UpdateSegmentModal = ({ return ( - - - {disabled && } + + {...segmentTypeSelectorProps} + value={type} + onChange={onSegmentTypeChange} + /> + {disabled && } - { const trimmedName = name.trim() @@ -510,51 +437,6 @@ const FiltersInSegment = ({ ) } -/** Keep this component styled the same as checkboxes in PlausibleWeb.Live.Installation.Instructions */ -const Checkbox = ({ - id, - checked, - onChange, - children -}: React.DetailedHTMLProps< - React.InputHTMLAttributes, - HTMLInputElement ->) => { - return ( - - ) -} - -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