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 (
+
+ )
+}
+
+/** 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 (
-
- )
-}
-
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