From d43258fd3f51a81a0dab5c2f220d56e43968083a Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Thu, 18 Jun 2026 09:33:55 -0400 Subject: [PATCH 1/6] fix: allow users to choose properties like data block table --- .../blocks/table/ranking-card-config.tsx | 84 +++++++++++++++++++ .../blocks/table/ranking-compose-screen.tsx | 13 ++- .../blocks/table/ranking-entry-row.tsx | 39 +++++++-- .../blocks/table/ranking-table-view.tsx | 6 +- .../blocks/table/table-block-ranking.tsx | 9 +- 5 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 apps/web/partials/blocks/table/ranking-card-config.tsx diff --git a/apps/web/partials/blocks/table/ranking-card-config.tsx b/apps/web/partials/blocks/table/ranking-card-config.tsx new file mode 100644 index 0000000000..5efc41e947 --- /dev/null +++ b/apps/web/partials/blocks/table/ranking-card-config.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { ContentIds, SystemIds } from '@geoprotocol/geo-sdk/lite'; + +import * as React from 'react'; + +import type { Source } from '~/core/blocks/data/source'; +import { useDataBlock } from '~/core/blocks/data/use-data-block'; +import type { Property } from '~/core/types'; + +const RANKING_CARD_EXCLUDED_PROPERTY_IDS = new Set([ + SystemIds.NAME_PROPERTY, + SystemIds.DESCRIPTION_PROPERTY, + SystemIds.COVER_PROPERTY, + ContentIds.AVATAR_PROPERTY, +]); + +export type RankingCardConfig = { + properties: Property[]; + source: Source; +}; + +export function selectRankingCardProperties(properties: Property[]): Property[] { + return properties.filter(property => !RANKING_CARD_EXCLUDED_PROPERTY_IDS.has(property.id)); +} + +const RankingCardConfigContext = React.createContext({ properties: [], source: { type: 'GEO' } }); + +export function RankingCardConfigProvider({ + value, + children, +}: { + value: RankingCardConfig; + children: React.ReactNode; +}) { + return {children}; +} + +export function useRankingCardConfig(): RankingCardConfig { + return React.useContext(RankingCardConfigContext); +} + +export function useRankingShownProperties() { + const { + properties, + shownColumnIds, + filterableProperties, + orderedShownColumnRelations, + toggleProperty, + hideAllShownPropertyColumns, + reorderShownPropertyRelations, + source, + } = useDataBlock(); + + const cardProperties = React.useMemo(() => selectRankingCardProperties(properties), [properties]); + + const cardConfig = React.useMemo( + () => ({ properties: cardProperties, source }), + [cardProperties, source] + ); + + const menuProps = React.useMemo( + () => ({ + sourceType: source.type, + filterableProperties, + shownColumnIds, + orderedShownColumnRelations, + toggleProperty, + hideAllShownPropertyColumns, + reorderShownPropertyRelations, + }), + [ + source.type, + filterableProperties, + shownColumnIds, + orderedShownColumnRelations, + toggleProperty, + hideAllShownPropertyColumns, + reorderShownPropertyRelations, + ] + ); + + return { cardConfig, menuProps }; +} diff --git a/apps/web/partials/blocks/table/ranking-compose-screen.tsx b/apps/web/partials/blocks/table/ranking-compose-screen.tsx index 4727aba7ee..0ab91a45a7 100644 --- a/apps/web/partials/blocks/table/ranking-compose-screen.tsx +++ b/apps/web/partials/blocks/table/ranking-compose-screen.tsx @@ -42,6 +42,7 @@ import type { SearchResult } from '~/core/types'; import { stepAtom } from '~/partials/onboarding/dialog'; +import { RankingCardConfigProvider, selectRankingCardProperties } from './ranking-card-config'; import { RankingComposeCreateEntityPanel } from './ranking-compose-create-entity-panel'; import { RankingComposeEntitySheet } from './ranking-compose-entity-sheet'; import { RankingComposeFullscreen } from './ranking-compose-fullscreen'; @@ -66,11 +67,15 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const searchParams = useSearchParams(); const parentEntityId = searchParams?.get('parentEntityId') ?? ''; const relationId = searchParams?.get('relationId') ?? ''; - const { name, entityId, filterState } = useDataBlock(); + const { name, entityId, filterState, properties, source } = useDataBlock(); const displayName = name?.trim() || 'Untitled ranking'; const createNewSpaceId = React.useMemo(() => resolveRankingSingleTargetSpaceId(filterState), [filterState]); + const cardConfig = React.useMemo( + () => ({ properties: selectRankingCardProperties(properties), source }), + [properties, source] + ); const { showOnboarding } = useOnboarding(); const composeAccessSpaceId = createNewSpaceId ?? spaceId; const { @@ -613,8 +618,8 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn } return ( - <> - + + setEntitySheetTarget(null)} /> {isMobile ? ( @@ -658,6 +663,6 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn )} - + ); } diff --git a/apps/web/partials/blocks/table/ranking-entry-row.tsx b/apps/web/partials/blocks/table/ranking-entry-row.tsx index 368c787664..b7c4ba59a5 100644 --- a/apps/web/partials/blocks/table/ranking-entry-row.tsx +++ b/apps/web/partials/blocks/table/ranking-entry-row.tsx @@ -12,6 +12,9 @@ import { ThumbGeoImage } from '~/design-system/geo-image'; import { PrefetchLink as Link } from '~/design-system/prefetch-link'; import { Skeleton } from '~/design-system/skeleton'; +import { useRankingCardConfig } from './ranking-card-config'; +import { TableBlockPropertyField } from './table-block-property-field'; + const ROW_NAME_CLASS = 'block truncate tracking-[-0.17px] text-text text-[19px] font-medium leading-[1.3]'; const ROW_DESCRIPTION_CLASS = 'break-words text-[16px] leading-[24px] text-grey-04'; @@ -20,12 +23,14 @@ export function RankingEntryRowSkeleton({ rank }: { rank?: number }) { const showLeadingRank = rank != null && rank > 0; return ( -
+
{showLeadingRank ? ( - {rank} + + {rank} + ) : null} -
+
@@ -34,17 +39,14 @@ export function RankingEntryRowSkeleton({ rank }: { rank?: number }) { } type Props = { - /** Omit or pass 0 to hide the rank indicator. */ rank?: number; entry: RankingEntryDisplay; spaceId: string; imageUrl?: string | null; - /** Aggregated Borda score — only rendered when `RANKING_POINTS_UI_ENABLED` (competition-linked). */ score?: number; pending?: boolean; /** When false, the name is plain text (e.g. compose pick rows that navigate on row click). */ linkToEntity?: boolean; - /** `leading` = rank column left of avatar; `avatar-badge` = overlapping corner badge (default). */ rankStyle?: 'leading' | 'avatar-badge'; }; @@ -58,6 +60,7 @@ export function RankingEntryRow({ linkToEntity = true, rankStyle = 'avatar-badge', }: Props) { + const { properties: cardProperties, source } = useRankingCardConfig(); const { avatarUrl, coverUrl } = useEntityMedia(entry.entityId, spaceId); const imageHint = entry.image; const directIpfs = @@ -84,12 +87,14 @@ export function RankingEntryRow({ ); return ( -
+
{showLeadingRank ? ( - {rank} + + {rank} + ) : null} {avatar} -
+
{linkToEntity ? ( {entry.name} @@ -109,6 +114,22 @@ export function RankingEntryRow({
) : null} {pending ?

Pending approval

: null} + {cardProperties.length > 0 ? ( +
+ {cardProperties.map(property => ( + {}} + browseListBody + disableLink + /> + ))} +
+ ) : null}
); diff --git a/apps/web/partials/blocks/table/ranking-table-view.tsx b/apps/web/partials/blocks/table/ranking-table-view.tsx index 9bb6bb33db..07f34e2cf2 100644 --- a/apps/web/partials/blocks/table/ranking-table-view.tsx +++ b/apps/web/partials/blocks/table/ranking-table-view.tsx @@ -7,6 +7,7 @@ import { useIsMobileLayout } from '~/core/hooks/use-is-mobile-layout'; import { Text } from '~/design-system/text'; import { RankingBlockBody } from './ranking-block-body'; +import { RankingCardConfigProvider, useRankingShownProperties } from './ranking-card-config'; import { RankingPeriodMetadata } from './ranking-period-metadata'; import { type InitialGlobalRanking, type InitialSharedRanking, useRankingBlockState } from './use-ranking-block-state'; @@ -33,6 +34,7 @@ export function RankingTableView({ initialSharedRanking, }: Props) { const isMobile = useIsMobileLayout(); + const { cardConfig } = useRankingShownProperties(); const state = useRankingBlockState({ spaceId, rankingStartDate, @@ -78,7 +80,9 @@ export function RankingTableView({
- + + +
); diff --git a/apps/web/partials/blocks/table/table-block-ranking.tsx b/apps/web/partials/blocks/table/table-block-ranking.tsx index 5c28dd32aa..e405baec5e 100644 --- a/apps/web/partials/blocks/table/table-block-ranking.tsx +++ b/apps/web/partials/blocks/table/table-block-ranking.tsx @@ -11,9 +11,11 @@ import { FilterTableWithFilters } from '~/design-system/icons/filter-table-with- import { Fullscreen } from '~/design-system/icons/full-screen'; import { RankingBlockBody } from './ranking-block-body'; +import { RankingCardConfigProvider, useRankingShownProperties } from './ranking-card-config'; import { RankingPeriodMetadata } from './ranking-period-metadata'; import { TableBlockContextMenu } from './table-block-context-menu'; import { TableBlockEditableFilters } from './table-block-editable-filters'; +import { TableBlockPropertiesMenu } from './table-block-properties-menu'; import { useRankingBlockState } from './use-ranking-block-state'; type Props = { @@ -24,6 +26,7 @@ type Props = { export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDate = '' }: Props) { const state = useRankingBlockState({ spaceId, rankingStartDate, rankingEndDate, paginateEmbeddedRanking: true }); + const { cardConfig, menuProps } = useRankingShownProperties(); const { canEdit, filterState, @@ -62,6 +65,8 @@ export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDa
+ {canEdit ? : null} + setIsFilterOpen(open => !open)} icon={filterState.length > 0 ? : } @@ -102,7 +107,9 @@ export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDa )} - + + +
); } From d967647bec0f778b194014b406c6955d9ab53397 Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Wed, 24 Jun 2026 11:20:00 -0400 Subject: [PATCH 2/6] fix: hydrate ranking entry properties so selected columns render on cards --- apps/web/core/blocks/ranking/use-ranking-entry-entities.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts index 0b63626b1b..a53bcc43bd 100644 --- a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts +++ b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts @@ -6,9 +6,10 @@ import { keepPreviousData } from '@tanstack/react-query'; import * as React from 'react'; import { EntityId } from '~/core/io/substream-schema'; -import { useQueryEntities } from '~/core/sync/use-store'; import type { Relation, Value } from '~/core/types'; import { compareBySpaceRank } from '~/core/utils/space/space-ranking'; +import { useHydrateEntities, useQueryEntities } from '~/core/sync/use-store'; +import { Entities } from '~/core/utils/entity'; export type RankingEntryDisplay = { entityId: string; @@ -20,6 +21,8 @@ export type RankingEntryDisplay = { export function useRankingEntryEntities(spaceId: string, entityIds: string[]) { const entityIdsKey = entityIds.filter(Boolean).join('|'); const stableIds = React.useMemo(() => [...new Set(entityIdsKey ? entityIdsKey.split('|') : [])], [entityIdsKey]); + + useHydrateEntities({ ids: stableIds, spaceId, enabled: stableIds.length > 0 }); const { entities, isLoading, isFetched } = useQueryEntities({ enabled: stableIds.length > 0, From a14b95f24e0818baef536dda7d1a54f35f633989 Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Thu, 25 Jun 2026 18:18:03 -0400 Subject: [PATCH 3/6] fix: ranking card image follows Cover/Avatar property choice and show property menu icon based on edit mode status --- .../ranking/ensure-ranking-shown-columns.ts | 1 - .../blocks/table/ranking-card-config.tsx | 19 ++++++++++++++++--- .../blocks/table/ranking-compose-screen.tsx | 16 ++++++++++++---- .../blocks/table/ranking-entry-row.tsx | 7 +++++-- .../blocks/table/table-block-ranking.tsx | 6 +++++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/apps/web/core/blocks/ranking/ensure-ranking-shown-columns.ts b/apps/web/core/blocks/ranking/ensure-ranking-shown-columns.ts index 17adb7c070..4b969b477a 100644 --- a/apps/web/core/blocks/ranking/ensure-ranking-shown-columns.ts +++ b/apps/web/core/blocks/ranking/ensure-ranking-shown-columns.ts @@ -5,7 +5,6 @@ import type { Mutator } from '~/core/sync/use-mutate'; import type { Relation } from '~/core/types'; const DEFAULT_SHOWN_COLUMNS: { id: string; name: string }[] = [ - { id: SystemIds.TYPES_PROPERTY, name: 'Types' }, { id: SystemIds.DESCRIPTION_PROPERTY, name: 'Description' }, ]; diff --git a/apps/web/partials/blocks/table/ranking-card-config.tsx b/apps/web/partials/blocks/table/ranking-card-config.tsx index 5efc41e947..b3acf159fe 100644 --- a/apps/web/partials/blocks/table/ranking-card-config.tsx +++ b/apps/web/partials/blocks/table/ranking-card-config.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import type { Source } from '~/core/blocks/data/source'; import { useDataBlock } from '~/core/blocks/data/use-data-block'; +import { ID } from '~/core/id'; import type { Property } from '~/core/types'; const RANKING_CARD_EXCLUDED_PROPERTY_IDS = new Set([ @@ -15,16 +16,27 @@ const RANKING_CARD_EXCLUDED_PROPERTY_IDS = new Set([ ContentIds.AVATAR_PROPERTY, ]); +export type RankingCardImageProperty = 'avatar' | 'cover'; + export type RankingCardConfig = { properties: Property[]; source: Source; + imageProperty: RankingCardImageProperty; }; export function selectRankingCardProperties(properties: Property[]): Property[] { return properties.filter(property => !RANKING_CARD_EXCLUDED_PROPERTY_IDS.has(property.id)); } -const RankingCardConfigContext = React.createContext({ properties: [], source: { type: 'GEO' } }); +export function selectRankingCardImageProperty(shownColumnIds: string[]): RankingCardImageProperty { + return shownColumnIds.some(id => ID.equals(id, SystemIds.COVER_PROPERTY)) ? 'cover' : 'avatar'; +} + +const RankingCardConfigContext = React.createContext({ + properties: [], + source: { type: 'GEO' }, + imageProperty: 'avatar', +}); export function RankingCardConfigProvider({ value, @@ -53,10 +65,11 @@ export function useRankingShownProperties() { } = useDataBlock(); const cardProperties = React.useMemo(() => selectRankingCardProperties(properties), [properties]); + const imageProperty = React.useMemo(() => selectRankingCardImageProperty(shownColumnIds), [shownColumnIds]); const cardConfig = React.useMemo( - () => ({ properties: cardProperties, source }), - [cardProperties, source] + () => ({ properties: cardProperties, source, imageProperty }), + [cardProperties, source, imageProperty] ); const menuProps = React.useMemo( diff --git a/apps/web/partials/blocks/table/ranking-compose-screen.tsx b/apps/web/partials/blocks/table/ranking-compose-screen.tsx index 0ab91a45a7..8cbf4d282f 100644 --- a/apps/web/partials/blocks/table/ranking-compose-screen.tsx +++ b/apps/web/partials/blocks/table/ranking-compose-screen.tsx @@ -42,7 +42,11 @@ import type { SearchResult } from '~/core/types'; import { stepAtom } from '~/partials/onboarding/dialog'; -import { RankingCardConfigProvider, selectRankingCardProperties } from './ranking-card-config'; +import { + RankingCardConfigProvider, + selectRankingCardImageProperty, + selectRankingCardProperties, +} from './ranking-card-config'; import { RankingComposeCreateEntityPanel } from './ranking-compose-create-entity-panel'; import { RankingComposeEntitySheet } from './ranking-compose-entity-sheet'; import { RankingComposeFullscreen } from './ranking-compose-fullscreen'; @@ -67,14 +71,18 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const searchParams = useSearchParams(); const parentEntityId = searchParams?.get('parentEntityId') ?? ''; const relationId = searchParams?.get('relationId') ?? ''; - const { name, entityId, filterState, properties, source } = useDataBlock(); + const { name, entityId, filterState, properties, shownColumnIds, source } = useDataBlock(); const displayName = name?.trim() || 'Untitled ranking'; const createNewSpaceId = React.useMemo(() => resolveRankingSingleTargetSpaceId(filterState), [filterState]); const cardConfig = React.useMemo( - () => ({ properties: selectRankingCardProperties(properties), source }), - [properties, source] + () => ({ + properties: selectRankingCardProperties(properties), + source, + imageProperty: selectRankingCardImageProperty(shownColumnIds), + }), + [properties, shownColumnIds, source] ); const { showOnboarding } = useOnboarding(); const composeAccessSpaceId = createNewSpaceId ?? spaceId; diff --git a/apps/web/partials/blocks/table/ranking-entry-row.tsx b/apps/web/partials/blocks/table/ranking-entry-row.tsx index b7c4ba59a5..e79cfccaad 100644 --- a/apps/web/partials/blocks/table/ranking-entry-row.tsx +++ b/apps/web/partials/blocks/table/ranking-entry-row.tsx @@ -60,13 +60,16 @@ export function RankingEntryRow({ linkToEntity = true, rankStyle = 'avatar-badge', }: Props) { - const { properties: cardProperties, source } = useRankingCardConfig(); + const { properties: cardProperties, source, imageProperty } = useRankingCardConfig(); const { avatarUrl, coverUrl } = useEntityMedia(entry.entityId, spaceId); const imageHint = entry.image; const directIpfs = imageHint && typeof imageHint === 'string' && imageHint.startsWith('ipfs://') ? imageHint : undefined; const lookedUpFromHint = useImageUrlFromEntity(imageHint && !directIpfs ? imageHint : undefined, spaceId); - const imageUrl = imageUrlOverride ?? directIpfs ?? lookedUpFromHint ?? avatarUrl ?? coverUrl; + const imageUrl = + imageProperty === 'cover' + ? (imageUrlOverride ?? coverUrl ?? avatarUrl ?? directIpfs ?? lookedUpFromHint) + : (imageUrlOverride ?? directIpfs ?? lookedUpFromHint ?? avatarUrl ?? coverUrl); const avatarImageValue = imageUrl ?? PLACEHOLDER_SPACE_IMAGE; const href = NavUtils.toEntity(spaceId, entry.entityId); const showRank = rank != null && rank > 0; diff --git a/apps/web/partials/blocks/table/table-block-ranking.tsx b/apps/web/partials/blocks/table/table-block-ranking.tsx index e405baec5e..298a08d048 100644 --- a/apps/web/partials/blocks/table/table-block-ranking.tsx +++ b/apps/web/partials/blocks/table/table-block-ranking.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import cx from 'classnames'; import { AnimatePresence, motion } from 'framer-motion'; +import { useUserIsEditing } from '~/core/hooks/use-user-is-editing'; + import { IconButton } from '~/design-system/button'; import { FilterTable } from '~/design-system/icons/filter-table'; import { FilterTableWithFilters } from '~/design-system/icons/filter-table-with-filters'; @@ -27,6 +29,8 @@ type Props = { export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDate = '' }: Props) { const state = useRankingBlockState({ spaceId, rankingStartDate, rankingEndDate, paginateEmbeddedRanking: true }); const { cardConfig, menuProps } = useRankingShownProperties(); + + const isEditing = useUserIsEditing(spaceId); const { canEdit, filterState, @@ -65,7 +69,7 @@ export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDa
- {canEdit ? : null} + {isEditing ? : null} setIsFilterOpen(open => !open)} From f06dcc6c739157a346037fcc265c7a65f1ed89f1 Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Thu, 25 Jun 2026 20:23:26 -0400 Subject: [PATCH 4/6] fix: run format --- apps/web/core/blocks/ranking/use-ranking-entry-entities.ts | 6 +++--- apps/web/partials/blocks/table/ranking-compose-screen.tsx | 2 +- apps/web/partials/blocks/table/table-block-ranking.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts index a53bcc43bd..7be1be8582 100644 --- a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts +++ b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts @@ -6,10 +6,10 @@ import { keepPreviousData } from '@tanstack/react-query'; import * as React from 'react'; import { EntityId } from '~/core/io/substream-schema'; -import type { Relation, Value } from '~/core/types'; -import { compareBySpaceRank } from '~/core/utils/space/space-ranking'; import { useHydrateEntities, useQueryEntities } from '~/core/sync/use-store'; +import type { Relation, Value } from '~/core/types'; import { Entities } from '~/core/utils/entity'; +import { compareBySpaceRank } from '~/core/utils/space/space-ranking'; export type RankingEntryDisplay = { entityId: string; @@ -21,7 +21,7 @@ export type RankingEntryDisplay = { export function useRankingEntryEntities(spaceId: string, entityIds: string[]) { const entityIdsKey = entityIds.filter(Boolean).join('|'); const stableIds = React.useMemo(() => [...new Set(entityIdsKey ? entityIdsKey.split('|') : [])], [entityIdsKey]); - + useHydrateEntities({ ids: stableIds, spaceId, enabled: stableIds.length > 0 }); const { entities, isLoading, isFetched } = useQueryEntities({ diff --git a/apps/web/partials/blocks/table/ranking-compose-screen.tsx b/apps/web/partials/blocks/table/ranking-compose-screen.tsx index 8cbf4d282f..4045f7335e 100644 --- a/apps/web/partials/blocks/table/ranking-compose-screen.tsx +++ b/apps/web/partials/blocks/table/ranking-compose-screen.tsx @@ -627,7 +627,7 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn return ( - + setEntitySheetTarget(null)} /> {isMobile ? ( diff --git a/apps/web/partials/blocks/table/table-block-ranking.tsx b/apps/web/partials/blocks/table/table-block-ranking.tsx index 298a08d048..033ca29068 100644 --- a/apps/web/partials/blocks/table/table-block-ranking.tsx +++ b/apps/web/partials/blocks/table/table-block-ranking.tsx @@ -29,7 +29,7 @@ type Props = { export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDate = '' }: Props) { const state = useRankingBlockState({ spaceId, rankingStartDate, rankingEndDate, paginateEmbeddedRanking: true }); const { cardConfig, menuProps } = useRankingShownProperties(); - + const isEditing = useUserIsEditing(spaceId); const { canEdit, From b66c87faf7e85fb852cd44b08b1f5769b799baa1 Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Thu, 25 Jun 2026 20:46:55 -0400 Subject: [PATCH 5/6] fix: resolve ranking card image by chosen Cover/Avatar property at the data layer --- .../use-ranking-entry-entities.test.ts | 33 +++++++++++++- .../ranking/use-ranking-entry-entities.ts | 45 +++++++++++-------- .../blocks/table/ranking-compose-screen.tsx | 16 ++++--- .../blocks/table/ranking-entry-row.tsx | 6 +-- .../blocks/table/use-ranking-block-state.ts | 10 +++-- 5 files changed, 79 insertions(+), 31 deletions(-) diff --git a/apps/web/core/blocks/ranking/use-ranking-entry-entities.test.ts b/apps/web/core/blocks/ranking/use-ranking-entry-entities.test.ts index 0b080f6f49..3b1d0a6014 100644 --- a/apps/web/core/blocks/ranking/use-ranking-entry-entities.test.ts +++ b/apps/web/core/blocks/ranking/use-ranking-entry-entities.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'; import { EntityId } from '~/core/io/substream-schema'; import type { Relation, Value } from '~/core/types'; -import { pickImage, pickRelationBySpace, pickValueBySpace } from './use-ranking-entry-entities'; +import { pickImage, pickImageByPreference, pickRelationBySpace, pickValueBySpace } from './use-ranking-entry-entities'; // Real space IDs from getSpaceRank: Root=0, Crypto=2, Software=5. const ROOT_SPACE = 'a19c345ab9866679b001d7d2138d88a1'; @@ -139,3 +139,34 @@ describe('pickImage', () => { expect(pickImage([], UNRANKED_SPACE)).toBeNull(); }); }); + +describe('pickImageByPreference (cover)', () => { + it('prefers a current-space cover over a current-space avatar', () => { + const relations = [ + relation(AVATAR_TYPE, UNRANKED_SPACE, 'ipfs://current-avatar'), + relation(COVER_TYPE, UNRANKED_SPACE, 'ipfs://current-cover'), + ]; + expect(pickImageByPreference(relations, UNRANKED_SPACE, 'cover')).toBe('ipfs://current-cover'); + }); + + it('falls back to the avatar when there is no cover', () => { + const relations = [relation(AVATAR_TYPE, UNRANKED_SPACE, 'ipfs://current-avatar')]; + expect(pickImageByPreference(relations, UNRANKED_SPACE, 'cover')).toBe('ipfs://current-avatar'); + }); + + it('prefers a current-space cover over a ranked-space avatar', () => { + const relations = [ + relation(AVATAR_TYPE, ROOT_SPACE, 'ipfs://root-avatar'), + relation(COVER_TYPE, UNRANKED_SPACE, 'ipfs://current-cover'), + ]; + expect(pickImageByPreference(relations, UNRANKED_SPACE, 'cover')).toBe('ipfs://current-cover'); + }); + + it('matches the avatar-preferring pickImage when preference is avatar', () => { + const relations = [ + relation(AVATAR_TYPE, ROOT_SPACE, 'ipfs://root-avatar'), + relation(COVER_TYPE, UNRANKED_SPACE, 'ipfs://current-cover'), + ]; + expect(pickImageByPreference(relations, UNRANKED_SPACE, 'avatar')).toBe(pickImage(relations, UNRANKED_SPACE)); + }); +}); diff --git a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts index 7be1be8582..d766bd7ea3 100644 --- a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts +++ b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts @@ -8,7 +8,6 @@ import * as React from 'react'; import { EntityId } from '~/core/io/substream-schema'; import { useHydrateEntities, useQueryEntities } from '~/core/sync/use-store'; import type { Relation, Value } from '~/core/types'; -import { Entities } from '~/core/utils/entity'; import { compareBySpaceRank } from '~/core/utils/space/space-ranking'; export type RankingEntryDisplay = { @@ -18,7 +17,13 @@ export type RankingEntryDisplay = { image: string | null; }; -export function useRankingEntryEntities(spaceId: string, entityIds: string[]) { +export type RankingImagePreference = 'avatar' | 'cover'; + +export function useRankingEntryEntities( + spaceId: string, + entityIds: string[], + imageProperty: RankingImagePreference = 'avatar' +) { const entityIdsKey = entityIds.filter(Boolean).join('|'); const stableIds = React.useMemo(() => [...new Set(entityIdsKey ? entityIdsKey.split('|') : [])], [entityIdsKey]); @@ -43,11 +48,11 @@ export function useRankingEntryEntities(spaceId: string, entityIds: string[]) { entityId: id, name: pickValueBySpace(entity.values, SystemIds.NAME_PROPERTY, spaceId) ?? 'Untitled', description: pickValueBySpace(entity.values, SystemIds.DESCRIPTION_PROPERTY, spaceId), - image: pickImage(entity.relations, spaceId), + image: pickImageByPreference(entity.relations, spaceId, imageProperty), }; }) .filter((e): e is RankingEntryDisplay => e != null), - [byId, stableIds, spaceId] + [byId, stableIds, spaceId, imageProperty] ); return { entries, isLoading: isLoading && !isFetched }; @@ -78,23 +83,27 @@ export function pickRelationBySpace(relations: Relation[], typeId: string, curre ); } -/** - * Resolve an image URL for a ranking card. Order: - * current-space avatar → current-space cover → ranked-space avatar → ranked-space cover. - * - * Relations whose target image has no URL value are skipped at every step so a - * placeholder avatar in the current space can't mask a usable image from another space. - */ -export function pickImage(relations: Relation[], currentSpaceId: string): string | null { +export function pickImageByPreference( + relations: Relation[], + currentSpaceId: string, + preference: RankingImagePreference +): string | null { const withValue = relations.filter(r => r.toEntity.value); - const avatar = pickRelationBySpace(withValue, EntityId(ContentIds.AVATAR_PROPERTY), currentSpaceId); - if (avatar?.spaceId === currentSpaceId) return avatar.toEntity.value; + const preferredType = preference === 'cover' ? SystemIds.COVER_PROPERTY : ContentIds.AVATAR_PROPERTY; + const otherType = preference === 'cover' ? ContentIds.AVATAR_PROPERTY : SystemIds.COVER_PROPERTY; - const cover = pickRelationBySpace(withValue, EntityId(SystemIds.COVER_PROPERTY), currentSpaceId); - if (cover?.spaceId === currentSpaceId) return cover.toEntity.value; + const preferred = pickRelationBySpace(withValue, EntityId(preferredType), currentSpaceId); + if (preferred?.spaceId === currentSpaceId) return preferred.toEntity.value; - if (avatar) return avatar.toEntity.value; - if (cover) return cover.toEntity.value; + const other = pickRelationBySpace(withValue, EntityId(otherType), currentSpaceId); + if (other?.spaceId === currentSpaceId) return other.toEntity.value; + + if (preferred) return preferred.toEntity.value; + if (other) return other.toEntity.value; return null; } + +export function pickImage(relations: Relation[], currentSpaceId: string): string | null { + return pickImageByPreference(relations, currentSpaceId, 'avatar'); +} diff --git a/apps/web/partials/blocks/table/ranking-compose-screen.tsx b/apps/web/partials/blocks/table/ranking-compose-screen.tsx index 4045f7335e..41036b8c97 100644 --- a/apps/web/partials/blocks/table/ranking-compose-screen.tsx +++ b/apps/web/partials/blocks/table/ranking-compose-screen.tsx @@ -76,13 +76,14 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const createNewSpaceId = React.useMemo(() => resolveRankingSingleTargetSpaceId(filterState), [filterState]); + const cardImageProperty = React.useMemo(() => selectRankingCardImageProperty(shownColumnIds), [shownColumnIds]); const cardConfig = React.useMemo( () => ({ properties: selectRankingCardProperties(properties), source, - imageProperty: selectRankingCardImageProperty(shownColumnIds), + imageProperty: cardImageProperty, }), - [properties, shownColumnIds, source] + [properties, cardImageProperty, source] ); const { showOnboarding } = useOnboarding(); const composeAccessSpaceId = createNewSpaceId ?? spaceId; @@ -179,7 +180,8 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const { entries: rankableEntries, isLoading: isLoadingRankableEntries } = useRankingEntryEntities( spaceId, - allRankableEntityIds + allRankableEntityIds, + cardImageProperty ); const [orderedIds, setOrderedIds] = React.useState(mySubmission?.orderedEntityIds ?? []); @@ -252,7 +254,11 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const filteredRankedIds = isSearchActive ? searchRankedIds : browseRankedIds; const filteredUnrankedIds = isSearchActive ? searchUnrankedIds : browseUnrankedIds; - const { entries: searchEntries } = useRankingEntryEntities(spaceId, isSearchActive ? searchEntityIds : []); + const { entries: searchEntries } = useRankingEntryEntities( + spaceId, + isSearchActive ? searchEntityIds : [], + cardImageProperty + ); const mySubmissionIdsKey = (mySubmission?.orderedEntityIds ?? []).join('|'); @@ -268,7 +274,7 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const displayMyEntityIds = orderedIds; - const { entries: myEntries } = useRankingEntryEntities(spaceId, displayMyEntityIds); + const { entries: myEntries } = useRankingEntryEntities(spaceId, displayMyEntityIds, cardImageProperty); const myEntriesById = React.useMemo(() => new Map(myEntries.map(e => [e.entityId, e])), [myEntries]); const rankableEntriesByIdRaw = React.useMemo( diff --git a/apps/web/partials/blocks/table/ranking-entry-row.tsx b/apps/web/partials/blocks/table/ranking-entry-row.tsx index e79cfccaad..eccb70e9e5 100644 --- a/apps/web/partials/blocks/table/ranking-entry-row.tsx +++ b/apps/web/partials/blocks/table/ranking-entry-row.tsx @@ -66,10 +66,8 @@ export function RankingEntryRow({ const directIpfs = imageHint && typeof imageHint === 'string' && imageHint.startsWith('ipfs://') ? imageHint : undefined; const lookedUpFromHint = useImageUrlFromEntity(imageHint && !directIpfs ? imageHint : undefined, spaceId); - const imageUrl = - imageProperty === 'cover' - ? (imageUrlOverride ?? coverUrl ?? avatarUrl ?? directIpfs ?? lookedUpFromHint) - : (imageUrlOverride ?? directIpfs ?? lookedUpFromHint ?? avatarUrl ?? coverUrl); + const mediaFallback = imageProperty === 'cover' ? (coverUrl ?? avatarUrl) : (avatarUrl ?? coverUrl); + const imageUrl = imageUrlOverride ?? directIpfs ?? lookedUpFromHint ?? mediaFallback; const avatarImageValue = imageUrl ?? PLACEHOLDER_SPACE_IMAGE; const href = NavUtils.toEntity(spaceId, entry.entityId); const showRank = rank != null && rank > 0; diff --git a/apps/web/partials/blocks/table/use-ranking-block-state.ts b/apps/web/partials/blocks/table/use-ranking-block-state.ts index 4a51b30ed7..9f12696e89 100644 --- a/apps/web/partials/blocks/table/use-ranking-block-state.ts +++ b/apps/web/partials/blocks/table/use-ranking-block-state.ts @@ -54,6 +54,7 @@ import { useEditorStoreLite } from '~/core/state/editor/use-editor'; import { stepAtom } from '~/partials/onboarding/dialog'; +import { selectRankingCardImageProperty } from './ranking-card-config'; import { postOnboardingRedirectAtom } from '~/atoms/post-onboarding-redirect'; import { rankingComposeReturnHrefAtom } from '~/atoms/ranking-compose-return'; @@ -120,7 +121,8 @@ export function useRankingBlockState({ const setRankingComposeReturnHref = useSetAtom(rankingComposeReturnHrefAtom); const setStep = useSetAtom(stepAtom); - const { name, entityId, relationId, rows } = useDataBlock(); + const { name, entityId, relationId, rows, shownColumnIds } = useDataBlock(); + const cardImageProperty = React.useMemo(() => selectRankingCardImageProperty(shownColumnIds), [shownColumnIds]); const { id: parentEntityId } = useEditorInstance(); const { blockRelations } = useEditorStoreLite(); @@ -336,12 +338,14 @@ export function useRankingBlockState({ // there; for embedded blocks it's one bounded, cached batch query. const { entries: globalEntries, isLoading: isLoadingGlobalEntries } = useRankingEntryEntities( spaceId, - globalDisplayEntityIds + globalDisplayEntityIds, + cardImageProperty ); const { entries: myEntries, isLoading: isLoadingMyEntries } = useRankingEntryEntities( spaceId, - myRankingListEntityIds + myRankingListEntityIds, + cardImageProperty ); const globalEntriesById = React.useMemo(() => new Map(globalEntries.map(e => [e.entityId, e])), [globalEntries]); From ae0361a5f8251b3e044f98fa69aa2d13d2d3a7a5 Mon Sep 17 00:00:00 2001 From: Juan Mardikian Date: Tue, 21 Jul 2026 23:46:13 -0400 Subject: [PATCH 6/6] fix: single card config builder and one useDataBlock per ranking surface --- .../ranking/use-ranking-entry-entities.ts | 4 +- .../blocks/table/ranking-card-config.test.ts | 95 +++++++++++++++++++ .../blocks/table/ranking-card-config.tsx | 80 ++++++---------- .../blocks/table/ranking-compose-screen.tsx | 16 +--- .../blocks/table/ranking-entry-row.tsx | 5 +- .../blocks/table/ranking-table-view.tsx | 4 +- .../table/table-block-property-field.tsx | 7 +- .../blocks/table/table-block-ranking.tsx | 5 +- .../blocks/table/use-ranking-block-state.ts | 47 ++++++++- 9 files changed, 185 insertions(+), 78 deletions(-) create mode 100644 apps/web/partials/blocks/table/ranking-card-config.test.ts diff --git a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts index d766bd7ea3..400c888c72 100644 --- a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts +++ b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts @@ -6,7 +6,7 @@ import { keepPreviousData } from '@tanstack/react-query'; import * as React from 'react'; import { EntityId } from '~/core/io/substream-schema'; -import { useHydrateEntities, useQueryEntities } from '~/core/sync/use-store'; +import { useQueryEntities } from '~/core/sync/use-store'; import type { Relation, Value } from '~/core/types'; import { compareBySpaceRank } from '~/core/utils/space/space-ranking'; @@ -27,8 +27,6 @@ export function useRankingEntryEntities( const entityIdsKey = entityIds.filter(Boolean).join('|'); const stableIds = React.useMemo(() => [...new Set(entityIdsKey ? entityIdsKey.split('|') : [])], [entityIdsKey]); - useHydrateEntities({ ids: stableIds, spaceId, enabled: stableIds.length > 0 }); - const { entities, isLoading, isFetched } = useQueryEntities({ enabled: stableIds.length > 0, where: { id: { in: stableIds } }, diff --git a/apps/web/partials/blocks/table/ranking-card-config.test.ts b/apps/web/partials/blocks/table/ranking-card-config.test.ts new file mode 100644 index 0000000000..100ee95772 --- /dev/null +++ b/apps/web/partials/blocks/table/ranking-card-config.test.ts @@ -0,0 +1,95 @@ +import { ContentIds, SystemIds } from '@geoprotocol/geo-sdk/lite'; + +import { describe, expect, it } from 'vitest'; + +import type { Property } from '~/core/types'; + +import { + buildRankingCardConfig, + selectRankingCardImageProperty, + selectRankingCardProperties, +} from './ranking-card-config'; + +function prop(id: string, name: string | null = null): Property { + return { id, name, dataType: 'RELATION' }; +} + +function dashed(id: string): string { + return [id.slice(0, 8), id.slice(8, 12), id.slice(12, 16), id.slice(16, 20), id.slice(20)].join('-'); +} + +describe('selectRankingCardProperties', () => { + const custom = prop('custom-prop', 'Custom'); + + it('drops name, description, cover and avatar', () => { + const properties = [ + prop(SystemIds.NAME_PROPERTY), + prop(SystemIds.DESCRIPTION_PROPERTY), + prop(SystemIds.COVER_PROPERTY), + prop(ContentIds.AVATAR_PROPERTY), + custom, + ]; + + expect(selectRankingCardProperties(properties)).toEqual([custom]); + }); + + it('drops excluded properties whose ids arrive dashed', () => { + const properties = [prop(dashed(SystemIds.COVER_PROPERTY)), prop(dashed(ContentIds.AVATAR_PROPERTY)), custom]; + + expect(selectRankingCardProperties(properties)).toEqual([custom]); + }); +}); + +describe('selectRankingCardImageProperty', () => { + it('prefers cover when the cover column is shown', () => { + expect(selectRankingCardImageProperty([SystemIds.COVER_PROPERTY])).toBe('cover'); + expect(selectRankingCardImageProperty([dashed(SystemIds.COVER_PROPERTY)])).toBe('cover'); + }); + + it('falls back to avatar otherwise', () => { + expect(selectRankingCardImageProperty([ContentIds.AVATAR_PROPERTY])).toBe('avatar'); + expect(selectRankingCardImageProperty([])).toBe('avatar'); + }); +}); + +describe('selector agreement', () => { + it.each([SystemIds.COVER_PROPERTY, dashed(SystemIds.COVER_PROPERTY)])('cover id %s is never both', coverId => { + expect(selectRankingCardImageProperty([coverId])).toBe('cover'); + expect(selectRankingCardProperties([prop(coverId)])).toEqual([]); + }); +}); + +describe('buildRankingCardConfig', () => { + const source = { type: 'GEO' } as const; + const custom = prop('custom-prop', 'Custom'); + + it('applies both selectors and passes source through', () => { + expect( + buildRankingCardConfig({ + properties: [prop(SystemIds.COVER_PROPERTY), custom], + shownColumnIds: [SystemIds.COVER_PROPERTY, custom.id], + source, + }) + ).toEqual({ properties: [custom], source, imageProperty: 'cover' }); + }); + + it('falls back to avatar when no cover column is shown', () => { + expect(buildRankingCardConfig({ properties: [custom], shownColumnIds: [custom.id], source }).imageProperty).toBe( + 'avatar' + ); + }); + + it.each([SystemIds.COVER_PROPERTY, dashed(SystemIds.COVER_PROPERTY)])( + 'never emits the image property as a field (%s)', + coverId => { + const config = buildRankingCardConfig({ + properties: [prop(coverId), custom], + shownColumnIds: [coverId, custom.id], + source, + }); + + expect(config.imageProperty).toBe('cover'); + expect(config.properties).toEqual([custom]); + } + ); +}); diff --git a/apps/web/partials/blocks/table/ranking-card-config.tsx b/apps/web/partials/blocks/table/ranking-card-config.tsx index b3acf159fe..8df9b51b2b 100644 --- a/apps/web/partials/blocks/table/ranking-card-config.tsx +++ b/apps/web/partials/blocks/table/ranking-card-config.tsx @@ -5,16 +5,17 @@ import { ContentIds, SystemIds } from '@geoprotocol/geo-sdk/lite'; import * as React from 'react'; import type { Source } from '~/core/blocks/data/source'; -import { useDataBlock } from '~/core/blocks/data/use-data-block'; import { ID } from '~/core/id'; import type { Property } from '~/core/types'; -const RANKING_CARD_EXCLUDED_PROPERTY_IDS = new Set([ - SystemIds.NAME_PROPERTY, - SystemIds.DESCRIPTION_PROPERTY, - SystemIds.COVER_PROPERTY, - ContentIds.AVATAR_PROPERTY, -]); +/** + * Normalized so membership matches `ID.equals` semantics + */ +const RANKING_CARD_EXCLUDED_PROPERTY_IDS = new Set( + [SystemIds.NAME_PROPERTY, SystemIds.DESCRIPTION_PROPERTY, SystemIds.COVER_PROPERTY, ContentIds.AVATAR_PROPERTY].map( + id => ID.uuidToHex(id) + ) +); export type RankingCardImageProperty = 'avatar' | 'cover'; @@ -25,13 +26,32 @@ export type RankingCardConfig = { }; export function selectRankingCardProperties(properties: Property[]): Property[] { - return properties.filter(property => !RANKING_CARD_EXCLUDED_PROPERTY_IDS.has(property.id)); + return properties.filter(property => !RANKING_CARD_EXCLUDED_PROPERTY_IDS.has(ID.uuidToHex(property.id))); } export function selectRankingCardImageProperty(shownColumnIds: string[]): RankingCardImageProperty { return shownColumnIds.some(id => ID.equals(id, SystemIds.COVER_PROPERTY)) ? 'cover' : 'avatar'; } +/** + * Single definition of a ranking card's config. Callers already hold the data block fields + */ +export function buildRankingCardConfig({ + properties, + shownColumnIds, + source, +}: { + properties: Property[]; + shownColumnIds: string[]; + source: Source; +}): RankingCardConfig { + return { + properties: selectRankingCardProperties(properties), + source, + imageProperty: selectRankingCardImageProperty(shownColumnIds), + }; +} + const RankingCardConfigContext = React.createContext({ properties: [], source: { type: 'GEO' }, @@ -51,47 +71,3 @@ export function RankingCardConfigProvider({ export function useRankingCardConfig(): RankingCardConfig { return React.useContext(RankingCardConfigContext); } - -export function useRankingShownProperties() { - const { - properties, - shownColumnIds, - filterableProperties, - orderedShownColumnRelations, - toggleProperty, - hideAllShownPropertyColumns, - reorderShownPropertyRelations, - source, - } = useDataBlock(); - - const cardProperties = React.useMemo(() => selectRankingCardProperties(properties), [properties]); - const imageProperty = React.useMemo(() => selectRankingCardImageProperty(shownColumnIds), [shownColumnIds]); - - const cardConfig = React.useMemo( - () => ({ properties: cardProperties, source, imageProperty }), - [cardProperties, source, imageProperty] - ); - - const menuProps = React.useMemo( - () => ({ - sourceType: source.type, - filterableProperties, - shownColumnIds, - orderedShownColumnRelations, - toggleProperty, - hideAllShownPropertyColumns, - reorderShownPropertyRelations, - }), - [ - source.type, - filterableProperties, - shownColumnIds, - orderedShownColumnRelations, - toggleProperty, - hideAllShownPropertyColumns, - reorderShownPropertyRelations, - ] - ); - - return { cardConfig, menuProps }; -} diff --git a/apps/web/partials/blocks/table/ranking-compose-screen.tsx b/apps/web/partials/blocks/table/ranking-compose-screen.tsx index 41036b8c97..81beaf9005 100644 --- a/apps/web/partials/blocks/table/ranking-compose-screen.tsx +++ b/apps/web/partials/blocks/table/ranking-compose-screen.tsx @@ -42,11 +42,7 @@ import type { SearchResult } from '~/core/types'; import { stepAtom } from '~/partials/onboarding/dialog'; -import { - RankingCardConfigProvider, - selectRankingCardImageProperty, - selectRankingCardProperties, -} from './ranking-card-config'; +import { RankingCardConfigProvider, buildRankingCardConfig } from './ranking-card-config'; import { RankingComposeCreateEntityPanel } from './ranking-compose-create-entity-panel'; import { RankingComposeEntitySheet } from './ranking-compose-entity-sheet'; import { RankingComposeFullscreen } from './ranking-compose-fullscreen'; @@ -76,15 +72,11 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const createNewSpaceId = React.useMemo(() => resolveRankingSingleTargetSpaceId(filterState), [filterState]); - const cardImageProperty = React.useMemo(() => selectRankingCardImageProperty(shownColumnIds), [shownColumnIds]); const cardConfig = React.useMemo( - () => ({ - properties: selectRankingCardProperties(properties), - source, - imageProperty: cardImageProperty, - }), - [properties, cardImageProperty, source] + () => buildRankingCardConfig({ properties, shownColumnIds, source }), + [properties, shownColumnIds, source] ); + const cardImageProperty = cardConfig.imageProperty; const { showOnboarding } = useOnboarding(); const composeAccessSpaceId = createNewSpaceId ?? spaceId; const { diff --git a/apps/web/partials/blocks/table/ranking-entry-row.tsx b/apps/web/partials/blocks/table/ranking-entry-row.tsx index eccb70e9e5..71d9beec5e 100644 --- a/apps/web/partials/blocks/table/ranking-entry-row.tsx +++ b/apps/web/partials/blocks/table/ranking-entry-row.tsx @@ -30,7 +30,7 @@ export function RankingEntryRowSkeleton({ rank }: { rank?: number }) { ) : null} -
+
@@ -39,6 +39,7 @@ export function RankingEntryRowSkeleton({ rank }: { rank?: number }) { } type Props = { + /** Omit or pass 0 to hide the rank indicator. */ rank?: number; entry: RankingEntryDisplay; spaceId: string; @@ -95,7 +96,7 @@ export function RankingEntryRow({ ) : null} {avatar} -
+
{linkToEntity ? ( {entry.name} diff --git a/apps/web/partials/blocks/table/ranking-table-view.tsx b/apps/web/partials/blocks/table/ranking-table-view.tsx index 07f34e2cf2..d513334fc4 100644 --- a/apps/web/partials/blocks/table/ranking-table-view.tsx +++ b/apps/web/partials/blocks/table/ranking-table-view.tsx @@ -7,7 +7,7 @@ import { useIsMobileLayout } from '~/core/hooks/use-is-mobile-layout'; import { Text } from '~/design-system/text'; import { RankingBlockBody } from './ranking-block-body'; -import { RankingCardConfigProvider, useRankingShownProperties } from './ranking-card-config'; +import { RankingCardConfigProvider } from './ranking-card-config'; import { RankingPeriodMetadata } from './ranking-period-metadata'; import { type InitialGlobalRanking, type InitialSharedRanking, useRankingBlockState } from './use-ranking-block-state'; @@ -34,7 +34,6 @@ export function RankingTableView({ initialSharedRanking, }: Props) { const isMobile = useIsMobileLayout(); - const { cardConfig } = useRankingShownProperties(); const state = useRankingBlockState({ spaceId, rankingStartDate, @@ -46,6 +45,7 @@ export function RankingTableView({ initialSharedRanking, }); const { + cardConfig, displayName, periodState, periodLabel, diff --git a/apps/web/partials/blocks/table/table-block-property-field.tsx b/apps/web/partials/blocks/table/table-block-property-field.tsx index 7d6e78586c..00d3bab7d8 100644 --- a/apps/web/partials/blocks/table/table-block-property-field.tsx +++ b/apps/web/partials/blocks/table/table-block-property-field.tsx @@ -13,7 +13,7 @@ import { Property } from '~/core/types'; import { isUrlTemplate } from '~/core/utils/url-template'; import { SquareButton } from '~/design-system/button'; -import { Checkbox, getChecked } from '~/design-system/checkbox'; +import { Checkbox, CheckboxVisual, getChecked } from '~/design-system/checkbox'; import { LinkableRelationChip } from '~/design-system/chip'; import { DateField } from '~/design-system/editable-fields/date-field'; import { TableImageField, TableStringField } from '~/design-system/editable-fields/editable-fields'; @@ -114,7 +114,7 @@ const RenderedProperty = ({ const isRelation = property.dataType === 'RELATION'; - if (property.renderableType === 'IMAGE') { + if (property.renderableTypeStrict === 'IMAGE') { // We don't support rendering images in list or gallery views except the main image return null; } @@ -414,6 +414,9 @@ function EditableValueGroup({ ); case 'BOOLEAN': { const checked = getChecked(value); + if (!isEditing) { + return withLabel(); + } return withLabel( onWriteValue(!checked ? '1' : '0')} />, true); } case 'DATE': diff --git a/apps/web/partials/blocks/table/table-block-ranking.tsx b/apps/web/partials/blocks/table/table-block-ranking.tsx index 033ca29068..9b2dc68a7c 100644 --- a/apps/web/partials/blocks/table/table-block-ranking.tsx +++ b/apps/web/partials/blocks/table/table-block-ranking.tsx @@ -13,7 +13,7 @@ import { FilterTableWithFilters } from '~/design-system/icons/filter-table-with- import { Fullscreen } from '~/design-system/icons/full-screen'; import { RankingBlockBody } from './ranking-block-body'; -import { RankingCardConfigProvider, useRankingShownProperties } from './ranking-card-config'; +import { RankingCardConfigProvider } from './ranking-card-config'; import { RankingPeriodMetadata } from './ranking-period-metadata'; import { TableBlockContextMenu } from './table-block-context-menu'; import { TableBlockEditableFilters } from './table-block-editable-filters'; @@ -28,10 +28,11 @@ type Props = { export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDate = '' }: Props) { const state = useRankingBlockState({ spaceId, rankingStartDate, rankingEndDate, paginateEmbeddedRanking: true }); - const { cardConfig, menuProps } = useRankingShownProperties(); const isEditing = useUserIsEditing(spaceId); const { + cardConfig, + menuProps, canEdit, filterState, setFilterState, diff --git a/apps/web/partials/blocks/table/use-ranking-block-state.ts b/apps/web/partials/blocks/table/use-ranking-block-state.ts index 9f12696e89..e31b56427f 100644 --- a/apps/web/partials/blocks/table/use-ranking-block-state.ts +++ b/apps/web/partials/blocks/table/use-ranking-block-state.ts @@ -54,7 +54,7 @@ import { useEditorStoreLite } from '~/core/state/editor/use-editor'; import { stepAtom } from '~/partials/onboarding/dialog'; -import { selectRankingCardImageProperty } from './ranking-card-config'; +import { buildRankingCardConfig } from './ranking-card-config'; import { postOnboardingRedirectAtom } from '~/atoms/post-onboarding-redirect'; import { rankingComposeReturnHrefAtom } from '~/atoms/ranking-compose-return'; @@ -121,8 +121,47 @@ export function useRankingBlockState({ const setRankingComposeReturnHref = useSetAtom(rankingComposeReturnHrefAtom); const setStep = useSetAtom(stepAtom); - const { name, entityId, relationId, rows, shownColumnIds } = useDataBlock(); - const cardImageProperty = React.useMemo(() => selectRankingCardImageProperty(shownColumnIds), [shownColumnIds]); + const { + name, + entityId, + relationId, + rows, + shownColumnIds, + properties, + filterableProperties, + orderedShownColumnRelations, + toggleProperty, + hideAllShownPropertyColumns, + reorderShownPropertyRelations, + source: dataBlockSource, + } = useDataBlock(); + + const cardConfig = React.useMemo( + () => buildRankingCardConfig({ properties, shownColumnIds, source: dataBlockSource }), + [properties, shownColumnIds, dataBlockSource] + ); + const cardImageProperty = cardConfig.imageProperty; + + const menuProps = React.useMemo( + () => ({ + sourceType: dataBlockSource.type, + filterableProperties, + shownColumnIds, + orderedShownColumnRelations, + toggleProperty, + hideAllShownPropertyColumns, + reorderShownPropertyRelations, + }), + [ + dataBlockSource.type, + filterableProperties, + shownColumnIds, + orderedShownColumnRelations, + toggleProperty, + hideAllShownPropertyColumns, + reorderShownPropertyRelations, + ] + ); const { id: parentEntityId } = useEditorInstance(); const { blockRelations } = useEditorStoreLite(); @@ -806,6 +845,8 @@ export function useRankingBlockState({ setIsFilterOpen, displayName, entityId, + cardConfig, + menuProps, submissions, periodState, periodLabel,