Skip to content

Commit 2fdbd65

Browse files
committed
wip: commit progress for updated roles column
1 parent d278c20 commit 2fdbd65

File tree

1 file changed

+114
-37
lines changed

1 file changed

+114
-37
lines changed

site/src/pages/UsersPage/UsersTable/UserRoleCell.tsx

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useLayoutEffect, useRef, useState } from "react";
22
import { useTheme } from "@emotion/react";
33
import { type User, type Role } from "api/typesGenerated";
44

@@ -7,35 +7,56 @@ import { Pill } from "components/Pill/Pill";
77
import TableCell from "@mui/material/TableCell";
88
import Stack from "@mui/material/Stack";
99

10-
const roleNameDisplayOrder: readonly string[] = [
10+
const fallbackRole: Role = {
11+
name: "member",
12+
display_name: "Member",
13+
} as const;
14+
15+
const roleNamesByAccessLevel: readonly string[] = [
1116
"owner",
1217
"user-admin",
1318
"template-admin",
1419
"auditor",
1520
];
1621

17-
const fallbackRole: Role = {
18-
name: "member",
19-
display_name: "Member",
20-
} as const;
22+
function sortRolesByAccessLevel(roles: readonly Role[]) {
23+
return [...roles].sort(
24+
(r1, r2) =>
25+
roleNamesByAccessLevel.indexOf(r1.name) -
26+
roleNamesByAccessLevel.indexOf(r2.name),
27+
);
28+
}
29+
30+
type RoleDisplayInfo = Readonly<{
31+
hasOwner: boolean;
32+
roles: readonly Role[];
33+
}>;
2134

22-
function getPillRoleList(userRoles: readonly Role[]): readonly Role[] {
35+
function getRoleDisplayInfo(userRoles: readonly Role[]): RoleDisplayInfo {
2336
if (userRoles.length === 0) {
24-
return [fallbackRole];
37+
return {
38+
hasOwner: false,
39+
roles: [fallbackRole],
40+
};
2541
}
2642

2743
const matchedOwnerRole = userRoles.find((role) => role.name === "owner");
2844
if (matchedOwnerRole !== undefined) {
29-
return [matchedOwnerRole];
45+
return {
46+
hasOwner: true,
47+
roles: [matchedOwnerRole],
48+
};
3049
}
3150

32-
return [...userRoles].sort((r1, r2) => {
51+
const sortedRoles = [...userRoles].sort((r1, r2) => {
3352
if (r1.name === r2.name) {
3453
return 0;
3554
}
3655

3756
return r1.name < r2.name ? -1 : 1;
3857
});
58+
59+
return { hasOwner: false, roles: sortedRoles };
3960
}
4061

4162
function getSelectedRoleNames(roles: readonly Role[]) {
@@ -47,12 +68,10 @@ function getSelectedRoleNames(roles: readonly Role[]) {
4768
return roleNameSet;
4869
}
4970

50-
function sortRolesByAccessLevel(roles: readonly Role[]) {
51-
return [...roles].sort(
52-
(r1, r2) =>
53-
roleNameDisplayOrder.indexOf(r1.name) -
54-
roleNameDisplayOrder.indexOf(r2.name),
55-
);
71+
// Defined as a function to ensure that render approach and mutation approaches
72+
// in the component don't get out of sync
73+
function getOverflowButtonText(overflowCount: number) {
74+
return `+${overflowCount} more`;
5675
}
5776

5877
type Props = {
@@ -74,13 +93,59 @@ export function UserRoleCell({
7493
}: Props) {
7594
const theme = useTheme();
7695

77-
const pillRoleList = getPillRoleList(user.roles);
78-
const [rolesTruncated, setRolesTruncated] = useState(
79-
user.roles.length - pillRoleList.length,
96+
const cellRef = useRef<HTMLDivElement>(null);
97+
const pillContainerRef = useRef<HTMLDivElement>(null);
98+
const overflowButtonRef = useRef<HTMLButtonElement>(null);
99+
100+
// Unless the user happens to be an owner, it is physically impossible for
101+
// React to know how many pills should be omitted for space reasons on the
102+
// first render, because that info comes from real DOM nodes, which can't
103+
// exist until the first render pass. Have to do a smoke-and-mirrors routine
104+
// to help mask that and avoid UI flickering
105+
const roleDisplayInfo = getRoleDisplayInfo(user.roles);
106+
const [rolesToTruncate, setRolesToTruncate] = useState(
107+
roleDisplayInfo.hasOwner
108+
? user.roles.length - roleDisplayInfo.roles.length
109+
: null,
80110
);
81111

112+
// Mutates the contents of the pill container to hide overflowing content on
113+
// the first render, and then updates rolesToTruncate so that these overflow
114+
// calculations can be done with 100% pure state/props calculations for all
115+
// re-renders
116+
useLayoutEffect(() => {
117+
const cell = cellRef.current;
118+
const pillContainer = pillContainerRef.current;
119+
if (roleDisplayInfo.hasOwner || cell === null || pillContainer === null) {
120+
return;
121+
}
122+
123+
let nodesRemoved = 0;
124+
const childrenCopy = [...pillContainer.children];
125+
126+
for (let i = childrenCopy.length - 1; i >= 0; i--) {
127+
const child = childrenCopy[i] as HTMLElement;
128+
if (pillContainer.clientWidth <= cell.clientWidth) {
129+
break;
130+
}
131+
132+
// Can't remove child, because then React will freak out about DOM nodes
133+
// disappearing in ways it wasn't aware of; have to rely on CSS styling
134+
child.style.visibility = "none";
135+
nodesRemoved++;
136+
}
137+
138+
setRolesToTruncate(nodesRemoved);
139+
if (overflowButtonRef.current !== null) {
140+
const mutationText = getOverflowButtonText(nodesRemoved);
141+
overflowButtonRef.current.innerText = mutationText;
142+
}
143+
}, [roleDisplayInfo.hasOwner]);
144+
145+
const finalRoleList = roleDisplayInfo.roles;
146+
82147
return (
83-
<TableCell>
148+
<TableCell ref={cellRef}>
84149
<Stack direction="row" spacing={1}>
85150
{canEditUsers && (
86151
<EditRolesButton
@@ -100,23 +165,35 @@ export function UserRoleCell({
100165
/>
101166
)}
102167

103-
{pillRoleList.map((role) => {
104-
const isOwnerRole = role.name === "owner";
105-
const { palette } = theme;
106-
107-
return (
108-
<Pill
109-
key={role.name}
110-
text={role.display_name}
111-
css={{
112-
backgroundColor: isOwnerRole
113-
? palette.info.dark
114-
: palette.background.paperLight,
115-
borderColor: isOwnerRole ? palette.info.light : palette.divider,
116-
}}
117-
/>
118-
);
119-
})}
168+
<Stack direction="row" spacing={1}>
169+
<Stack direction="row" spacing={1} ref={pillContainerRef}>
170+
{finalRoleList.map((role) => {
171+
const isOwnerRole = role.name === "owner";
172+
const { palette } = theme;
173+
174+
return (
175+
<Pill
176+
key={role.name}
177+
text={role.display_name}
178+
css={{
179+
backgroundColor: isOwnerRole
180+
? palette.info.dark
181+
: palette.background.paperLight,
182+
borderColor: isOwnerRole
183+
? palette.info.light
184+
: palette.divider,
185+
}}
186+
/>
187+
);
188+
})}
189+
</Stack>
190+
191+
{rolesToTruncate !== 0 && (
192+
<button ref={overflowButtonRef}>
193+
{getOverflowButtonText(rolesToTruncate ?? 0)}
194+
</button>
195+
)}
196+
</Stack>
120197
</Stack>
121198
</TableCell>
122199
);

0 commit comments

Comments
 (0)