Open History to see the last commit
1import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
2import {
3 capitalizeTerm,
4 sourceControlClients,
5} from "@t3tools/client-runtime/source-control-clients";
6import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
7import { useAtomValue } from "@effect/atom-react";
8import type { PullRequestComment } from "@t3tools/contracts";
9import { AsyncResult } from "effect/reactivity";
10import { useRef, useState } from "react";
11import { AppText as Text, AppTextInput as TextInput } from "../../components/ui/AppText";
12import { Button } from "../../components/ui/Button";
13import { composerPullRequests } from "../../state/pull-requests";
14import { useEnvironmentQuery } from "../../state/query";
15import { useAtomCommand } from "../../state/use-atom-command";
16import { PullRequestSheetContent } from "./PullRequestSheetContent";
17import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff";
18import { usePullRequestPermissions } from "./usePullRequestPermissions";
19import { PullRequestCompletionSuggestions } from "./PullRequestCompletionSuggestions";
20import { usePullRequestCompletions } from "./usePullRequestCompletions";
21
22export type PullRequestEditParams = PullRequestParams &
23 (
24 | { readonly field: "title" | "body" }
25 | {
26 readonly field: "comment";
27 /** Carried along because a reply loaded page by page is not in the cached activity. */
28 readonly comment: Pick<PullRequestComment, "id" | "body" | "author" | "canEdit"> & {
29 readonly kind: "issue-comment" | "review-comment";
30 };
31 }
32 );
33
34export function PullRequestEditScreen({ route }: StaticScreenProps<PullRequestEditParams>) {
35 const params = route.params;
36 const navigation = useNavigation();
37 const target = pullRequestTarget(params);
38 const detail = useEnvironmentQuery(composerPullRequests.detail(target));
39 const pr = detail.data;
40 const permissions = usePullRequestPermissions(params, pr);
41 const update = useAtomCommand(composerPullRequests.update, { reportFailure: false });
42 const updateComment = useAtomCommand(composerPullRequests.updateComment, {
43 reportFailure: false,
44 });
45 const singular = sourceControlClients.get(pr?.provider).changeRequest.singular;
46 const original =
47 params.field === "comment"
48 ? params.comment.body
49 : params.field === "title"
50 ? (pr?.title ?? null)
51 : (pr?.body ?? null);
52 const [draft, setDraft] = useState<string | null>(null);
53 const value = draft ?? original ?? "";
54 const completions = usePullRequestCompletions({
55 environmentId: params.environmentId,
56 projectId: params.projectId,
57 value: value,
58 onChangeValue: setDraft,
59 support: params.field === "title" ? null : (pr?.capabilities ?? null),
60 });
61 const [busy, setBusy] = useState(false);
62 const [error, setError] = useState<string | null>(null);
63 const inFlight = useRef(false);
64 const allowed =
65 params.field === "comment"
66 ? permissions.canEditComment(params.comment)
67 : permissions.canEditChangeRequest;
68 const canWrite = useAtomValue(
69 (params.field === "comment"
70 ? composerPullRequests.updateComment
71 : composerPullRequests.update
72 ).permissionAtom(target.environmentId),
73 );
74 // Name the first thing in the way, so the notice says what would actually fix it.
75 const readOnlyReason =
76 !pr || allowed
77 ? null
78 : !permissions.connected
79 ? "Reconnect to save changes."
80 : !canWrite
81 ? "This device isn't allowed to change source control on this environment."
82 : params.field === "comment"
83 ? "You can't edit this comment."
84 : `You can't edit this ${singular}.`;
85 const unchanged = draft === null || draft === original;
86 const invalid = params.field === "title" && value.trim().length === 0;
87 const save = async () => {
88 if (!allowed || unchanged || invalid || inFlight.current) return;
89 inFlight.current = true;
90 setBusy(true);
91 setError(null);
92 const result =
93 params.field === "comment"
94 ? await updateComment({
95 ...target,
96 input: {
97 ...target.input,
98 commentId: params.comment.id,
99 kind: params.comment.kind,
100 body: value,
101 },
102 })
103 : await update({
104 ...target,
105 input: {
106 ...target.input,
107 ...(params.field === "title" ? { title: value.trim() } : { body: value }),
108 },
109 });
110 inFlight.current = false;
111 setBusy(false);
112 if (AsyncResult.isFailure(result)) {
113 const cause = squashAtomCommandFailure(result);
114 setError(cause instanceof Error ? cause.message : "The provider refused the change.");
115 return;
116 }
117 // updateComment refreshes the activity on its own; a title or body lives on the detail.
118 if (params.field !== "comment") detail.refresh();
119 navigation.goBack();
120 };
121 const title =
122 params.field === "title"
123 ? "Edit title"
124 : params.field === "body"
125 ? "Edit description"
126 : "Edit comment";
127 return (
128 <PullRequestSheetContent
129 busy={busy}
130 title={title}
131 subtitle={`#${params.number} · ${params.repository}`}
132 footer={
133 <Button
134 fullWidth
135 tone="contrast"
136 label={busy ? "Saving…" : "Save"}
137 loading={busy}
138 disabled={!allowed || unchanged || invalid || original === null}
139 onPress={() => void save()}
140 />
141 }
142 >
143 <PullRequestCompletionSuggestions completions={completions} />
144 <TextInput
145 multiline={params.field !== "title"}
146 selection={completions.selection}
147 onSelectionChange={completions.onSelectionChange}
148 onBlur={completions.onBlur}
149 editable={!busy && original !== null}
150 autoFocus
151 accessibilityLabel={
152 params.field === "title"
153 ? `${capitalizeTerm(singular)} title`
154 : params.field === "body"
155 ? `${capitalizeTerm(singular)} description`
156 : "Comment"
157 }
158 placeholder={params.field === "title" ? "Title" : "Write in Markdown…"}
159 placeholderTextColorClassName="accent-foreground-muted"
160 maxLength={params.field === "title" ? 1024 : 65536}
161 value={value}
162 onChangeText={setDraft}
163 returnKeyType={params.field === "title" ? "done" : undefined}
164 onSubmitEditing={params.field === "title" ? () => void save() : undefined}
165 className={
166 params.field === "title"
167 ? "min-h-12 rounded-[20px] bg-grouped-card px-4 text-base text-foreground"
168 : "min-h-48 rounded-[20px] bg-grouped-card p-4 text-base text-foreground"
169 }
170 style={params.field === "title" ? undefined : { textAlignVertical: "top" }}
171 />
172 {readOnlyReason ? (
173 <Text className="text-xs text-foreground-muted">{readOnlyReason}</Text>
174 ) : null}
175 {error || detail.error ? (
176 <Text selectable className="text-sm text-danger-foreground">
177 {error ?? detail.error}
178 </Text>
179 ) : null}
180 </PullRequestSheetContent>
181 );
182}