From b9459474246077a34a6f2c7b76e4351865aed8df Mon Sep 17 00:00:00 2001 From: Ciaran Morinan Date: Mon, 29 Jun 2026 22:28:10 +0100 Subject: [PATCH 1/6] better 'load more' handling in entities table --- apps/hash-frontend/src/pages/shared/entities-visualizer.tsx | 1 + .../src/pages/shared/entities-visualizer/entities-table.tsx | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/hash-frontend/src/pages/shared/entities-visualizer.tsx b/apps/hash-frontend/src/pages/shared/entities-visualizer.tsx index e182290062f..d90152846c9 100644 --- a/apps/hash-frontend/src/pages/shared/entities-visualizer.tsx +++ b/apps/hash-frontend/src/pages/shared/entities-visualizer.tsx @@ -649,6 +649,7 @@ export const EntitiesVisualizer: FunctionComponent<{ currentlyDisplayedColumnsRef={currentlyDisplayedColumnsRef} currentlyDisplayedRowsRef={currentlyDisplayedRowsRef} handleEntityClick={handleEntityClick} + hasMoreRowsAvailable={nextCursor != null} loading={dataLoading} isViewingOnlyPages={isViewingOnlyPages} maxHeight={`calc(${tableHeight} - ${toolbarHeight}px)`} diff --git a/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx b/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx index fba55d5ab3c..fcd9689de04 100644 --- a/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx +++ b/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx @@ -106,6 +106,7 @@ export const EntitiesTable: FunctionComponent< loading: boolean; isViewingOnlyPages: boolean; maxHeight: string | number; + hasMoreRowsAvailable: boolean; loadMoreRows?: () => void; selectedRows: EntitiesTableRow[]; setActiveConversions: Dispatch< @@ -136,6 +137,7 @@ export const EntitiesTable: FunctionComponent< loading: entityDataLoading, isViewingOnlyPages, maxHeight, + hasMoreRowsAvailable, loadMoreRows, selectedRows, setActiveConversions, @@ -810,8 +812,6 @@ export const EntitiesTable: FunctionComponent< }); }, [rows.length]); - const hasMoreRowsAvailable = - !!totalResultCount && totalResultCount > rows.length; const loadMoreRowHeight = 60; return ( @@ -906,7 +906,7 @@ export const EntitiesTable: FunctionComponent< component="span" sx={{ color: ({ palette }) => palette.gray[50], ml: 0.5 }} > - - {formatNumber(totalResultCount - rows.length)} remaining + {totalResultCount != null ? `- ${formatNumber(totalResultCount - rows.length)} remaining` : ""} Date: Mon, 29 Jun 2026 22:28:26 +0100 Subject: [PATCH 2/6] better notification bell routing --- apps/hash-frontend/src/pages/_app.page.tsx | 7 +---- .../src/shared/get-inbox-href.ts | 29 +++++++++++++++++++ .../notifications-dropdown.tsx | 10 ++++++- .../layout/layout-with-sidebar/sidebar.tsx | 16 +++++----- 4 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 apps/hash-frontend/src/shared/get-inbox-href.ts diff --git a/apps/hash-frontend/src/pages/_app.page.tsx b/apps/hash-frontend/src/pages/_app.page.tsx index f3e982280fa..a31d77984c1 100644 --- a/apps/hash-frontend/src/pages/_app.page.tsx +++ b/apps/hash-frontend/src/pages/_app.page.tsx @@ -333,12 +333,7 @@ const featureFlagHiddenPathnames: Record = { notes: ["/notes"], workers: ["/goals", "/flows", "/workers", "/agents"], ai: ["/goals"], - supplyChain: [ - "/supply-chain", - "/supply-chain/product/[product-id]", - "/supply-chain/site/[site-id]", - "/supply-chain/site/[site-id]/opportunity/[opportunity-type]/[product-id]/[step-id]", - ], + supplyChain: [], }; AppWithTypeSystemContextProvider.getInitialProps = async (appContext) => { diff --git a/apps/hash-frontend/src/shared/get-inbox-href.ts b/apps/hash-frontend/src/shared/get-inbox-href.ts new file mode 100644 index 00000000000..f9b4595cbdd --- /dev/null +++ b/apps/hash-frontend/src/shared/get-inbox-href.ts @@ -0,0 +1,29 @@ +type GetInboxHrefParams = { + includeDraftEntityActions?: boolean; + numberOfDraftEntityActions?: number; + numberOfPendingInvites?: number; + numberOfUnreadNotifications?: number; + fallbackHref: string; +}; + +export const getInboxHref = ({ + fallbackHref, + includeDraftEntityActions = true, + numberOfDraftEntityActions = 0, + numberOfPendingInvites = 0, + numberOfUnreadNotifications = 0, +}: GetInboxHrefParams) => { + if (includeDraftEntityActions && numberOfDraftEntityActions > 0) { + return "/actions"; + } + + if (numberOfUnreadNotifications > 0) { + return "/notifications"; + } + + if (numberOfPendingInvites > 0) { + return "/invites"; + } + + return fallbackHref; +}; diff --git a/apps/hash-frontend/src/shared/layout/layout-with-header/notifications-dropdown.tsx b/apps/hash-frontend/src/shared/layout/layout-with-header/notifications-dropdown.tsx index 05a121ed58e..9a192a40bf3 100644 --- a/apps/hash-frontend/src/shared/layout/layout-with-header/notifications-dropdown.tsx +++ b/apps/hash-frontend/src/shared/layout/layout-with-header/notifications-dropdown.tsx @@ -3,6 +3,7 @@ import { Tooltip, useTheme } from "@mui/material"; import { FontAwesomeIcon } from "@hashintel/design-system"; +import { getInboxHref } from "../../get-inbox-href"; import { useInvites } from "../../invites-context"; import { useNotificationCount } from "../../notification-count-context"; import { Link } from "../../ui"; @@ -16,9 +17,16 @@ export const NotificationsDropdown: FunctionComponent = () => { const { numberOfUnreadNotifications } = useNotificationCount(); const { pendingInvites } = useInvites(); + const href = getInboxHref({ + fallbackHref: "/notifications", + includeDraftEntityActions: false, + numberOfPendingInvites: pendingInvites.length, + numberOfUnreadNotifications, + }); + return ( - + { const numberOfPendingActions = draftEntitiesCount ?? 0; const unreadNotifications = numberOfUnreadNotifications ?? 0; - const shouldInboxLinkToActions = - numberOfPendingActions > 0 || - (unreadNotifications === 0 && pendingInvites.length === 0); - return [ { title: "Home", @@ -165,11 +162,12 @@ export const PageSidebar: FunctionComponent = () => { }, { title: "Inbox", - path: shouldInboxLinkToActions - ? "/actions" - : unreadNotifications > 0 - ? "/notifications" - : "/invites", + path: getInboxHref({ + fallbackHref: "/actions", + numberOfDraftEntityActions: numberOfPendingActions, + numberOfPendingInvites: pendingInvites.length, + numberOfUnreadNotifications: unreadNotifications, + }), icon: , tooltipTitle: "", count: From 4f5cfa694e4ef825618ac1a70ce54853182695ac Mon Sep 17 00:00:00 2001 From: Ciaran Morinan Date: Mon, 29 Jun 2026 23:11:34 +0100 Subject: [PATCH 3/6] fix auto-accepting org invite if arriving via email verification link --- apps/hash-api/src/graphql/resolvers/index.ts | 7 +- apps/hash-frontend/src/pages/signup.page.tsx | 99 +++++++++++++------- 2 files changed, 69 insertions(+), 37 deletions(-) diff --git a/apps/hash-api/src/graphql/resolvers/index.ts b/apps/hash-api/src/graphql/resolvers/index.ts index ed49351228b..7fa9911468f 100644 --- a/apps/hash-api/src/graphql/resolvers/index.ts +++ b/apps/hash-api/src/graphql/resolvers/index.ts @@ -143,6 +143,9 @@ export const resolvers: Omit & { /** Logged in users (who may not have completed signup) */ me: loggedInMiddleware(meResolver), getWaitlistPosition: loggedInMiddleware(getWaitlistPositionResolver), + getMyPendingInvitations: loggedInMiddleware( + getMyPendingInvitationsResolver, + ), /** Logged in and signed up users */ getBlockProtocolBlocks: loggedInAndSignedUpMiddleware( @@ -166,10 +169,6 @@ export const resolvers: Omit & { queryEntitySubgraph: loggedInAndSignedUpMiddleware( queryEntitySubgraphResolver, ), - getMyPendingInvitations: loggedInAndSignedUpMiddleware( - getMyPendingInvitationsResolver, - ), - getLinearOrganization: loggedInAndSignedUpMiddleware( getLinearOrganizationResolver, ), diff --git a/apps/hash-frontend/src/pages/signup.page.tsx b/apps/hash-frontend/src/pages/signup.page.tsx index a62851b9e6b..456e7ed39c5 100644 --- a/apps/hash-frontend/src/pages/signup.page.tsx +++ b/apps/hash-frontend/src/pages/signup.page.tsx @@ -8,15 +8,18 @@ import { AlertModal, ArrowUpRightRegularIcon } from "@hashintel/design-system"; import { useUpdateAuthenticatedUser } from "../components/hooks/use-update-authenticated-user"; import { acceptOrgInvitationMutation, + getMyPendingInvitationsQuery, getPendingInvitationByEntityIdQuery, } from "../graphql/queries/knowledge/org.queries"; import { hasAccessToHashQuery } from "../graphql/queries/user.queries"; +import { useInvites } from "../shared/invites-context"; import { getPlainLayout } from "../shared/layout"; import { Button } from "../shared/ui"; import { useAuthInfo } from "./shared/auth-info-context"; import { AuthLayout } from "./shared/auth-layout"; import { parseGraphQLError } from "./shared/auth-utils"; import { VerifyEmailStep } from "./shared/verify-email-step"; +import { useActiveWorkspace } from "./shared/workspace-context"; import { AcceptOrgInvitation } from "./signup.page/accept-org-invitation"; import { AccountSetupForm } from "./signup.page/account-setup-form"; import { SignupRegistrationForm } from "./signup.page/signup-registration-form"; @@ -26,6 +29,8 @@ import { SignupSteps } from "./signup.page/signup-steps"; import type { AcceptOrgInvitationMutation, AcceptOrgInvitationMutationVariables, + GetMyPendingInvitationsQuery, + GetMyPendingInvitationsQueryVariables, GetPendingInvitationByEntityIdQuery, GetPendingInvitationByEntityIdQueryVariables, HasAccessToHashQuery, @@ -95,6 +100,8 @@ const SignupPage: NextPageWithLayout = () => { const { authenticatedUser, refetch: refetchAuthenticatedUser } = useAuthInfo(); + const { refetch: refetchInvites } = useInvites(); + const { updateActiveWorkspaceWebId } = useActiveWorkspace(); const userHasVerifiedEmail = authenticatedUser?.emails.find(({ verified }) => verified) !== undefined; @@ -139,12 +146,26 @@ const SignupPage: NextPageWithLayout = () => { skip: !invitationId, }); + const { + data: pendingInvitationsData, + loading: pendingInvitationsLoading, + } = useQuery< + GetMyPendingInvitationsQuery, + GetMyPendingInvitationsQueryVariables + >(getMyPendingInvitationsQuery, { + skip: !!invitationId || !authenticatedUser || !userHasVerifiedEmail, + }); + const [acceptInvitation] = useMutation< AcceptOrgInvitationMutation, AcceptOrgInvitationMutationVariables >(acceptOrgInvitationMutation); - const invitation = invitationData?.getPendingInvitationByEntityId; + const invitation = + invitationData?.getPendingInvitationByEntityId ?? + pendingInvitationsData?.getMyPendingInvitations[0]; + + const loadingInvitation = invitationLoading || pendingInvitationsLoading; const [acceptingInvitation, setAcceptingInvitation] = useState(false); const acceptingInvitationEntityIdRef = useRef( @@ -220,7 +241,9 @@ const SignupPage: NextPageWithLayout = () => { }) .then(async (result) => { if (result?.accepted || result?.alreadyAMember) { + refetchInvites(); await refetchAuthenticatedUser(); + updateActiveWorkspaceWebId(invitation.org.webId); void router.replace("/"); return; } @@ -245,7 +268,9 @@ const SignupPage: NextPageWithLayout = () => { clearInvitationAcceptanceReservation, invitation, refetchAuthenticatedUser, + refetchInvites, router, + updateActiveWorkspaceWebId, ]); useEffect(() => { @@ -253,7 +278,7 @@ const SignupPage: NextPageWithLayout = () => { router.isReady && authenticatedUser?.accountSignupComplete && invitationId && - !invitationLoading && + !loadingInvitation && !invitation && !acceptingInvitation ) { @@ -264,7 +289,7 @@ const SignupPage: NextPageWithLayout = () => { authenticatedUser?.accountSignupComplete, invitation, invitationId, - invitationLoading, + loadingInvitation, router, ]); @@ -340,6 +365,10 @@ const SignupPage: NextPageWithLayout = () => { } await refetchAuthenticatedUser(); + refetchInvites(); + if (invitation) { + updateActiveWorkspaceWebId(invitation.org.webId); + } void router.push("/"); }, @@ -347,8 +376,10 @@ const SignupPage: NextPageWithLayout = () => { acceptInvitationOnce, clearInvitationAcceptanceReservation, invitation, + refetchInvites, refetchAuthenticatedUser, updateAuthenticatedUser, + updateActiveWorkspaceWebId, router, ], ); @@ -395,38 +426,40 @@ const SignupPage: NextPageWithLayout = () => { > - {invitationLoading ? null : invitation && showInvitationStep ? ( - setShowInvitationStep(false)} - /> - ) : authenticatedUser ? ( - userHasVerifiedEmail ? ( - userHasAccessToHashData?.hasAccessToHash ? ( - setShowInvitationStep(false)} + /> + ) : authenticatedUser ? ( + userHasVerifiedEmail ? ( + userHasAccessToHashData?.hasAccessToHash ? ( + + ) : null + ) : ( + { + await refetchAuthenticatedUser(); + + const { data } = await fetchHasAccess(); + + if (!data?.hasAccessToHash) { + void router.replace("/"); + } + }} /> - ) : null + ) ) : ( - { - await refetchAuthenticatedUser(); - - const { data } = await fetchHasAccess(); - - if (!data?.hasAccessToHash) { - void router.replace("/"); - } - }} - /> - ) - ) : ( - - )} + + )} Date: Mon, 29 Jun 2026 23:20:40 +0100 Subject: [PATCH 4/6] supply chain loading improvements --- .../product/[product-id].page.tsx | 7 +- .../pages/supply-chain/shared/load-state.tsx | 57 +++++++ .../shared/supply-chain-layout.tsx | 14 +- .../[product-id]/[step-id].page.tsx | 7 +- .../supply-chain/supply-chain-data-shell.tsx | 140 +++++++++++++----- .../supply-chain-data-shell/opportunity.tsx | 9 +- .../supply-chain-data-shell/site.tsx | 5 +- 7 files changed, 175 insertions(+), 64 deletions(-) diff --git a/apps/hash-frontend/src/pages/supply-chain/product/[product-id].page.tsx b/apps/hash-frontend/src/pages/supply-chain/product/[product-id].page.tsx index dcfccd2ae9e..ce0d2f6f1d5 100644 --- a/apps/hash-frontend/src/pages/supply-chain/product/[product-id].page.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/product/[product-id].page.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react"; import { css } from "@hashintel/ds-helpers/css"; import { fetchGraph } from "../shared/data"; -import { ErrorState, LoadingState } from "../shared/load-state"; +import { ErrorState, SupplyChainAppSkeleton } from "../shared/load-state"; import { getSupplyChainLayout } from "../shared/supply-chain-layout"; import { trackSupplyChainError, @@ -16,7 +16,6 @@ import { Overview } from "../supply-chain-data-shell/product"; import type { NextPageWithLayout } from "../../../shared/layout"; import type { GraphData } from "../shared/types"; -const loadingH = css({ h: "64" }); const errorPad = css({ px: "6", py: "4" }); const ProductPage: NextPageWithLayout = () => { @@ -79,9 +78,7 @@ const ProductPage: NextPageWithLayout = () => { }, [productId]); if (loading) { - return ( - - ); + return ; } if (error) { return ; diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/load-state.tsx b/apps/hash-frontend/src/pages/supply-chain/shared/load-state.tsx index f9222cbda58..5ac1bd9ff39 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/load-state.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/shared/load-state.tsx @@ -14,6 +14,43 @@ const loadingRow = css({ }); const loadingText = css({ textStyle: "sm", color: "fg.subtle" }); const errorText = css({ textStyle: "sm", color: "status.error.fg.body" }); +const appSkeletonRoot = css({ + display: "flex", + flexDirection: "column", + flex: "1", + minH: "0", + h: "full", + w: "full", + bg: "bgSolid.min", +}); +const appSkeletonTopBar = css({ + display: "flex", + alignItems: "center", + px: "6", + py: "3", + borderBottomWidth: "1px", + borderColor: "bd.subtle", + flexShrink: 0, +}); +const appSkeletonSitePicker = css({ + h: "10", + w: "64", + maxW: "[min(260px,45vw)]", + rounded: "md", + bg: "bg.subtle", +}); +const appSkeletonContent = css({ + flex: "1", + minH: "0", + p: "6", + display: "flex", +}); +const appSkeletonContentBlock = css({ + flex: "1", + minH: "[280px]", + rounded: "lg", + bg: "bg.subtle", +}); /** Centered, muted "loading…" message with a ds spinner. Size the area via `className` (e.g. `h-32`). */ export const LoadingState = ({ @@ -33,6 +70,26 @@ export const LoadingState = ({ ); }; +/** Supply-chain route loading skeleton matching the page chrome. */ +export const SupplyChainAppSkeleton = ({ + className, +}: { + className?: string; +}) => ( +
+
+
+
+
+
+
+
+); + /** Inline error message. Pad/position via `className` (e.g. `px-6 py-4`). */ export const ErrorState = ({ message, diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/supply-chain-layout.tsx b/apps/hash-frontend/src/pages/supply-chain/shared/supply-chain-layout.tsx index ec6bd93fbf6..2406203c260 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/supply-chain-layout.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/shared/supply-chain-layout.tsx @@ -1,25 +1,15 @@ import { useRef } from "react"; import { PortalContainerContext } from "@hashintel/ds-components"; -import { css } from "@hashintel/ds-helpers/css"; import { getLayoutWithSidebar } from "../../../shared/layout"; import { HEADER_HEIGHT } from "../../../shared/layout/layout-with-header/page-header"; import { useActiveWorkspace } from "../../shared/workspace-context"; import { SupplyChainDataShell } from "../supply-chain-data-shell"; -import { LoadingState } from "./load-state"; +import { SupplyChainAppSkeleton } from "./load-state"; import type { ReactElement, ReactNode } from "react"; -const emptyState = css({ - h: "full", - display: "flex", - alignItems: "center", - justifyContent: "center", - color: "fg.subtle", - textStyle: "sm", -}); - /** * Layout shared by every `/supply-chain/*` page. * @@ -46,7 +36,7 @@ const SupplyChainShell = ({ children }: { children: ReactNode }) => { }} > {activeWorkspaceWebId === undefined ? ( - + ) : ( { }, [opportunityType, productId, siteId, stepId]); if (!router.isReady) { - return ; + return ; } if (!siteId || !productId || !stepId) { diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx index 361be4412ba..eb57d595825 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx @@ -8,6 +8,8 @@ import { import { css } from "@hashintel/ds-helpers/css"; +import { useAuthInfo } from "../shared/auth-info-context"; +import { useActiveWorkspace } from "../shared/workspace-context"; import { CostParamsContext, DEFAULT_CURRENCY, @@ -22,7 +24,7 @@ import { type SiteRef, } from "./shared/data"; import { DocsProvider } from "./shared/docs/docs-context"; -import { LoadingState } from "./shared/load-state"; +import { SupplyChainAppSkeleton } from "./shared/load-state"; import { LowSampleContext } from "./shared/low-sample-context"; import { BASE_MEASURES, @@ -52,23 +54,15 @@ const screenBg = css({ minH: "0", bg: "bgSolid.min", }); -const centerScreen = css({ - minH: "screen", - display: "flex", - alignItems: "center", - justifyContent: "center", - bg: "bgSolid.min", -}); + const errorStack = css({ - textAlign: "center", + textAlign: "left", display: "flex", flexDirection: "column", gap: "3", - maxW: "md", }); -const errorTitle = css({ color: "status.error.fg.body", fontWeight: "medium" }); -const subtleSm = css({ textStyle: "sm", color: "fg.subtle" }); -const subtleXs = css({ textStyle: "xs", color: "fg.subtle" }); +const errorTitle = css({ fontWeight: "medium", textStyle: "lg" }); +const subtleSm = css({ color: "fg.subtle" }); const emptyText = css({ color: "fg.subtle" }); const mainArea = css({ flex: "1", @@ -80,6 +74,18 @@ const mainArea = css({ flexDirection: "column", }); +const getOrderedWebIds = (webIds: (WebId | null | undefined)[]): WebId[] => { + const orderedWebIds: WebId[] = []; + + for (const webId of webIds) { + if (webId && !orderedWebIds.includes(webId)) { + orderedWebIds.push(webId); + } + } + + return orderedWebIds; +}; + function normaliseTimeRange( value: string | null, fallback: TimeRange = "12m", @@ -151,6 +157,7 @@ export const SupplyChainDataShell = ({ const [mounted, setMounted] = useState(false); const [products, setProducts] = useState([]); const [sites, setSites] = useState([]); + const [dataScope, setDataScope] = useState(scope); const [defaultCurrency, setDefaultCurrency] = useState(DEFAULT_CURRENCY); const [defaultStorageCost, setDefaultStorageCost] = useState(DEFAULT_STORAGE_COST); @@ -162,6 +169,19 @@ export const SupplyChainDataShell = ({ saveSettings: saveUserPreferenceSettings, } = useSupplyChainUserPreferences(); + const { authenticatedUser } = useAuthInfo(); + const { activeWorkspace } = useActiveWorkspace(); + + const candidateWebIds = useMemo( + () => + getOrderedWebIds([ + scope, + ...(authenticatedUser?.memberOf.map(({ org }) => org.webId) ?? []), + authenticatedUser?.accountId as WebId | undefined, + ]), + [authenticatedUser, scope], + ); + const [searchParams, setSearchParams] = useSearchParams(); const waccRate = normaliseWacc(searchParams.get("wacc")); const storageCost = normaliseStorageCost( @@ -303,25 +323,73 @@ export const SupplyChainDataShell = ({ }, []); useEffect(() => { - configureDataSource({ scope }); - }, [scope]); + let cancelled = false; + const isCancelled = () => cancelled; + + const loadRegistry = async () => { + let lastError: unknown = null; + + for (const candidateWebId of candidateWebIds) { + if (isCancelled()) { + return; + } + + configureDataSource({ scope: candidateWebId }); + + try { + const [candidateProducts, candidateSiteRefs] = await Promise.all([ + fetchProducts(), + fetchSites(), + ]); + + if (candidateProducts.length === 0) { + lastError = new Error("No supply chain products found."); + continue; + } + + if (isCancelled()) { + return; + } + + setDataScope(candidateWebId); + setProducts(candidateProducts); + setSites(candidateSiteRefs); + setLoading(false); + return; + } catch (caught) { + lastError = caught; + } + } + + if (isCancelled()) { + return; + } + + configureDataSource({ scope }); + setDataScope(scope); + setProducts([]); + setSites([]); + setError( + lastError instanceof Error + ? lastError.message + : typeof lastError === "string" + ? lastError + : "No supply chain data found.", + ); + setLoading(false); + }; - useEffect(() => { setLoading(true); setError(null); - Promise.all([fetchProducts(), fetchSites()]) - .then(([prods, siteRefs]) => { - setProducts(prods); - setSites(siteRefs); - }) - .catch((caught) => { - setError(caught instanceof Error ? caught.message : String(caught)); - }) - .finally(() => setLoading(false)); - }, [scope]); + void loadRegistry(); + + return () => { + cancelled = true; + }; + }, [candidateWebIds, scope]); const demoActive = products.some((product) => product.id === "_demo"); - const scopeContextValue = useMemo(() => ({ scope }), [scope]); + const scopeContextValue = useMemo(() => ({ scope: dataScope }), [dataScope]); const registryContextValue = useMemo( () => ({ products, sites, demoActive }), [demoActive, products, sites], @@ -366,17 +434,19 @@ export const SupplyChainDataShell = ({ ); if (!mounted || loading || userPreferencesLoading) { - return ; + return ; } if (error) { return ( -
+
-

Failed to load data

-

{error}

-

- Check that a supply chain dataset is published for this workspace. +

+ No supply chain data found in{" "} + @{activeWorkspace?.shortname} +

+

+ Use the web switcher in the top left to switch to a different web.

@@ -385,9 +455,9 @@ export const SupplyChainDataShell = ({ if (products.length === 0) { return ( -
+

- No supply-chain products are available in this workspace. + No supply chain products are available in this workspace.

); diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/opportunity.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/opportunity.tsx index c862fdf6816..012f5c65edc 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/opportunity.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/opportunity.tsx @@ -15,7 +15,7 @@ import { detailDateKeyFromColumns } from "../shared/detail-date-key"; import { useDocs } from "../shared/docs/use-docs"; import { buildCsvContent, downloadCsv } from "../shared/export-utils"; import { useSupplierPerformanceEnabled } from "../shared/feature-flags"; -import { LoadingState, ErrorState } from "../shared/load-state"; +import { ErrorState, SupplyChainAppSkeleton } from "../shared/load-state"; import { ensureNodeStats } from "../shared/normalize-contract"; import { applyOutlierSelectionToStep } from "../shared/outlier-selection"; import { @@ -1788,12 +1788,7 @@ export const OpportunityBrief = ({ } if (siteSummaryLoading || !step) { - return ( - - ); + return ; } if (!filteredStep || !historicalStep || !brief) { diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx index 3341d710ae8..fc6152d9447 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx @@ -9,7 +9,7 @@ import { AnalysisSettingsPanel, HeaderActionButtons, } from "../shared/header-actions"; -import { LoadingState, ErrorState } from "../shared/load-state"; +import { ErrorState, SupplyChainAppSkeleton } from "../shared/load-state"; import { useLowSampleSetting } from "../shared/low-sample-context"; import { ScopeSelect } from "../shared/scope-select"; import { StatChip } from "../shared/stat-chip"; @@ -52,7 +52,6 @@ import type { SupplierMode, } from "./site/shared/row-types"; -const loadingH = css({ h: "64" }); const errorPad = css({ px: "6", py: "4" }); // Fill the layout's main area (a flex column) and clamp our own height to it so // the content pane can scroll internally instead of overflowing the viewport. @@ -406,7 +405,7 @@ export const SiteOverview = ({ ? statusTarget : null; if (loading) { - return ; + return ; } if (error) { return ; From 6ce497b8603370ba7e39c02fbe0ab678972c4216 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan <37743469+CiaranMn@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:35:17 +0100 Subject: [PATCH 5/6] fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/pages/supply-chain/supply-chain-data-shell.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx index eb57d595825..a8723b9e828 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell.tsx @@ -443,7 +443,9 @@ export const SupplyChainDataShell = ({

No supply chain data found in{" "} - @{activeWorkspace?.shortname} + + {activeWorkspace?.shortname ? `@${activeWorkspace.shortname}` : "this web"} +

Use the web switcher in the top left to switch to a different web. From cad45636cad92e48edd9109b2c7adca73164e612 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan Date: Mon, 29 Jun 2026 23:36:20 +0100 Subject: [PATCH 6/6] format --- .../entities-visualizer/entities-table.tsx | 4 +- apps/hash-frontend/src/pages/signup.page.tsx | 74 +++++++++---------- .../supply-chain/supply-chain-data-shell.tsx | 4 +- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx b/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx index fcd9689de04..2c0d8c27d1e 100644 --- a/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx +++ b/apps/hash-frontend/src/pages/shared/entities-visualizer/entities-table.tsx @@ -906,7 +906,9 @@ export const EntitiesTable: FunctionComponent< component="span" sx={{ color: ({ palette }) => palette.gray[50], ml: 0.5 }} > - {totalResultCount != null ? `- ${formatNumber(totalResultCount - rows.length)} remaining` : ""} + {totalResultCount != null + ? `- ${formatNumber(totalResultCount - rows.length)} remaining` + : ""} { skip: !invitationId, }); - const { - data: pendingInvitationsData, - loading: pendingInvitationsLoading, - } = useQuery< - GetMyPendingInvitationsQuery, - GetMyPendingInvitationsQueryVariables - >(getMyPendingInvitationsQuery, { - skip: !!invitationId || !authenticatedUser || !userHasVerifiedEmail, - }); + const { data: pendingInvitationsData, loading: pendingInvitationsLoading } = + useQuery< + GetMyPendingInvitationsQuery, + GetMyPendingInvitationsQueryVariables + >(getMyPendingInvitationsQuery, { + skip: !!invitationId || !authenticatedUser || !userHasVerifiedEmail, + }); const [acceptInvitation] = useMutation< AcceptOrgInvitationMutation, @@ -429,37 +427,37 @@ const SignupPage: NextPageWithLayout = () => { {loadingInvitation ? null : invitation && showInvitationStep && !authenticatedUser ? ( - setShowInvitationStep(false)} - /> - ) : authenticatedUser ? ( - userHasVerifiedEmail ? ( - userHasAccessToHashData?.hasAccessToHash ? ( - - ) : null - ) : ( - { - await refetchAuthenticatedUser(); - - const { data } = await fetchHasAccess(); - - if (!data?.hasAccessToHash) { - void router.replace("/"); - } - }} + setShowInvitationStep(false)} + /> + ) : authenticatedUser ? ( + userHasVerifiedEmail ? ( + userHasAccessToHashData?.hasAccessToHash ? ( + - ) + ) : null ) : ( - - )} + { + await refetchAuthenticatedUser(); + + const { data } = await fetchHasAccess(); + + if (!data?.hasAccessToHash) { + void router.replace("/"); + } + }} + /> + ) + ) : ( + + )} No supply chain data found in{" "} - {activeWorkspace?.shortname ? `@${activeWorkspace.shortname}` : "this web"} + {activeWorkspace?.shortname + ? `@${activeWorkspace.shortname}` + : "this web"}