Skip to content

Commit

Repository access: a role change or removal is confirmed under the list; the dead page-level messages, which fetcher rows never reached, are gone

syntaqxcommitted Parent1f68383Browse files
2 files+43−120/2 viewed
+2−1
154154 To change someone's role, pick another beside their name. To take it away,
155155 choose **Remove**. Removing takes away only the role given on this
156156 repository: an owner's Admin and a member's base permission stay. Anyone
157−can remove their own role from a repository.
157+can remove their own role from a repository. Each change is confirmed
158+under the list; one that is refused says why on that person's row.
158159
159160 ## Outside collaborators
160161
+41−11
9595 }
9696 case "remove": {
9797 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);
99101 }
100102 case "revoke": {
101103 const revoked = await identity.revokeRepoInvitation(user, params.owner, params.repo, text("id"));
121123 (a, b) => REPO_ROLES.indexOf(b.role) - REPO_ROLES.indexOf(a.role) || a.username.localeCompare(b.username),
122124 );
123125 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);
124129 return (
125130 <>
126131 <RepoSettingsHeading base={base} />
170175 >
171176 <ul className="divide-y divide-line rounded-xl border border-line">
172177 {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+ />
174186 ))}
175187 </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>
182192 )}
183193 </Section>
184194
195205 ))}
196206 </ul>
197207 )}
198− {actionData?.intent === "revoke" && !actionData.ok && <ErrorText>{actionData.error}</ErrorText>}
199208 </Section>
200209 )}
201210
264273 manage,
265274 base,
266275 full,
276+ onDone,
267277 }: {
268278 person: Collaborator;
269279 manage: boolean;
270280 base: Parameters<typeof baseRole>[0];
271281 full: string;
282+ onDone: (notice: string | null) => void;
272283 }) {
273284 const fetcher = useFetcher<Outcome>();
285+ useEffect(() => {
286+ if (fetcher.data) onDone(fetcher.data.ok ? fetcher.data.message : null);
287+ }, [fetcher.data, onDone]);
274288 const outside = person.workspace_role == null;
275289 // A member keeps the base permission whatever role they are given here,
276290 // so only higher roles mean anything for them.
317331 {/* The same room for Remove on every row, so the roles line up. */}
318332 {manage && (
319333 <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+ )}
321337 </span>
322338 )}
323339 </div>
326342 }
327343
328344 /** 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+}) {
330356 const fetcher = useFetcher<Outcome>();
331357 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]);
332362 useEffect(() => {
333363 if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false);
334364 }, [fetcher.state, fetcher.data]);