Repository: payloadcms/payload
File: packages/next/src/views/List/handleGroupBy.ts (TypeScript)

Code before:
    relationTo.forEach((rel) => {
      const config = clientConfig.collections.find((c) => c.slug === rel)
      populate[rel] = { [config?.admin?.useAsTitle || 'id']: true }
    })
  }

  // Get distinct values
  const distinct = await req.payload.findDistinct({
    collection: collectionSlug,
    depth: 1,
    field: groupByFieldPath,
    limit: query?.limit ? Number(query.limit) : undefined,
    locale: req.locale,
    overrideAccess: false,
    page: query?.page ? Number(query.page) : undefined,
    populate,
    req,
    sort: query?.groupBy,
    trash,
    where: whereWithMergedSearch,
  })

  // Process values - flatten hasMany arrays and deduplicate
  let processedValues = distinct.values || []

  if ('hasMany' in groupByField && groupByField?.hasMany) {
    const flattenedValues = []

    // For hasMany relationships, flatten arrays so each relationship becomes its own group
    // Example: document with [Category1, Category2] creates separate groups for Category1 and Category2
    for (const distinctValue of distinct.values || []) {
      const relationshipValue = distinctValue[groupByFieldPath]

      if (Array.isArray(relationshipValue) && relationshipValue.length > 0) {
        relationshipValue.forEach((individualRelationship) => {
          flattenedValues.push({ ...distinctValue, [groupByFieldPath]: individualRelationship })
        })
      } else {
        flattenedValues.push(distinctValue)
      }
    }

    // Remove duplicate groups (same relationship appearing in multiple documents)
    const seenRelationships = new Set()
    processedValues = flattenedValues.filter((value) => {
      const relationshipKey = getRelationshipKey(value[groupByFieldPath])
      if (seenRelationships.has(relationshipKey)) {
        return false
      }
      seenRelationships.add(relationshipKey)
      return true

Review comment (suggestion):
I agree with this behavior although this really ought to be done on the API level in the `findDistinct` operation. This is unnecessary overhead for the frontend.

Code after:
    relationTo.forEach((rel) => {
      const config = clientConfig.collections.find((c) => c.slug === rel)
      populate[rel] = { [config?.admin?.useAsTitle || 'id']: true }
    })
  }

  const distinct = await req.payload.findDistinct({
    collection: collectionSlug,
    depth: 1,
    field: groupByFieldPath,
    limit: query?.limit ? Number(query.limit) : undefined,
    locale: req.locale,
    overrideAccess: false,
    page: query?.page ? Number(query.page) : undefined,
    populate,
    req,
    sort: query?.groupBy,
    trash,
    where: whereWithMergedSearch,
  })

  const data = {
    ...distinct,
    docs: distinct.values?.map(() => ({})) || [],
    values: undefined,
  }

  await Promise.all(
    (distinct.values || []).map(async (distinctValue, i) => {
      const potentiallyPopulatedRelationship = distinctValue[groupByFieldPath]

      // Extract value or relationship ID for database query
      const valueOrRelationshipID = extractValueOrRelationshipID(potentiallyPopulatedRelationship)

      const groupData = await req.payload.find({
        collection: collectionSlug,
        depth: 0,
        draft: true,
        fallbackLocale: false,
        includeLockStatus: true,
        limit: query?.queryByGroup?.[valueOrRelationshipID]?.limit
          ? Number(query.queryByGroup[valueOrRelationshipID].limit)
          : undefined,
        locale: req.locale,
        overrideAccess: false,
        page: query?.queryByGroup?.[valueOrRelationshipID]?.page
          ? Number(query.queryByGroup[valueOrRelationshipID].page)
          : undefined,
        req,
        // Note: if we wanted to enable table-by-table sorting, we could use this:
        // sort: query?.queryByGroup?.[valueOrRelationshipID]?.sort,