mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
0139b91b5d
* chore: Replace lodash with es-toolkit Migrate all direct lodash imports to es-toolkit/compat for a smaller, faster, lodash-compatible utility library. Transitive lodash usage from other packages remains unchanged. * fix: Restore isPlainObject semantics in CanCan policy The lodash migration aliased `isObject` to `lodash/isPlainObject` and the codemod incorrectly mapped the local name to es-toolkit's `isObject`, which also returns true for arrays and functions. This caused condition objects in policy definitions to be skipped, breaking authorization checks across the codebase. * fix: Restore unicode-aware length counting in validators es-toolkit/compat's size() returns string.length, while lodash's _.size() counts unicode code points. Switch to [...value].length to preserve the previous behavior so multi-byte characters like emoji count as one.
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { sortBy } from "es-toolkit/compat";
|
|
import { observer } from "mobx-react";
|
|
import { useState, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { PAGINATION_SYMBOL } from "~/stores/base/Store";
|
|
import type Collection from "~/models/Collection";
|
|
import { AvatarSize } from "~/components/Avatar";
|
|
import Facepile from "~/components/Facepile";
|
|
import Fade from "~/components/Fade";
|
|
import NudeButton from "~/components/NudeButton";
|
|
import useMobile from "~/hooks/useMobile";
|
|
import useStores from "~/hooks/useStores";
|
|
|
|
type Props = {
|
|
collection: Collection;
|
|
limit?: number;
|
|
};
|
|
|
|
const MembershipPreview = ({ collection, limit = 8 }: Props) => {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [usersCount, setUsersCount] = useState(0);
|
|
const [groupsCount, setGroupsCount] = useState(0);
|
|
const { t } = useTranslation();
|
|
const { memberships, groupMemberships, users } = useStores();
|
|
const collectionUsers = users.inCollection(collection.id);
|
|
const isMobile = useMobile();
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
if (collection.permission || isMobile) {
|
|
return;
|
|
}
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const options = {
|
|
id: collection.id,
|
|
limit,
|
|
};
|
|
const [users, groups] = await Promise.all([
|
|
memberships.fetchPage(options),
|
|
groupMemberships.fetchPage(options),
|
|
]);
|
|
if (users[PAGINATION_SYMBOL]) {
|
|
setUsersCount(users[PAGINATION_SYMBOL].total ?? 0);
|
|
}
|
|
if (groups[PAGINATION_SYMBOL]) {
|
|
setGroupsCount(groups[PAGINATION_SYMBOL].total ?? 0);
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
void fetchData();
|
|
}, [
|
|
isMobile,
|
|
collection.permission,
|
|
collection.id,
|
|
groupMemberships,
|
|
memberships,
|
|
limit,
|
|
]);
|
|
|
|
if (isLoading || collection.permission || isMobile) {
|
|
return null;
|
|
}
|
|
|
|
const overflow = usersCount + groupsCount - collectionUsers.length;
|
|
|
|
return (
|
|
<NudeButton
|
|
tooltip={{
|
|
content:
|
|
usersCount > 0
|
|
? groupsCount > 0
|
|
? groupsCount > 1
|
|
? t(
|
|
`{{ usersCount }} users and {{ groupsCount }} groups with access`,
|
|
{ usersCount, groupsCount, count: usersCount }
|
|
)
|
|
: t(`{{ usersCount }} users and a group have access`, {
|
|
usersCount,
|
|
count: usersCount,
|
|
})
|
|
: t(`{{ usersCount }} users with access`, {
|
|
usersCount,
|
|
count: usersCount,
|
|
})
|
|
: t(`{{ groupsCount }} groups with access`, {
|
|
groupsCount,
|
|
count: groupsCount,
|
|
}),
|
|
delay: 250,
|
|
}}
|
|
width="auto"
|
|
height="auto"
|
|
>
|
|
<Fade>
|
|
<Facepile
|
|
size={AvatarSize.Large}
|
|
users={sortBy(collectionUsers, "lastActiveAt")}
|
|
overflow={overflow}
|
|
limit={limit}
|
|
/>
|
|
</Fade>
|
|
</NudeButton>
|
|
);
|
|
};
|
|
|
|
export default observer(MembershipPreview);
|