Backfill and forward fill contracts with group info (#686)

* Backfill and forward fill contracts with group info

* No nested queries :(

* Fix filter

* Pass empty arrays instead of undefined
This commit is contained in:
Ian Philips 2022-07-22 16:28:53 -06:00 committed by GitHub
parent 5899c1f3c0
commit 5f074206de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 191 additions and 83 deletions

View File

@ -1,6 +1,7 @@
import { Answer } from './answer' import { Answer } from './answer'
import { Fees } from './fees' import { Fees } from './fees'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { GroupLink } from 'common/group'
export type AnyMechanism = DPM | CPMM export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
@ -46,6 +47,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
collectedFees: Fees collectedFees: Fees
groupSlugs?: string[] groupSlugs?: string[]
groupLinks?: GroupLink[]
uniqueBettorIds?: string[] uniqueBettorIds?: string[]
uniqueBettorCount?: number uniqueBettorCount?: number
popularityScore?: number popularityScore?: number

View File

@ -19,3 +19,11 @@ export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60 export const MAX_ID_LENGTH = 60
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
export const GROUP_CHAT_SLUG = 'chat' export const GROUP_CHAT_SLUG = 'chat'
export type GroupLink = {
slug: string
name: string
groupId: string
createdTime: number
userId?: string
}

View File

@ -74,7 +74,7 @@ service cloud.firestore {
match /contracts/{contractId} { match /contracts/{contractId} {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs']); .hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']);
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'question']) .hasOnly(['description', 'closeTime', 'question'])
&& resource.data.creatorId == request.auth.uid; && resource.data.creatorId == request.auth.uid;

View File

@ -3,6 +3,7 @@ import * as admin from 'firebase-admin'
import { Group } from 'common/group' import { Group } from 'common/group'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
const firestore = admin.firestore() const firestore = admin.firestore()
export const onDeleteGroup = functions.firestore export const onDeleteGroup = functions.firestore
@ -15,17 +16,21 @@ export const onDeleteGroup = functions.firestore
.collection('contracts') .collection('contracts')
.where('groupSlugs', 'array-contains', group.slug) .where('groupSlugs', 'array-contains', group.slug)
.get() .get()
console.log("contracts with group's slug:", contracts)
for (const doc of contracts.docs) { for (const doc of contracts.docs) {
const contract = doc.data() as Contract const contract = doc.data() as Contract
const newGroupLinks = contract.groupLinks?.filter(
(link) => link.slug !== group.slug
)
// remove the group from the contract // remove the group from the contract
await firestore await firestore
.collection('contracts') .collection('contracts')
.doc(contract.id) .doc(contract.id)
.update({ .update({
groupSlugs: (contract.groupSlugs ?? []).filter( groupSlugs: contract.groupSlugs?.filter((s) => s !== group.slug),
(groupSlug) => groupSlug !== group.slug groupLinks: newGroupLinks ?? [],
),
}) })
} }
}) })

View File

@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
import { initAdmin } from './script-init' import { initAdmin } from './script-init'
import { getValues, isProd } from '../utils' import { getValues, isProd } from '../utils'
import { CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories' import { CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories'
import { Group } from 'common/group' import { Group, GroupLink } from 'common/group'
import { uniq } from 'lodash' import { uniq } from 'lodash'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
@ -17,27 +17,6 @@ initAdmin()
const adminFirestore = admin.firestore() const adminFirestore = admin.firestore()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const addGroupIdToContracts = async () => {
const groups = await getValues<Group>(adminFirestore.collection('groups'))
const contracts = await getValues<Contract>(
adminFirestore.collection('contracts')
)
for (const group of groups) {
const groupContracts = contracts.filter((contract) =>
group.contractIds.includes(contract.id)
)
for (const contract of groupContracts) {
await adminFirestore
.collection('contracts')
.doc(contract.id)
.update({
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
})
}
}
}
const convertCategoriesToGroupsInternal = async (categories: string[]) => { const convertCategoriesToGroupsInternal = async (categories: string[]) => {
for (const category of categories) { for (const category of categories) {
const markets = await getValues<Contract>( const markets = await getValues<Contract>(
@ -93,18 +72,30 @@ const convertCategoriesToGroupsInternal = async (categories: string[]) => {
}) })
for (const market of markets) { for (const market of markets) {
if (market.groupLinks?.map((l) => l.groupId).includes(newGroup.id))
continue // already in that group
const newGroupLinks = [
...(market.groupLinks ?? []),
{
groupId: newGroup.id,
createdTime: Date.now(),
slug: newGroup.slug,
name: newGroup.name,
} as GroupLink,
]
await adminFirestore await adminFirestore
.collection('contracts') .collection('contracts')
.doc(market.id) .doc(market.id)
.update({ .update({
groupSlugs: uniq([...(market?.groupSlugs ?? []), newGroup.slug]), groupSlugs: uniq([...(market.groupSlugs ?? []), newGroup.slug]),
groupLinks: newGroupLinks,
}) })
} }
} }
} }
async function convertCategoriesToGroups() { async function convertCategoriesToGroups() {
// await addGroupIdToContracts()
// const defaultCategories = Object.values(DEFAULT_CATEGORIES) // const defaultCategories = Object.values(DEFAULT_CATEGORIES)
const moreCategories = ['world', 'culture'] const moreCategories = ['world', 'culture']
await convertCategoriesToGroupsInternal(moreCategories) await convertCategoriesToGroupsInternal(moreCategories)

View File

@ -0,0 +1,52 @@
import { getValues } from 'functions/src/utils'
import { Group } from 'common/group'
import { Contract } from 'common/contract'
import { initAdmin } from 'functions/src/scripts/script-init'
import * as admin from 'firebase-admin'
import { filterDefined } from 'common/util/array'
import { uniq } from 'lodash'
initAdmin()
const adminFirestore = admin.firestore()
const addGroupIdToContracts = async () => {
const groups = await getValues<Group>(adminFirestore.collection('groups'))
const contracts = await getValues<Contract>(
adminFirestore.collection('contracts')
)
for (const group of groups) {
const groupContracts = contracts.filter((contract) =>
group.contractIds.includes(contract.id)
)
for (const contract of groupContracts) {
const oldGroupLinks = contract.groupLinks?.filter(
(l) => l.slug != group.slug
)
const newGroupLinks = filterDefined([
...(oldGroupLinks ?? []),
group.id
? {
slug: group.slug,
name: group.name,
groupId: group.id,
createdTime: Date.now(),
}
: undefined,
])
await adminFirestore
.collection('contracts')
.doc(contract.id)
.update({
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
groupLinks: newGroupLinks,
})
}
}
}
if (require.main === module) {
addGroupIdToContracts()
.then(() => process.exit())
.catch(console.log)
}

View File

@ -23,11 +23,8 @@ import { useState } from 'react'
import { ContractInfoDialog } from './contract-info-dialog' import { ContractInfoDialog } from './contract-info-dialog'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import NewContractBadge from '../new-contract-badge' import NewContractBadge from '../new-contract-badge'
import { CATEGORY_LIST } from 'common/categories'
import { TagsList } from '../tags-list'
import { UserFollowButton } from '../follow-button' import { UserFollowButton } from '../follow-button'
import { DAY_MS } from 'common/util/time' import { DAY_MS } from 'common/util/time'
import { useGroupsWithContract } from 'web/hooks/use-group'
import { ShareIconButton } from 'web/components/share-icon-button' import { ShareIconButton } from 'web/components/share-icon-button'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { Editor } from '@tiptap/react' import { Editor } from '@tiptap/react'
@ -37,6 +34,8 @@ import { Button } from 'web/components/button'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { ContractGroupsList } from 'web/components/groups/contract-groups-list' import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
import { SiteLink } from 'web/components/site-link'
import { groupPath } from 'web/lib/firebase/groups'
export type ShowTime = 'resolve-date' | 'close-date' export type ShowTime = 'resolve-date' | 'close-date'
@ -50,15 +49,16 @@ export function MiscDetails(props: {
volume, volume,
volume24Hours, volume24Hours,
closeTime, closeTime,
tags,
isResolved, isResolved,
createdTime, createdTime,
resolutionTime, resolutionTime,
groupLinks,
} = contract } = contract
// Show at most one category that this contract is tagged by // Show at most one category that this contract is tagged by
const categories = CATEGORY_LIST.filter((category) => // const categories = CATEGORY_LIST.filter((category) =>
tags.map((t) => t.toLowerCase()).includes(category) // tags.map((t) => t.toLowerCase()).includes(category)
).slice(0, 1) // ).slice(0, 1)
const isNew = createdTime > Date.now() - DAY_MS && !isResolved const isNew = createdTime > Date.now() - DAY_MS && !isResolved
return ( return (
@ -80,13 +80,24 @@ export function MiscDetails(props: {
{fromNow(resolutionTime || 0)} {fromNow(resolutionTime || 0)}
</Row> </Row>
) : volume > 0 || !isNew ? ( ) : volume > 0 || !isNew ? (
<Row>{contractPool(contract)} pool</Row> <Row className={'shrink-0'}>{contractPool(contract)} pool</Row>
) : ( ) : (
<NewContractBadge /> <NewContractBadge />
)} )}
{categories.length > 0 && ( {/*{categories.length > 0 && (*/}
<TagsList className="text-gray-400" tags={categories} noLabel /> {/* <TagsList className="text-gray-400" tags={categories} noLabel />*/}
{/*)}*/}
{groupLinks && groupLinks.length > 0 && (
<SiteLink
href={groupPath(groupLinks[0].slug)}
className="text-sm text-gray-400"
>
<Row className={'line-clamp-1 flex-wrap items-center '}>
<UserGroupIcon className="mx-1 mb-0.5 inline h-4 w-4 shrink-0" />
{groupLinks[0].name}
</Row>
</SiteLink>
)} )}
</Row> </Row>
) )
@ -134,11 +145,12 @@ export function ContractDetails(props: {
disabled?: boolean disabled?: boolean
}) { }) {
const { contract, bets, isCreator, disabled } = props const { contract, bets, isCreator, disabled } = props
const { closeTime, creatorName, creatorUsername, creatorId } = contract const { closeTime, creatorName, creatorUsername, creatorId, groupLinks } =
contract
const { volumeLabel, resolvedDate } = contractMetrics(contract) const { volumeLabel, resolvedDate } = contractMetrics(contract)
const groups = useGroupsWithContract(contract) const groupToDisplay =
const groupToDisplay = groups[0] ?? null groupLinks?.sort((a, b) => a.createdTime - b.createdTime)[0] ?? null
const user = useUser() const user = useUser()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -172,11 +184,7 @@ export function ContractDetails(props: {
<Row> <Row>
<UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" /> <UserGroupIcon className="mx-1 inline h-5 w-5 shrink-0" />
<span className={'line-clamp-1'}> <span className={'line-clamp-1'}>
{contract.groupSlugs && !groupToDisplay {groupToDisplay ? groupToDisplay.name : 'No group'}
? ''
: groupToDisplay
? groupToDisplay.name
: 'No group'}
</span> </span>
</Row> </Row>
</Button> </Button>
@ -187,7 +195,11 @@ export function ContractDetails(props: {
'max-h-[70vh] min-h-[20rem] overflow-auto rounded bg-white p-6' 'max-h-[70vh] min-h-[20rem] overflow-auto rounded bg-white p-6'
} }
> >
<ContractGroupsList groups={groups} contract={contract} user={user} /> <ContractGroupsList
groupLinks={groupLinks ?? []}
contract={contract}
user={user}
/>
</Col> </Col>
</Modal> </Modal>

View File

@ -1,8 +1,7 @@
import { Group } from 'common/group'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import clsx from 'clsx' import clsx from 'clsx'
import { GroupLink } from 'web/pages/groups' import { GroupLinkItem } from 'web/pages/groups'
import { XIcon } from '@heroicons/react/outline' import { XIcon } from '@heroicons/react/outline'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
import { GroupSelector } from 'web/components/groups/group-selector' import { GroupSelector } from 'web/components/groups/group-selector'
@ -13,14 +12,16 @@ import {
import { User } from 'common/user' import { User } from 'common/user'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { GroupLink } from 'common/group'
import { useGroupsWithContract } from 'web/hooks/use-group'
export function ContractGroupsList(props: { export function ContractGroupsList(props: {
groups: Group[] groupLinks: GroupLink[]
contract: Contract contract: Contract
user: User | null | undefined user: User | null | undefined
}) { }) {
const { groups, user, contract } = props const { groupLinks, user, contract } = props
const groups = useGroupsWithContract(contract)
return ( return (
<Col className={'gap-2'}> <Col className={'gap-2'}>
<span className={'text-xl text-indigo-700'}> <span className={'text-xl text-indigo-700'}>
@ -33,10 +34,10 @@ export function ContractGroupsList(props: {
options={{ options={{
showSelector: true, showSelector: true,
showLabel: false, showLabel: false,
ignoreGroupIds: groups.map((g) => g.id), ignoreGroupIds: groupLinks.map((g) => g.groupId),
}} }}
setSelectedGroup={(group) => setSelectedGroup={(group) =>
group && addContractToGroup(group, contract) group && addContractToGroup(group, contract, user.id)
} }
selectedGroup={undefined} selectedGroup={undefined}
creator={user} creator={user}
@ -54,7 +55,7 @@ export function ContractGroupsList(props: {
className={clsx('items-center justify-between gap-2 p-2')} className={clsx('items-center justify-between gap-2 p-2')}
> >
<Row className="line-clamp-1 items-center gap-2"> <Row className="line-clamp-1 items-center gap-2">
<GroupLink group={group} /> <GroupLinkItem group={group} />
</Row> </Row>
{user && group.memberIds.includes(user.id) && ( {user && group.memberIds.includes(user.id) && (
<Button <Button

View File

@ -11,7 +11,7 @@ import { Modal } from 'web/components/layout/modal'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { joinGroup, leaveGroup } from 'web/lib/firebase/groups'
import { firebaseLogin } from 'web/lib/firebase/users' import { firebaseLogin } from 'web/lib/firebase/users'
import { GroupLink } from 'web/pages/groups' import { GroupLinkItem } from 'web/pages/groups'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
export function GroupsButton(props: { user: User }) { export function GroupsButton(props: { user: User }) {
@ -77,7 +77,7 @@ function GroupItem(props: { group: Group; className?: string }) {
return ( return (
<Row className={clsx('items-center justify-between gap-2 p-2', className)}> <Row className={clsx('items-center justify-between gap-2 p-2', className)}>
<Row className="line-clamp-1 items-center gap-2"> <Row className="line-clamp-1 items-center gap-2">
<GroupLink group={group} /> <GroupLinkItem group={group} />
</Row> </Row>
<JoinOrLeaveGroupButton group={group} /> <JoinOrLeaveGroupButton group={group} />
</Row> </Row>

View File

@ -1,17 +1,17 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
doc,
setDoc,
deleteDoc,
where,
collection, collection,
query, deleteDoc,
getDocs, doc,
orderBy,
getDoc, getDoc,
updateDoc, getDocs,
limit, limit,
orderBy,
query,
setDoc,
startAfter, startAfter,
updateDoc,
where,
} from 'firebase/firestore' } from 'firebase/firestore'
import { sortBy, sum } from 'lodash' import { sortBy, sum } from 'lodash'
@ -129,6 +129,7 @@ export async function listContractsByGroupSlug(
): Promise<Contract[]> { ): Promise<Contract[]> {
const q = query(contracts, where('groupSlugs', 'array-contains', slug)) const q = query(contracts, where('groupSlugs', 'array-contains', slug))
const snapshot = await getDocs(q) const snapshot = await getDocs(q)
console.log(snapshot.docs.map((doc) => doc.data()))
return snapshot.docs.map((doc) => doc.data()) return snapshot.docs.map((doc) => doc.data())
} }

View File

@ -7,7 +7,7 @@ import {
where, where,
} from 'firebase/firestore' } from 'firebase/firestore'
import { sortBy, uniq } from 'lodash' import { sortBy, uniq } from 'lodash'
import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group'
import { updateContract } from './contracts' import { updateContract } from './contracts'
import { import {
coll, coll,
@ -124,9 +124,27 @@ export async function leaveGroup(group: Group, userId: string): Promise<void> {
return await updateGroup(group, { memberIds: uniq(newMemberIds) }) return await updateGroup(group, { memberIds: uniq(newMemberIds) })
} }
export async function addContractToGroup(group: Group, contract: Contract) { export async function addContractToGroup(
group: Group,
contract: Contract,
userId: string
) {
if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // already in that group
const newGroupLinks = [
...(contract.groupLinks ?? []),
{
groupId: group.id,
createdTime: Date.now(),
slug: group.slug,
userId,
name: group.name,
} as GroupLink,
]
await updateContract(contract.id, { await updateContract(contract.id, {
groupSlugs: [...(contract.groupSlugs ?? []), group.slug], groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
groupLinks: newGroupLinks,
}) })
return await updateGroup(group, { return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contract.id]), contractIds: uniq([...group.contractIds, contract.id]),
@ -142,11 +160,15 @@ export async function removeContractFromGroup(
group: Group, group: Group,
contract: Contract contract: Contract
) { ) {
const newGroupSlugs = contract.groupSlugs?.filter( if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // not in that group
(slug) => slug !== group.slug
const newGroupLinks = contract.groupLinks?.filter(
(link) => link.slug !== group.slug
) )
await updateContract(contract.id, { await updateContract(contract.id, {
groupSlugs: uniq(newGroupSlugs ?? []), groupSlugs:
contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [],
groupLinks: newGroupLinks ?? [],
}) })
const newContractIds = group.contractIds.filter((id) => id !== contract.id) const newContractIds = group.contractIds.filter((id) => id !== contract.id)
return await updateGroup(group, { return await updateGroup(group, {
@ -159,8 +181,22 @@ export async function removeContractFromGroup(
}) })
} }
export async function setContractGroupSlugs(group: Group, contractId: string) { export async function setContractGroupLinks(
await updateContract(contractId, { groupSlugs: [group.slug] }) group: Group,
contractId: string,
userId: string
) {
await updateContract(contractId, {
groupLinks: [
{
groupId: group.id,
name: group.name,
slug: group.slug,
userId,
createdTime: Date.now(),
} as GroupLink,
],
})
return await updateGroup(group, { return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contractId]), contractIds: uniq([...group.contractIds, contractId]),
}) })

View File

@ -19,7 +19,7 @@ import {
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { removeUndefinedProps } from 'common/util/object' import { removeUndefinedProps } from 'common/util/object'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { getGroup, setContractGroupSlugs } from 'web/lib/firebase/groups' import { getGroup, setContractGroupLinks } from 'web/lib/firebase/groups'
import { Group } from 'common/group' import { Group } from 'common/group'
import { useTracking } from 'web/hooks/use-tracking' import { useTracking } from 'web/hooks/use-tracking'
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
@ -226,7 +226,7 @@ export function NewContract(props: {
isFree: false, isFree: false,
}) })
if (result && selectedGroup) { if (result && selectedGroup) {
await setContractGroupSlugs(selectedGroup, result.id) await setContractGroupLinks(selectedGroup, result.id, creator.id)
} }
await router.push(contractPath(result as Contract)) await router.push(contractPath(result as Contract))

View File

@ -1,4 +1,4 @@
import { take, sortBy, debounce } from 'lodash' import { debounce, sortBy, take } from 'lodash'
import PlusSmIcon from '@heroicons/react/solid/PlusSmIcon' import PlusSmIcon from '@heroicons/react/solid/PlusSmIcon'
import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Group, GROUP_CHAT_SLUG } from 'common/group'
@ -6,11 +6,11 @@ import { Page } from 'web/components/page'
import { listAllBets } from 'web/lib/firebase/bets' import { listAllBets } from 'web/lib/firebase/bets'
import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts'
import { import {
groupPath,
getGroupBySlug,
updateGroup,
joinGroup,
addContractToGroup, addContractToGroup,
getGroupBySlug,
groupPath,
joinGroup,
updateGroup,
} from 'web/lib/firebase/groups' } from 'web/lib/firebase/groups'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { UserLink } from 'web/components/user-page' import { UserLink } from 'web/components/user-page'
@ -543,7 +543,7 @@ function AddContractButton(props: { group: Group; user: User }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
async function addContractToCurrentGroup(contract: Contract) { async function addContractToCurrentGroup(contract: Contract) {
await addContractToGroup(group, contract) await addContractToGroup(group, contract, user.id)
setOpen(false) setOpen(false)
} }

View File

@ -1,4 +1,4 @@
import { sortBy, debounce } from 'lodash' import { debounce, sortBy } from 'lodash'
import Link from 'next/link' import Link from 'next/link'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Group } from 'common/group' import { Group } from 'common/group'
@ -238,7 +238,7 @@ function GroupMembersList(props: { group: Group }) {
) )
} }
export function GroupLink(props: { group: Group; className?: string }) { export function GroupLinkItem(props: { group: Group; className?: string }) {
const { group, className } = props const { group, className } = props
return ( return (