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/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 0b63626b1b..400c888c72 100644 --- a/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts +++ b/apps/web/core/blocks/ranking/use-ranking-entry-entities.ts @@ -17,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]); @@ -40,11 +46,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 }; @@ -75,23 +81,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-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 new file mode 100644 index 0000000000..8df9b51b2b --- /dev/null +++ b/apps/web/partials/blocks/table/ranking-card-config.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { ContentIds, SystemIds } from '@geoprotocol/geo-sdk/lite'; + +import * as React from 'react'; + +import type { Source } from '~/core/blocks/data/source'; +import { ID } from '~/core/id'; +import type { Property } from '~/core/types'; + +/** + * 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'; + +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(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' }, + imageProperty: 'avatar', +}); + +export function RankingCardConfigProvider({ + value, + children, +}: { + value: RankingCardConfig; + children: React.ReactNode; +}) { + return {children}; +} + +export function useRankingCardConfig(): RankingCardConfig { + return React.useContext(RankingCardConfigContext); +} diff --git a/apps/web/partials/blocks/table/ranking-compose-screen.tsx b/apps/web/partials/blocks/table/ranking-compose-screen.tsx index 9fec0194d7..f7867c9857 100644 --- a/apps/web/partials/blocks/table/ranking-compose-screen.tsx +++ b/apps/web/partials/blocks/table/ranking-compose-screen.tsx @@ -43,6 +43,7 @@ import type { SearchResult } from '~/core/types'; import { stepAtom } from '~/partials/onboarding/dialog'; +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'; @@ -67,11 +68,16 @@ 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, shownColumnIds, source } = useDataBlock(); const displayName = name?.trim() || 'Untitled ranking'; const createNewSpaceId = React.useMemo(() => resolveRankingSingleTargetSpaceId(filterState), [filterState]); + const cardConfig = React.useMemo( + () => buildRankingCardConfig({ properties, shownColumnIds, source }), + [properties, shownColumnIds, source] + ); + const cardImageProperty = cardConfig.imageProperty; const { showOnboarding } = useOnboarding(); const composeAccessSpaceId = createNewSpaceId ?? spaceId; const { @@ -189,7 +195,8 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn const { entries: rankableEntries, isLoading: isLoadingRankableEntries } = useRankingEntryEntities( spaceId, - allRankableEntityIds + allRankableEntityIds, + cardImageProperty ); const [orderedIds, setOrderedIds] = React.useState(mySubmission?.orderedEntityIds ?? []); @@ -262,7 +269,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('|'); @@ -278,7 +289,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( @@ -636,7 +647,7 @@ export function RankingComposeScreen({ spaceId, rankingStartDate = '', rankingEn } return ( - <> + setEntitySheetTarget(null)} /> @@ -681,6 +692,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..71d9beec5e 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,9 +23,11 @@ export function RankingEntryRowSkeleton({ rank }: { rank?: number }) { const showLeadingRank = rank != null && rank > 0; return ( -
+
{showLeadingRank ? ( - {rank} + + {rank} + ) : null}
@@ -39,12 +44,10 @@ type Props = { 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,12 +61,14 @@ export function RankingEntryRow({ linkToEntity = true, rankStyle = 'avatar-badge', }: Props) { + 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 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; @@ -84,9 +89,11 @@ export function RankingEntryRow({ ); return ( -
+
{showLeadingRank ? ( - {rank} + + {rank} + ) : null} {avatar}
@@ -109,6 +116,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..d513334fc4 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 } from './ranking-card-config'; import { RankingPeriodMetadata } from './ranking-period-metadata'; import { type InitialGlobalRanking, type InitialSharedRanking, useRankingBlockState } from './use-ranking-block-state'; @@ -44,6 +45,7 @@ export function RankingTableView({ initialSharedRanking, }); const { + cardConfig, displayName, periodState, periodLabel, @@ -78,7 +80,9 @@ export function RankingTableView({
- + + +
); 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 2cfe4c0263..8aea812466 100644 --- a/apps/web/partials/blocks/table/table-block-ranking.tsx +++ b/apps/web/partials/blocks/table/table-block-ranking.tsx @@ -21,6 +21,7 @@ import { Fullscreen } from '~/design-system/icons/full-screen'; import { DataBlockScopeDropdown } from './data-block-scope-dropdown'; import { DataBlockViewMenu } from './data-block-view-menu'; import { RankingBlockBody } from './ranking-block-body'; +import { RankingCardConfigProvider } from './ranking-card-config'; import { RankingExploreView } from './ranking-explore-view'; import { RankingGalleryView } from './ranking-gallery-view'; import { RankingHeaderActions } from './ranking-header-actions'; @@ -31,6 +32,7 @@ import { TableBlockContextMenu } from './table-block-context-menu'; import { TableBlockEditableFilters } from './table-block-editable-filters'; import type { TableBlockFilterPromptHandle } from './table-block-filter-creation-prompt'; import { TableBlockFilterGroupPill, groupFilters } from './table-block-filter-pill'; +import { TableBlockPropertiesMenu } from './table-block-properties-menu'; import { useRankingBlockState } from './use-ranking-block-state'; type Props = { @@ -43,6 +45,8 @@ export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDa const state = useRankingBlockState({ spaceId, rankingStartDate, rankingEndDate, paginateEmbeddedRanking: true }); const isEditing = useUserIsEditing(spaceId); const { + cardConfig, + menuProps, filterState, resolvedFilterState, filterMode, @@ -116,6 +120,8 @@ export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDa
+ {isEditing ? : null} + {showHeaderActions ? : null} {showBrowseChrome ? ( @@ -227,17 +233,19 @@ export function TableBlockRanking({ spaceId, rankingStartDate = '', rankingEndDa )} - {isGalleryView ? ( - - ) : isListView ? ( - - ) : isPillView ? ( - - ) : isExploreView ? ( - - ) : ( - - )} + + {isGalleryView ? ( + + ) : isListView ? ( + + ) : isPillView ? ( + + ) : isExploreView ? ( + + ) : ( + + )} +
); } 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 8be46d1974..ad72626a1f 100644 --- a/apps/web/partials/blocks/table/use-ranking-block-state.ts +++ b/apps/web/partials/blocks/table/use-ranking-block-state.ts @@ -55,6 +55,7 @@ import { useEditorStoreLite } from '~/core/state/editor/use-editor'; import { stepAtom } from '~/partials/onboarding/dialog'; +import { buildRankingCardConfig } from './ranking-card-config'; import { postOnboardingRedirectAtom } from '~/atoms/post-onboarding-redirect'; import { rankingComposeReturnHrefAtom } from '~/atoms/ranking-compose-return'; @@ -130,7 +131,42 @@ export function useRankingBlockState({ view: stateView, viewRelation: stateViewRelation, 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(); @@ -378,12 +414,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]); @@ -884,6 +922,8 @@ export function useRankingBlockState({ setIsFilterOpen, displayName, entityId, + cardConfig, + menuProps, submissions, periodState, periodLabel,