diff --git a/packages/sanity/src/tasks/src/tasks/components/create/TaskCreate.tsx b/packages/sanity/src/tasks/src/tasks/components/create/TaskCreate.tsx index 0a490e99034a..563a79888094 100644 --- a/packages/sanity/src/tasks/src/tasks/components/create/TaskCreate.tsx +++ b/packages/sanity/src/tasks/src/tasks/components/create/TaskCreate.tsx @@ -33,5 +33,5 @@ export function TaskCreate(props: TaskCreateProps) { : undefined, } - return + return } diff --git a/packages/sanity/src/tasks/src/tasks/components/edit/TaskEdit.tsx b/packages/sanity/src/tasks/src/tasks/components/edit/TaskEdit.tsx index b7e0440737d2..643e1fd3f751 100644 --- a/packages/sanity/src/tasks/src/tasks/components/edit/TaskEdit.tsx +++ b/packages/sanity/src/tasks/src/tasks/components/edit/TaskEdit.tsx @@ -7,5 +7,5 @@ interface TaskEditProps { export function TaskEdit(props: TaskEditProps) { const {onDelete, selectedTask} = props - return + return } diff --git a/packages/sanity/src/tasks/src/tasks/components/form/AddOnWorkspaceProvider.tsx b/packages/sanity/src/tasks/src/tasks/components/form/AddOnWorkspaceProvider.tsx index 775d1520430a..ff8ede9ac605 100644 --- a/packages/sanity/src/tasks/src/tasks/components/form/AddOnWorkspaceProvider.tsx +++ b/packages/sanity/src/tasks/src/tasks/components/form/AddOnWorkspaceProvider.tsx @@ -10,7 +10,13 @@ import { import {taskSchema} from './taskSchema' -export function AddOnWorkspaceProvider({children}: {children: React.ReactNode}) { +export function AddOnWorkspaceProvider({ + children, + mode, +}: { + children: React.ReactNode + mode: 'edit' | 'create' +}) { // Parent workspace source, we want to use the same project id const source = useSource() const basePath = undefined // TODO: Is basePath necessary here? @@ -23,10 +29,10 @@ export function AddOnWorkspaceProvider({children}: {children: React.ReactNode}) // TODO: Get this host from the studio config. apiHost: 'https://api.sanity.work', schema: { - types: [taskSchema], + types: [taskSchema(mode)], }, }), - [source.projectId], + [source.projectId, mode], ) const {workspaces} = useMemo( diff --git a/packages/sanity/src/tasks/src/tasks/components/form/FormEdit.tsx b/packages/sanity/src/tasks/src/tasks/components/form/FormEdit.tsx new file mode 100644 index 000000000000..fced2b408c95 --- /dev/null +++ b/packages/sanity/src/tasks/src/tasks/components/form/FormEdit.tsx @@ -0,0 +1,30 @@ +import {type ObjectInputProps} from 'sanity' +import styled from 'styled-components' + +import {type TaskDocument} from '../../types' +import {StatusSelector} from './StatusSelector' +import {Title} from './TitleField' + +const FirstRow = styled.div` + display: flex; + margin-top: 7px; +` +export function FormEdit(props: ObjectInputProps) { + const statusField = props.schemaType.fields.find((f) => f.name === 'status') + if (!statusField) { + throw new Error('Status field not found') + } + return ( +
+ + <FirstRow> + <StatusSelector + value={props.value?.status} + path={['status']} + onChange={props.onChange} + options={statusField.type.options.list} + /> + </FirstRow> + </div> + ) +} diff --git a/packages/sanity/src/tasks/src/tasks/components/form/StatusSelector.tsx b/packages/sanity/src/tasks/src/tasks/components/form/StatusSelector.tsx new file mode 100644 index 000000000000..9397a3b71346 --- /dev/null +++ b/packages/sanity/src/tasks/src/tasks/components/form/StatusSelector.tsx @@ -0,0 +1,75 @@ +import {CheckmarkCircleIcon, CheckmarkIcon, CircleIcon} from '@sanity/icons' +import {Menu} from '@sanity/ui' +import {type ForwardedRef, forwardRef, type ReactNode} from 'react' +import { + type FormPatch, + isString, + type PatchEvent, + type Path, + set, + type TitledListValue, +} from 'sanity' + +import {Button, MenuButton, MenuItem} from '../../../../../ui-components' + +// TODO: support customizing icons and options. +const OPTION_ICONS: Record<string, ReactNode> = { + closed: <CheckmarkCircleIcon />, + open: <CircleIcon />, +} + +export const StatusMenuButton = forwardRef(function StatusMenuButton( + props: {value: string | undefined; options: TitledListValue<string>[]}, + ref: ForwardedRef<HTMLButtonElement>, +) { + const {value, options} = props + const selectedOption = options.find((option) => option.value === value) + return ( + <Button + {...props} + ref={ref} + tooltipProps={null} + icon={value && OPTION_ICONS[value]} + text={selectedOption?.title || value} + tone="default" + mode="ghost" + /> + ) +}) + +interface StatusSelectorProps { + value: string | undefined + path: Path + options: TitledListValue<string>[] + onChange: (patch: FormPatch | PatchEvent | FormPatch[]) => void +} + +export function StatusSelector(props: StatusSelectorProps) { + const {value, onChange, options, path} = props + return ( + <MenuButton + button={<StatusMenuButton value={value} options={options} />} + id={`reference-menuButton`} + menu={ + <Menu> + {options.map((option) => { + const isSelected = value === option.value + return ( + <MenuItem + key={option.title} + icon={ + isString(option.value) ? OPTION_ICONS[option.value] || CircleIcon : CircleIcon + } + text={option.title || option.value} + pressed={isSelected} + iconRight={isSelected && <CheckmarkIcon />} + // eslint-disable-next-line react/jsx-no-bind + onClick={() => onChange(set(option.value, path))} + /> + ) + })} + </Menu> + } + /> + ) +} diff --git a/packages/sanity/src/tasks/src/tasks/components/form/TasksForm.tsx b/packages/sanity/src/tasks/src/tasks/components/form/TasksForm.tsx index 0e3917f2cbee..9e0e55f00856 100644 --- a/packages/sanity/src/tasks/src/tasks/components/form/TasksForm.tsx +++ b/packages/sanity/src/tasks/src/tasks/components/form/TasksForm.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components' import {CommentsEnabledProvider} from '../../../../../structure/comments' import {MentionUserProvider} from '../../context/mentionUser' -import {type TaskDocument} from '../../types' +import {type FormMode, type TaskDocument} from '../../types' import {AddOnWorkspaceProvider} from './AddOnWorkspaceProvider' import {useTasksFormBuilder} from './useTasksFormBuilder' @@ -33,7 +33,6 @@ const TasksCreateFormInner = ({ documentType: 'tasks.task', documentId, initialValue, - actiob: 'create', }) return ( @@ -57,9 +56,11 @@ const TasksCreateFormInner = ({ export function TasksForm({ documentId, initialValue, + mode, }: { documentId: string initialValue?: Partial<TaskDocument> + mode: FormMode }) { const currentUser = useCurrentUser() @@ -68,7 +69,7 @@ export function TasksForm({ return ( // This provider needs to be mounted before the AddonWorkspaceProvider. <MentionUserProvider> - <AddOnWorkspaceProvider> + <AddOnWorkspaceProvider mode={mode}> <TasksCreateFormInner documentId={documentId} currentUser={currentUser} diff --git a/packages/sanity/src/tasks/src/tasks/components/form/TitleField.tsx b/packages/sanity/src/tasks/src/tasks/components/form/TitleField.tsx index a9ac1f4c6ee0..5f677c14ee8b 100644 --- a/packages/sanity/src/tasks/src/tasks/components/form/TitleField.tsx +++ b/packages/sanity/src/tasks/src/tasks/components/form/TitleField.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line camelcase import {getTheme_v2} from '@sanity/ui/theme' import {type ChangeEvent, useCallback, useEffect, useRef} from 'react' -import {set, type StringFieldProps, unset} from 'sanity' +import {type FormPatch, type PatchEvent, type Path, set, type StringFieldProps, unset} from 'sanity' import styled, {css} from 'styled-components' const Root = styled.div` @@ -51,9 +51,13 @@ const TitleInput = styled.textarea((props) => { ` }) -export function TitleField(props: StringFieldProps) { - const {value, inputProps} = props - const {onChange} = inputProps +export function Title(props: { + value: string | undefined + path?: Path + onChange: (patch: FormPatch | PatchEvent | FormPatch[]) => void + placeholder?: string +}) { + const {value, onChange, placeholder, path} = props const ref = useRef<HTMLTextAreaElement | null>(null) useEffect(() => { @@ -66,9 +70,9 @@ export function TitleField(props: StringFieldProps) { (event: ChangeEvent<HTMLTextAreaElement>) => { const inputValue = event.currentTarget.value if (!inputValue) onChange(unset()) - return onChange(set(inputValue.replace(/\n/g, ''))) + return onChange(set(inputValue.replace(/\n/g, ''), path)) }, - [onChange], + [onChange, path], ) return ( @@ -77,10 +81,17 @@ export function TitleField(props: StringFieldProps) { ref={ref} autoFocus={!value} value={value} - placeholder={props.inputProps.schemaType.placeholder} + placeholder={placeholder} onChange={handleChange} rows={1} /> </Root> ) } + +export function TitleField(props: StringFieldProps) { + const {value, inputProps} = props + const {onChange, schemaType} = inputProps + + return <Title value={value} onChange={onChange} placeholder={schemaType.placeholder} /> +} diff --git a/packages/sanity/src/tasks/src/tasks/components/form/taskSchema.tsx b/packages/sanity/src/tasks/src/tasks/components/form/taskSchema.tsx index 173829180b38..32930887b326 100644 --- a/packages/sanity/src/tasks/src/tasks/components/form/taskSchema.tsx +++ b/packages/sanity/src/tasks/src/tasks/components/form/taskSchema.tsx @@ -1,113 +1,119 @@ import {defineType} from 'sanity' +import {type FormMode} from '../../types' import {DescriptionInput} from './DescriptionInput' +import {FormEdit} from './FormEdit' import {MentionUserFormField} from './MentionUser' import {TargetField} from './TargetField' import {TitleField} from './TitleField' -export const taskSchema = defineType({ - type: 'document', - name: 'tasks.task', - liveEdit: true, - fields: [ - { - type: 'string', - title: 'Title', - name: 'title', - placeholder: 'Task title', - components: { - field: TitleField, - }, +export const taskSchema = (mode: FormMode) => + defineType({ + type: 'document', + name: 'tasks.task', + liveEdit: true, + components: { + input: mode === 'edit' ? FormEdit : undefined, }, - { - type: 'array', - name: 'description', - title: 'Description', - components: { - input: DescriptionInput, - }, - of: [ - { - type: 'block', - name: 'block', - of: [ - { - name: 'mention', - type: 'object', - fields: [ - { - name: 'userId', - type: 'string', - }, - ], - }, - ], - marks: { - annotations: [], - }, - styles: [{title: 'Normal', value: 'normal'}], - lists: [], + fields: [ + { + type: 'string', + title: 'Title', + name: 'title', + placeholder: 'Task title', + components: { + field: TitleField, }, - ], - }, - { - type: 'object', - name: 'target', - title: 'Target content', - components: { - field: TargetField, }, - fields: [ - { - name: 'document', - type: 'crossDatasetReference', - dataset: 'playground', - weak: true, - studioUrl: ({id, type}) => `intent/edit/id=${id};type=${type}/`, - to: [ - { - type: 'any_document', - preview: { - select: {title: 'title'}, + { + type: 'array', + name: 'description', + title: 'Description', + components: { + input: DescriptionInput, + }, + of: [ + { + type: 'block', + name: 'block', + of: [ + { + name: 'mention', + type: 'object', + fields: [ + { + name: 'userId', + type: 'string', + }, + ], }, + ], + marks: { + annotations: [], }, - ], + styles: [{title: 'Normal', value: 'normal'}], + lists: [], + }, + ], + }, + { + type: 'object', + name: 'target', + title: 'Target content', + components: { + field: TargetField, }, - { - name: 'documentType', - type: 'string', - title: 'Document type', + fields: [ + { + name: 'document', + type: 'crossDatasetReference', + dataset: 'playground', + weak: true, + studioUrl: ({id, type}) => `intent/edit/id=${id};type=${type}/`, + to: [ + { + type: 'any_document', + preview: { + select: {title: 'title'}, + }, + }, + ], + }, + { + name: 'documentType', + type: 'string', + title: 'Document type', + }, + ], + }, + { + type: 'string', + name: 'assignedTo', + title: 'Assigned to', + placeholder: 'Search username', + components: { + input: MentionUserFormField, }, - ], - }, - { - type: 'string', - name: 'assignedTo', - title: 'Assigned to', - placeholder: 'Search username', - components: { - input: MentionUserFormField, }, - }, - { - type: 'date', - name: 'dueBy', - title: 'Deadline', - placeholder: 'Select date', - }, - { - type: 'string', - name: 'authorId', - hidden: true, - }, - { - type: 'string', - name: 'status', - title: 'Status', - options: { - list: ['open', 'closed'], + { + type: 'date', + name: 'dueBy', + title: 'Deadline', + placeholder: 'Select date', }, - hidden: true, - }, - ], -}) + { + type: 'string', + name: 'authorId', + hidden: true, + }, + { + type: 'string', + name: 'status', + title: 'Status', + options: { + list: ['open', 'closed'], + }, + hidden: true, + }, + ], + }) diff --git a/packages/sanity/src/tasks/src/tasks/components/form/useTasksFormBuilder.ts b/packages/sanity/src/tasks/src/tasks/components/form/useTasksFormBuilder.ts index a0e13adc077b..0688de189aff 100644 --- a/packages/sanity/src/tasks/src/tasks/components/form/useTasksFormBuilder.ts +++ b/packages/sanity/src/tasks/src/tasks/components/form/useTasksFormBuilder.ts @@ -39,11 +39,10 @@ interface TasksFormBuilderOptions { documentType: string documentId: string initialValue?: Partial<TaskDocument> - action: 'create' | 'edit' } export function useTasksFormBuilder(options: TasksFormBuilderOptions): TasksFormBuilder { - const {documentType = 'tasks.task', documentId, initialValue = {}, action} = options + const {documentType = 'tasks.task', documentId, initialValue = {}} = options const schema = useSchema() const tasksSchemaType = schema.get(documentType) as ObjectSchemaType | undefined @@ -94,14 +93,8 @@ export function useTasksFormBuilder(options: TasksFormBuilderOptions): TasksForm patch.execute(toMutationPatches(event.patches), initialValue) } - const handleChange = useCallback( - (event: PatchEvent) => { - if (action === 'create') { - console.log('EVENT', event) - } else patchRef.current(event) - }, - [action], - ) + const handleChange = useCallback((event: PatchEvent) => patchRef.current(event), []) + const connectionState = useConnectionState(documentId, documentType) const editState = useEditState(documentId, documentType) diff --git a/packages/sanity/src/tasks/src/tasks/types.ts b/packages/sanity/src/tasks/src/tasks/types.ts index c41bd0e1aa08..ae77b43b3ac0 100644 --- a/packages/sanity/src/tasks/src/tasks/types.ts +++ b/packages/sanity/src/tasks/src/tasks/types.ts @@ -127,3 +127,5 @@ export type TaskEditPayload = { status?: TaskStatus assignedTo?: string } + +export type FormMode = 'create' | 'edit'