Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b411ffd
Move links queries from entity to the individual link tables
alex-e-leon Jun 17, 2026
ac79203
Add infinite scroll to link tables
alex-e-leon Jun 17, 2026
526f3fb
Use readonly outgoing links
alex-e-leon Jun 19, 2026
2817206
Remove client side sorting in link sections
alex-e-leon Jun 19, 2026
46ff45b
Fix seed data win-rate logic
alex-e-leon Jun 19, 2026
667393e
Fix outgoing-links-table scrolling
alex-e-leon Jun 19, 2026
5497281
Re-apply logic for readonly tables in entity page
alex-e-leon Jun 19, 2026
ef8a467
Better error handling on tables
alex-e-leon Jun 19, 2026
9d6eb37
Simplify virtuoso table
alex-e-leon Jun 19, 2026
be73d4e
Do not clobber users edited data when refetching
alex-e-leon Jun 19, 2026
0fdc893
Fix canEdit logic
alex-e-leon Jun 19, 2026
96761bf
Add a subtle overlay to virtualized loading spinner
alex-e-leon Jun 19, 2026
6bc39e5
Reset fetch state if change in entities
alex-e-leon Jun 19, 2026
efa13e6
Remove client side sorting from outgoing table
alex-e-leon Jun 22, 2026
70b8545
Factor out gql pagination accumulation
alex-e-leon Jun 22, 2026
125adb0
Add client-side sorting to readonly incoming links
alex-e-leon Jun 22, 2026
1af367c
Remove broken link title server side sorting
alex-e-leon Jun 22, 2026
b74f4d2
Re-apply server side filtering for link tables
alex-e-leon Jun 22, 2026
86da518
Fix entity-links filtering
alex-e-leon Jun 22, 2026
710cea3
Filter links when graph edges are clicked
alex-e-leon Jun 22, 2026
5d8e7c9
Fix use-accumulated-cursor-pagination
alex-e-leon Jun 22, 2026
041e332
Switch use-accumulated-cursor pagination to not be marked as binary
alex-e-leon Jun 22, 2026
46d92a8
Removed unnecessary comments
alex-e-leon Jun 22, 2026
b2644af
Simplify comments
alex-e-leon Jun 22, 2026
78d84c2
Show columns on link tables when a filter is applied
alex-e-leon Jun 22, 2026
3a32dbd
Simpler comments
alex-e-leon Jun 22, 2026
f0d4c1b
refactor use-accumulate-cursor-pagination
alex-e-leon Jun 24, 2026
98eb69b
Adjust overscan behaviour
alex-e-leon Jun 24, 2026
356160f
Fix rebase
alex-e-leon Jun 24, 2026
86d82fe
Show loading state until we know whether the entity is readonly or not
alex-e-leon Jun 24, 2026
58de967
Merge branch 'main' into H-4819-entity-links-pagination
CiaranMn Jun 25, 2026
b71d928
update for new Graph summarize query
CiaranMn Jun 25, 2026
a4c3854
increase virtualized table viewport
CiaranMn Jun 25, 2026
7db06fb
allow properties to load in while waiting for links to be resolved
CiaranMn Jun 25, 2026
0d994fc
lint fix
CiaranMn Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion apps/hash-api/src/seed-data/seed-crm-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1282,14 +1282,25 @@ const seedCrmData = async () => {
}),
);

// Open stages ramp up linearly; closed stages take their literal win
// probability (Closed Won = certain win, Closed Lost = no chance) rather
// than continuing the linear scale, which would otherwise mislabel
// "Closed Lost" as a 100% win.
const openStageCount = stageNames.filter(
(name) => !name.startsWith("Closed"),
).length;
const stages = await batchMap([...stageNames], (name, index) =>
makeEntity(stageType.schema.$id, {
[nameBaseUrl]: value(dt.text, name),
[descriptionBaseUrl]: value(dt.text, sentence(8)),
[stageOrderProp.metadata.recordId.baseUrl]: value(dt.number, index + 1),
[defaultProbabilityProp.metadata.recordId.baseUrl]: value(
dt.percentage,
Math.round((index / (stageNames.length - 1)) * 100),
name === "Closed Won"
? 100
: name === "Closed Lost"
? 0
: Math.round((index / (openStageCount - 1)) * 90),
),
[isClosedProp.metadata.recordId.baseUrl]: value(
dt.boolean,
Expand Down
82 changes: 69 additions & 13 deletions apps/hash-frontend/src/pages/shared/entity.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMutation, useQuery } from "@apollo/client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { getRoots } from "@blockprotocol/graph/stdlib";
import { mustHaveAtLeastOne, splitEntityId } from "@blockprotocol/type-system";
Expand Down Expand Up @@ -56,6 +56,7 @@ import type { EntityEditorProps } from "./entity/entity-editor";
import type { EntityRootType, Subgraph } from "@blockprotocol/graph";
import type { EntityId, PropertyObject } from "@blockprotocol/type-system";
import type { VersionedUrl } from "@blockprotocol/type-system/slim";
import type { EntityTraversalPath } from "@rust/hash-graph-store/types";

interface EntityProps {
entityId: EntityId;
Expand Down Expand Up @@ -250,6 +251,25 @@ export const Entity = ({

const [isDirty, setIsDirty] = useState(!!draftLocalEntity);

/**
* Whether the main entity query should include the entity's incoming and
* outgoing link data.
*
* We only fetch it here when the entity is editable, so that the edit flow has
* the links available in the editor subgraph. When the entity is readonly, the
* link tables fetch this data themselves (see `isReadOnly` below), keeping the
* main query (and the editor shell) independent of the entity's link volume.
*
* This starts `false` (we don't know the user's permissions until the first
* response) and is set from the entity permissions in `onCompleted`. If the
* user can edit, the query variables change and the query refetches with the
* link data included.
*/
const [includeLinkDataInQuery, setIncludeLinkDataInQuery] = useState(false);
const hasCompletedInitialLoadRef = useRef(false);
const [hasRootLinkDataBeenResolved, setHasRootLinkDataBeenResolved] =
useState(false);

const { data: queryEntitySubgraphData, refetch } = useQuery<
QueryEntitySubgraphQuery,
QueryEntitySubgraphQueryVariables
Expand Down Expand Up @@ -296,12 +316,33 @@ export const Entity = ({
closedMultiEntityTypes,
});

const isInitialLoad = !hasCompletedInitialLoadRef.current;
hasCompletedInitialLoadRef.current = true;

setDraftEntitySubgraph(subgraph);

setIsDirty(false);
setDraftLinksToCreate([]);
setDraftLinksToArchive([]);

const canUpdate =
!!data.queryEntitySubgraph.entityPermissions?.[entityId]?.update;

if (isInitialLoad) {
/**
* For editable entities this flips the query variables, triggering the
* link-data upgrade refetch handled above. For readonly entities it stays
* false and the link tables self-fetch instead.
*/
setIncludeLinkDataInQuery(canUpdate);
Comment thread
alex-e-leon marked this conversation as resolved.
} else if (canUpdate) {
/**
* If this is _not_ the initial load, and the entity is editable,
* this is the result of a subsequent fetch with the link traversal included.
*/
setHasRootLinkDataBeenResolved(true);
}
Comment thread
cursor[bot] marked this conversation as resolved.

Comment thread
alex-e-leon marked this conversation as resolved.
Comment thread
alex-e-leon marked this conversation as resolved.
setLoading(false);
},
variables: {
Expand All @@ -325,18 +366,31 @@ export const Entity = ({
},
temporalAxes: currentTimeInstantTemporalAxes,
traversalPaths: [
{
edges: [
{ kind: "has-left-entity", direction: "incoming" },
{ kind: "has-right-entity", direction: "outgoing" },
],
},
{
edges: [
{ kind: "has-right-entity", direction: "incoming" },
{ kind: "has-left-entity", direction: "outgoing" },
],
},
/**
* Incoming and outgoing links (and their source/target entities) are
* only fetched here when the entity is editable. When readonly, the
* link tables fetch this data themselves (see `isReadOnly`).
*/
...(includeLinkDataInQuery
? ([
{
edges: [
{ kind: "has-left-entity", direction: "incoming" },
{ kind: "has-right-entity", direction: "outgoing" },
],
},
{
edges: [
{ kind: "has-right-entity", direction: "incoming" },
{ kind: "has-left-entity", direction: "outgoing" },
],
},
] satisfies EntityTraversalPath[])
: []),
/**
* These paths resolve the entity's own source/target when the entity
* is itself a link, and are always required (e.g. by `LinkSection`).
*/
{
edges: [{ kind: "has-left-entity", direction: "outgoing" }],
},
Expand Down Expand Up @@ -570,6 +624,7 @@ export const Entity = ({
JSON.stringify(entityFromDb?.metadata.entityTypeIds.toSorted()),
);
}}
hasRootLinkDataBeenResolved={hasRootLinkDataBeenResolved}
isDirty={isDirty}
isInSlide={isInSlide}
onEntityClick={(clickedEntityId) =>
Expand Down Expand Up @@ -696,6 +751,7 @@ export const Entity = ({
),
);
}}
hasRootLinkDataBeenResolved={hasRootLinkDataBeenResolved}
isDirty={isDirty}
onEntityClick={(clickedEntityId) =>
pushToSlideStack({
Expand Down
76 changes: 72 additions & 4 deletions apps/hash-frontend/src/pages/shared/entity/entity-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box, Stack } from "@mui/material";
import { useMemo } from "react";

import { getRoots } from "@blockprotocol/graph/stdlib";
Expand All @@ -8,7 +8,8 @@ import { EntityEditorContextProvider } from "./entity-editor/entity-editor-conte
import { FilePreviewSection } from "./entity-editor/file-preview-section";
import { HistorySection } from "./entity-editor/history-section";
import { LinkSection } from "./entity-editor/link-section";
import { LinksSection } from "./entity-editor/links-section";
import { IncomingLinksSection } from "./entity-editor/links-section/incoming-links-section";
import { OutgoingLinksSection } from "./entity-editor/links-section/outgoing-links-section";
import { PropertiesSection } from "./entity-editor/properties-section";
import { TypesSection } from "./entity-editor/types-section";
import { useEntityEditorTab } from "./shared/entity-editor-tabs";
Expand Down Expand Up @@ -55,6 +56,10 @@ export interface EntityEditorProps extends DraftLinkState {
* Whether the entity is dirty (has unsaved changes)
*/
isDirty: boolean;
/**
* When the component is not in readonly, whether the link data (if any) is now included in the subgraph.
*/
hasRootLinkDataBeenResolved: boolean;
/**
* The label of the entity being edited
*/
Expand Down Expand Up @@ -111,7 +116,24 @@ export interface EntityEditorProps extends DraftLinkState {
}

export const EntityEditor = (props: EntityEditorProps) => {
const { entitySubgraph } = props;
const {
closedMultiEntityType,
closedMultiEntityTypesDefinitions,
customEntityLinksColumns,
defaultOutgoingLinkFilters,
draftLinksToArchive,
draftLinksToCreate,
entityLabel,
entitySubgraph,
hasRootLinkDataBeenResolved,
linkAndDestinationEntitiesClosedMultiEntityTypesMap,
onEntityClick,
onTypeClick,
readonly,
setDraftLinksToArchive,
setDraftLinksToCreate,
slideContainerRef,
} = props;

const entity = useMemo(() => {
const roots = getRoots(entitySubgraph);
Expand Down Expand Up @@ -159,7 +181,53 @@ export const EntityEditor = (props: EntityEditorProps) => {

<PropertiesSection />

<LinksSection isLinkEntity={isLinkEntity} />
<Stack gap={6}>
<OutgoingLinksSection
closedMultiEntityType={closedMultiEntityType}
closedMultiEntityTypesDefinitions={
closedMultiEntityTypesDefinitions
}
customEntityLinksColumns={customEntityLinksColumns}
defaultOutgoingLinkFilters={defaultOutgoingLinkFilters}
draftLinksToArchive={draftLinksToArchive}
draftLinksToCreate={draftLinksToCreate}
entity={entity}
entitySubgraph={entitySubgraph}
isLinkEntity={isLinkEntity}
key={`outgoing-${entity.metadata.recordId.editionId}`}
linkAndDestinationEntitiesClosedMultiEntityTypesMap={
linkAndDestinationEntitiesClosedMultiEntityTypesMap
}
hasRootLinkDataBeenResolved={hasRootLinkDataBeenResolved}
onEntityClick={onEntityClick}
onTypeClick={onTypeClick}
readonly={readonly}
setDraftLinksToArchive={setDraftLinksToArchive}
setDraftLinksToCreate={setDraftLinksToCreate}
slideContainerRef={slideContainerRef}
/>

<IncomingLinksSection
closedMultiEntityTypesDefinitions={
closedMultiEntityTypesDefinitions
}
customEntityLinksColumns={customEntityLinksColumns}
draftLinksToArchive={draftLinksToArchive}
entity={entity}
entityLabel={entityLabel}
entitySubgraph={entitySubgraph}
isLinkEntity={isLinkEntity}
hasRootLinkDataBeenResolved={hasRootLinkDataBeenResolved}
key={`incoming-${entity.metadata.recordId.editionId}`}
linkAndDestinationEntitiesClosedMultiEntityTypesMap={
linkAndDestinationEntitiesClosedMultiEntityTypesMap
}
onEntityClick={onEntityClick}
onTypeClick={onTypeClick}
readonly={readonly}
slideContainerRef={slideContainerRef}
/>
</Stack>

<ClaimsSection />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { PropsWithChildren } from "react";

export type TableExpandStatus = Record<string, boolean>;

interface Props extends EntityEditorProps {
interface Props extends Omit<EntityEditorProps, "hasRootLinkDataBeenResolved"> {
entity: HashEntity;
isLocalDraftOnly: boolean;
propertyExpandStatus: TableExpandStatus;
Expand Down

This file was deleted.

Loading
Loading