| 95 | 95 | | } |
| 96 | 96 | | case "remove": { |
| 97 | 97 | | const removed = await identity.removeCollaborator(user, params.owner, params.repo, text("username")); |
| 98 | | − | return removed.ok ? { intent, ok: true, error: null, message: null } : failed(removed.error.message); |
| 98 | + | return removed.ok |
| 99 | + | ? { intent, ok: true, error: null, message: `${text("username")} no longer has a role given here.` } |
| 100 | + | : failed(removed.error.message); |
| 99 | 101 | | } |
| 100 | 102 | | case "revoke": { |
| 101 | 103 | | const revoked = await identity.revokeRepoInvitation(user, params.owner, params.repo, text("id")); |
| ⋯ |
| 121 | 123 | | (a, b) => REPO_ROLES.indexOf(b.role) - REPO_ROLES.indexOf(a.role) || a.username.localeCompare(b.username), |
| 122 | 124 | | ); |
| 123 | 125 | | const result = actionData?.intent === "add" ? actionData : undefined; |
| 126 | + | // Roles are changed and people removed from their own rows, which say |
| 127 | + | // what went wrong; what went right is said once, under the list. |
| 128 | + | const [notice, setNotice] = useState<string | null>(null); |
| 124 | 129 | | return ( |
| 125 | 130 | | <> |
| 126 | 131 | | <RepoSettingsHeading base={base} /> |
| ⋯ |
| 170 | 175 | | > |
| 171 | 176 | | <ul className="divide-y divide-line rounded-xl border border-line"> |
| 172 | 177 | | {people.map((person) => ( |
| 173 | | − | <PersonRow key={person.username} person={person} manage={manage} base={access.base_permission} full={full} /> |
| 178 | + | <PersonRow |
| 179 | + | key={person.username} |
| 180 | + | person={person} |
| 181 | + | manage={manage} |
| 182 | + | base={access.base_permission} |
| 183 | + | full={full} |
| 184 | + | onDone={setNotice} |
| 185 | + | /> |
| 174 | 186 | | ))} |
| 175 | 187 | | </ul> |
| 176 | | − | {actionData && ["role", "remove"].includes(actionData.intent) && ( |
| 177 | | − | actionData.ok ? ( |
| 178 | | − | actionData.message && <p className="text-sm text-muted" role="status">{actionData.message}</p> |
| 179 | | − | ) : ( |
| 180 | | − | <ErrorText>{actionData.error}</ErrorText> |
| 181 | | − | ) |
| 188 | + | {notice && ( |
| 189 | + | <p className="text-sm text-accent" role="status"> |
| 190 | + | {notice} |
| 191 | + | </p> |
| 182 | 192 | | )} |
| 183 | 193 | | </Section> |
| 184 | 194 | | |
| ⋯ |
| 195 | 205 | | ))} |
| 196 | 206 | | </ul> |
| 197 | 207 | | )} |
| 198 | | − | {actionData?.intent === "revoke" && !actionData.ok && <ErrorText>{actionData.error}</ErrorText>} |
| 199 | 208 | | </Section> |
| 200 | 209 | | )} |
| 201 | 210 | | |
| ⋯ |
| 264 | 273 | | manage, |
| 265 | 274 | | base, |
| 266 | 275 | | full, |
| 276 | + | onDone, |
| 267 | 277 | | }: { |
| 268 | 278 | | person: Collaborator; |
| 269 | 279 | | manage: boolean; |
| 270 | 280 | | base: Parameters<typeof baseRole>[0]; |
| 271 | 281 | | full: string; |
| 282 | + | onDone: (notice: string | null) => void; |
| 272 | 283 | | }) { |
| 273 | 284 | | const fetcher = useFetcher<Outcome>(); |
| 285 | + | useEffect(() => { |
| 286 | + | if (fetcher.data) onDone(fetcher.data.ok ? fetcher.data.message : null); |
| 287 | + | }, [fetcher.data, onDone]); |
| 274 | 288 | | const outside = person.workspace_role == null; |
| 275 | 289 | | // A member keeps the base permission whatever role they are given here, |
| 276 | 290 | | // so only higher roles mean anything for them. |
| ⋯ |
| 317 | 331 | | {/* The same room for Remove on every row, so the roles line up. */} |
| 318 | 332 | | {manage && ( |
| 319 | 333 | | <span className="flex w-[5.5rem] justify-end"> |
| 320 | | − | {editable && person.direct != null && <RemoveButton person={person} full={full} outside={outside} />} |
| 334 | + | {editable && person.direct != null && ( |
| 335 | + | <RemoveButton person={person} full={full} outside={outside} onDone={onDone} /> |
| 336 | + | )} |
| 321 | 337 | | </span> |
| 322 | 338 | | )} |
| 323 | 339 | | </div> |
| ⋯ |
| 326 | 342 | | } |
| 327 | 343 | | |
| 328 | 344 | | /** Taking someone's role here away, after saying what that means. */ |
| 329 | | − | function RemoveButton({ person, full, outside }: { person: Collaborator; full: string; outside: boolean }) { |
| 345 | + | function RemoveButton({ |
| 346 | + | person, |
| 347 | + | full, |
| 348 | + | outside, |
| 349 | + | onDone, |
| 350 | + | }: { |
| 351 | + | person: Collaborator; |
| 352 | + | full: string; |
| 353 | + | outside: boolean; |
| 354 | + | onDone: (notice: string | null) => void; |
| 355 | + | }) { |
| 330 | 356 | | const fetcher = useFetcher<Outcome>(); |
| 331 | 357 | | const [open, setOpen] = useState(false); |
| 358 | + | // Said as soon as the answer comes, before the row leaves the list. |
| 359 | + | useEffect(() => { |
| 360 | + | if (fetcher.data) onDone(fetcher.data.ok ? fetcher.data.message : null); |
| 361 | + | }, [fetcher.data, onDone]); |
| 332 | 362 | | useEffect(() => { |
| 333 | 363 | | if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false); |
| 334 | 364 | | }, [fetcher.state, fetcher.data]); |