From 786dd90e1ea0a79d78d5fa50db4663574a481645 Mon Sep 17 00:00:00 2001 From: Waleed Khalid Date: Fri, 11 Sep 2026 22:01:27 +0500 Subject: [PATCH] Add issue creation and editing Closes #6. Every property the web's new-issue modal offers, as tappable rows opening a sheet rather than a column of selects -- a phone has no ``s. Only the title is required -- the rest default the + * way the API defaults them, so capturing a thought takes one field and a tap. + */ +type Picker = + | 'status' + | 'priority' + | 'estimate' + | 'assignee' + | 'labels' + | 'project' + | 'cycle' + | null; + +export function NewIssueScreen() { + const t = useTokens(); + const queryClient = useQueryClient(); + const { team } = useTeams(); + const props = useIssueProperties(team); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [statusId, setStatusId] = useState(null); + const [priority, setPriority] = useState('no_priority'); + const [estimate, setEstimate] = useState(null); + const [assigneeId, setAssigneeId] = useState(null); + const [labelIds, setLabelIds] = useState([]); + const [projectId, setProjectId] = useState(null); + const [cycleId, setCycleId] = useState(null); + + const [picker, setPicker] = useState(null); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + async function submit() { + if (!team) return; + setError(null); + setSubmitting(true); + try { + const issue = await createIssueTeamsTeamIdIssuesPost(team.id, { + title: title.trim(), + // An empty description is nothing, not an empty string to render. + description: description.trim() || undefined, + status_id: statusId ?? undefined, + priority, + estimate: estimate as never, + assignee_id: assigneeId ?? undefined, + label_ids: labelIds, + project_id: projectId ?? undefined, + cycle_id: cycleId ?? undefined, + }); + + await queryClient.invalidateQueries({ queryKey: [`/teams/${team.id}/issues`] }); + void queryClient.invalidateQueries({ queryKey: [`/teams/${team.id}/estimates`] }); + // Straight to what was just made, replacing this screen so Back returns + // to the board rather than to an empty form. + router.replace(href(`/issue/${issue.id}`)); + } catch (err) { + setError(errorDetail(err, 'Could not create the issue.')); + } finally { + setSubmitting(false); + } + } + + const status = labelFor(props.statusOptions, statusId, 'Team default'); + const assignee = labelFor(props.assigneeOptions, assigneeId, 'Unassigned'); + const project = labelFor(props.projectOptions, projectId, 'None'); + const cycle = labelFor(props.cycleOptions, cycleId, 'None'); + const points = labelFor(props.estimateOptions, estimate, 'Not sized'); + const priorityMeta = labelFor(props.priorityOptions, priority, 'No priority'); + + return ( + + + router.back()} + > + Cancel + + + New issue + + {/* Balances the cancel button so the title sits centred. */} + + + + + + {error ? {error} : null} + + + + + + + + setPicker('status')} + /> + setPicker('priority')} + /> + setPicker('estimate')} + /> + setPicker('assignee')} + /> + + props.labelOptions.find((o) => o.value === id)?.label ?? String(id), + ) + .join(', ') + } + muted={labelIds.length === 0} + onPress={() => setPicker('labels')} + /> + setPicker('project')} + /> + setPicker('cycle')} + /> + + + + + + + setPicker(null)} + /> + setPriority(value ?? 'no_priority')} + onClose={() => setPicker(null)} + /> + setPicker(null)} + /> + setPicker(null)} + /> + + setLabelIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], + ) + } + onClose={() => setPicker(null)} + footer={ + { + const id = await props.createLabel(name); + if (id !== null) setLabelIds((prev) => [...prev, id]); + }} + /> + } + /> + setPicker(null)} + footer={ + { + const id = await props.createProject(name); + if (id !== null) setProjectId(id); + }} + /> + } + /> + setPicker(null)} + /> + + ); +} diff --git a/src/issues/property-picker.tsx b/src/issues/property-picker.tsx new file mode 100644 index 0000000..28556f4 --- /dev/null +++ b/src/issues/property-picker.tsx @@ -0,0 +1,275 @@ +import { useState, type ReactNode } from 'react'; +import { Pressable, View } from 'react-native'; + +import { Icon } from '@/ui/icon'; +import { AppText, Button, Field } from '@/ui/primitives'; +import { Sheet } from '@/ui/sheet'; +import { useTokens } from '@/ui/theme'; + +/** + * The pickers every issue property is edited through. + * + * The web uses `