From b780e390741deb79df60cac63ce3f6ca034b1b00 Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 12:02:59 +0100 Subject: [PATCH 1/9] refactor: remove irrelevant fields from query and fix type errors --- packages/app/src/app/graphql/types.ts | 51 ++++------ .../overmind/effects/gql/dashboard/queries.ts | 21 +++- .../overmind/namespaces/dashboard/state.ts | 10 +- .../Components/Sandbox/SandboxBadge.tsx | 17 ++-- .../Components/Sandbox/SandboxCard.tsx | 8 +- .../Components/Sandbox/SandboxListItem.tsx | 7 +- .../Dashboard/Components/Sandbox/index.tsx | 96 ++++++++++++++----- .../Dashboard/Components/Sandbox/types.ts | 11 +-- .../Content/routes/Deleted/index.tsx | 10 +- packages/app/src/app/pages/Dashboard/types.ts | 11 ++- .../components/src/components/Stats/index.tsx | 38 -------- .../src/components/Stats/stats.stories.tsx | 28 ------ 12 files changed, 151 insertions(+), 157 deletions(-) delete mode 100644 packages/components/src/components/Stats/stats.stories.tsx diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts index 2c36f683425..f40d5c97a99 100644 --- a/packages/app/src/app/graphql/types.ts +++ b/packages/app/src/app/graphql/types.ts @@ -4636,6 +4636,20 @@ export type JoinEligibleWorkspaceMutation = { joinEligibleWorkspace: { __typename?: 'Team'; id: any }; }; +export type RecentlyDeletedTeamSandboxesFragmentFragment = { + __typename?: 'Sandbox'; + id: string; + alias: string | null; + isV2: boolean; + removedAt: string | null; + title: string | null; + collection: { + __typename?: 'Collection'; + id: any | null; + path: string; + } | null; +}; + export type RecentlyDeletedTeamSandboxesQueryVariables = Exact<{ teamId: Scalars['UUID4']; }>; @@ -4651,44 +4665,13 @@ export type RecentlyDeletedTeamSandboxesQuery = { __typename?: 'Sandbox'; id: string; alias: string | null; - title: string | null; - description: string | null; - lastAccessedAt: any; - insertedAt: string; - updatedAt: string; - removedAt: string | null; - privacy: number; - isFrozen: boolean; - screenshotUrl: string | null; - viewCount: number; - likeCount: number; isV2: boolean; - draft: boolean; - restricted: boolean; - authorId: any | null; - teamId: any | null; - source: { __typename?: 'Source'; template: string | null }; - customTemplate: { - __typename?: 'Template'; - id: any | null; - iconUrl: string | null; - } | null; - forkedTemplate: { - __typename?: 'Template'; - id: any | null; - color: string | null; - iconUrl: string | null; - } | null; + removedAt: string | null; + title: string | null; collection: { __typename?: 'Collection'; - path: string; id: any | null; - } | null; - author: { __typename?: 'User'; username: string } | null; - permissions: { - __typename?: 'SandboxProtectionSettings'; - preventSandboxLeaving: boolean; - preventSandboxExport: boolean; + path: string; } | null; }>; } | null; diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts index d834c15c529..e7a6a38bd5f 100644 --- a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts +++ b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts @@ -60,6 +60,23 @@ import { githubRepoFragment, } from './fragments'; +const RECENTLY_DELETED_TEAM_SANDBOXES_FRAGMENT = gql` +fragment recentlyDeletedTeamSandboxesFragment on Sandbox { + id + + alias + + collection { + id + path + } + + isV2 + removedAt + title +} +`; + export const deletedTeamSandboxes: Query< RecentlyDeletedTeamSandboxesQuery, RecentlyDeletedTeamSandboxesQueryVariables @@ -73,12 +90,12 @@ export const deletedTeamSandboxes: Query< showDeleted: true orderBy: { field: "updated_at", direction: DESC } ) { - ...sandboxFragmentDashboard + ...recentlyDeletedTeamSandboxesFragment } } } } - ${sandboxFragmentDashboard} + ${RECENTLY_DELETED_TEAM_SANDBOXES_FRAGMENT} `; export const sandboxesByPath: Query< diff --git a/packages/app/src/app/overmind/namespaces/dashboard/state.ts b/packages/app/src/app/overmind/namespaces/dashboard/state.ts index 30a8d40a8b5..8d006b42cd3 100755 --- a/packages/app/src/app/overmind/namespaces/dashboard/state.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/state.ts @@ -6,6 +6,7 @@ import { BranchFragment as Branch, ProjectFragment as Repository, ProjectWithBranchesFragment as RepositoryWithBranches, + RecentlyDeletedTeamSandboxesFragmentFragment, } from 'app/graphql/types'; import isSameWeek from 'date-fns/isSameWeek'; import { sortBy } from 'lodash-es'; @@ -17,7 +18,7 @@ import { DELETE_ME_COLLECTION, OrderBy } from './types'; export type DashboardSandboxStructure = { DRAFTS: Sandbox[] | null; TEMPLATES: Template[] | null; - DELETED: Sandbox[] | null; + DELETED: RecentlyDeletedTeamSandboxesFragmentFragment[] | null; RECENT_SANDBOXES: Sandbox[] | null; RECENT_BRANCHES: Branch[] | null; SEARCH: Sandbox[] | null; @@ -50,8 +51,8 @@ export type State = { sandboxes: Array ) => Sandbox[]; deletedSandboxesByTime: { - week: Sandbox[]; - older: Sandbox[]; + week: RecentlyDeletedTeamSandboxesFragmentFragment[]; + older: RecentlyDeletedTeamSandboxesFragmentFragment[]; }; contributions: Branch[] | null; /** @@ -104,8 +105,7 @@ export const state: State = { week: [], older: [], }; - const noTemplateSandboxes = deletedSandboxes.filter(s => !s.customTemplate); - const timeSandboxes = noTemplateSandboxes.reduce( + const timeSandboxes = deletedSandboxes.reduce( (accumulator, currentValue) => { if (!currentValue.removedAt) return accumulator; if (isSameWeek(new Date(currentValue.removedAt), new Date())) { diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxBadge.tsx b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxBadge.tsx index ae2eb6f40a0..42ef33029b8 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxBadge.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxBadge.tsx @@ -1,19 +1,20 @@ import { Icon, Stack, Text } from '@codesandbox/components'; -import { SandboxFragmentDashboardFragment as Sandbox } from 'app/graphql/types'; import React from 'react'; export interface SandboxBadgeProps { - sandbox: Sandbox; - restricted: boolean; + isSandboxV2: boolean; + isSandboxTemplate: boolean; + isSandboxRestricted: boolean; } export const SandboxBadge: React.FC = ({ - sandbox, - restricted, + isSandboxV2, + isSandboxTemplate, + isSandboxRestricted, }) => { - const isDevbox = sandbox.isV2; - const isRestricted = restricted; - const isTemplate = !!sandbox.customTemplate; + const isDevbox = isSandboxV2; + const isRestricted = isSandboxRestricted; + const isTemplate = isSandboxTemplate; const boxIcon = isDevbox ? 'server' : 'boxDevbox'; let boxTypeLabel = isDevbox ? 'Devbox' : 'Sandbox'; diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx index 01867288813..3b92681c1de 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx @@ -166,7 +166,11 @@ const SandboxStats: React.FC = React.memo( )} {noDrag ? null : timeAgoText} - + ); } @@ -297,7 +301,7 @@ export const SandboxCard = ({ - + diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx b/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx index 7bea89a9fde..f7e0a495ab7 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx @@ -8,7 +8,6 @@ import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator'; import { ESC } from '@codesandbox/common/lib/utils/keycodes'; import track from '@codesandbox/common/lib/utils/analytics'; import { Icon } from '@codesandbox/components'; -import { formatNumber } from '@codesandbox/components/lib/components/Stats'; import { SandboxCard } from './SandboxCard'; import { SandboxListItem } from './SandboxListItem'; @@ -19,7 +18,7 @@ import { SandboxItemComponentProps } from './types'; import { useDrag } from '../../utils/dnd'; const PrivacyIcons = { - 0: () => null, + 0: null, 1: () => , 2: () => , }; @@ -47,7 +46,7 @@ function getFolderName(item: GenericSandboxProps['item']): string | undefined { const { sandbox } = item; if (sandbox.collection) { - if (sandbox.collection.path === '/' && !sandbox.teamId) { + if (sandbox.collection.path === '/' && !('teamId' in sandbox)) { return undefined; } @@ -66,26 +65,72 @@ const GenericSandbox = ({ isScrolling, item, page }: GenericSandboxProps) => { const sandboxTitle = sandbox.title || sandbox.alias || sandbox.id; const sandboxLocation = getFolderName(item); - const timeStampToUse = - page === 'recent' ? sandbox.lastAccessedAt : sandbox.updatedAt; - const timeAgo = formatDistanceStrict( - zonedTimeToUtc(timeStampToUse, 'Etc/UTC'), - new Date(), - { + let timeStampToUse: string | undefined; + + // 'lastAccessedAt' and 'updatedAt' are irrelevant for: + // - deleted sandboxes + if (page === 'recent' && 'lastAccessedAt' in sandbox) { + timeStampToUse = sandbox.lastAccessedAt; + } else if ('updatedAt' in sandbox) { + timeStampToUse = sandbox.updatedAt; + } + + let timeAgo: string | undefined; + + // timeStampToUse might be undefined due to the checks above, but + // typescript is not smart enought to know. + if (timeStampToUse) { + const timeStampToUseDate = zonedTimeToUtc(timeStampToUse, 'Etc/UTC'); + const now = new Date(); + + timeAgo = formatDistanceStrict(timeStampToUseDate, now, { addSuffix: true, + }); + } + + const url = sandboxUrl(sandbox); + + let TemplateIcon: + | React.ComponentType<{ width: string; height: string }> + | undefined; + + // 'source' is not present in: + // - deleted sandboxes + if ('source' in sandbox) { + TemplateIcon = getTemplateIcon(sandbox); + } + + let PrivacyIcon: React.ComponentType | undefined; + + // 'privacy' is not present in: + // - deleted sandboxes + if ('privacy' in sandbox) { + PrivacyIcon = PrivacyIcons[sandbox.privacy]; + } + + let restricted = false; + + // 'restricted' and 'draft' are not present in: + // - deleted sandboxes + if ('restricted' in sandbox) { + restricted = sandbox.restricted; + + if ('draft' in sandbox) { + restricted = sandbox.restricted && !sandbox.draft; } - ); + } - const viewCount = formatNumber(sandbox.viewCount); + let screenshotUrl: string | undefined; - const url = sandboxUrl(sandbox); + // 'screenshotUrl' is not present in: + // - deleted sandboxes + if ('screenshotUrl' in sandbox) { + screenshotUrl = sandbox.screenshotUrl; + } - const TemplateIcon = getTemplateIcon(sandbox); - const PrivacyIcon = PrivacyIcons[sandbox.privacy]; - const restricted = sandbox.restricted && !sandbox.draft; + // TODO: Check if screnshotUrl fallback below is still relevant - let screenshotUrl = sandbox.screenshotUrl; // We set a fallback thumbnail in the API which is used for // both old and new dashboard, we can move this logic to the // backend when we deprecate the old dashboard @@ -244,7 +289,6 @@ const GenericSandbox = ({ isScrolling, item, page }: GenericSandboxProps) => { sandboxTitle, sandboxLocation, timeAgo, - viewCount, sandbox, TemplateIcon, PrivacyIcon, @@ -275,18 +319,24 @@ const GenericSandbox = ({ isScrolling, item, page }: GenericSandboxProps) => { }); }, [preview]); + let username: string | undefined; + + // 'author' is not present in: + // - deleted sandboxes + if ('author' in sandbox) { + username = + sandbox.author.username === user?.username + ? 'you' + : sandbox.author.username; + } + return (
); diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/types.ts b/packages/app/src/app/pages/Dashboard/Components/Sandbox/types.ts index c27fb1e408b..a44317811e6 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/types.ts +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/types.ts @@ -5,17 +5,16 @@ export interface SandboxItemComponentProps { sandbox: DashboardSandbox['sandbox'] | DashboardTemplate['sandbox']; sandboxTitle: string; sandboxLocation?: string; - timeAgo: string; - viewCount: number | string; - TemplateIcon: React.FC<{ + timeAgo?: string; + TemplateIcon?: React.ComponentType<{ width: string; height: string; style?: React.CSSProperties; }>; - PrivacyIcon: React.FC; - screenshotUrl: string | null; + PrivacyIcon?: React.ComponentType; + screenshotUrl?: string; restricted: boolean; - username: string | null; + username?: string; interaction: 'button' | 'link'; isScrolling: boolean; selected: boolean; diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx index e17a07241f4..5c15ce32564 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx @@ -6,7 +6,7 @@ import { Header } from 'app/pages/Dashboard/Components/Header'; import { VariableGrid } from 'app/pages/Dashboard/Components/VariableGrid'; import { SelectionProvider } from 'app/pages/Dashboard/Components/Selection'; import { DashboardGridItem, PageTypes } from 'app/pages/Dashboard/types'; -import { SandboxFragmentDashboardFragment } from 'app/graphql/types'; +import { RecentlyDeletedTeamSandboxesFragmentFragment } from 'app/graphql/types'; import { EmptyPage } from 'app/pages/Dashboard/Components/EmptyPage'; import { Loading } from '@codesandbox/components'; @@ -16,7 +16,7 @@ const DESCRIPTION = export const Deleted = () => { const { activeTeam, - dashboard: { deletedSandboxesByTime, getFilteredSandboxes, sandboxes }, + dashboard: { deletedSandboxesByTime, sandboxes }, } = useAppState(); const { dashboard: { getPage }, @@ -29,7 +29,7 @@ export const Deleted = () => { const getSection = ( title: string, - deletedSandboxes: SandboxFragmentDashboardFragment[] + deletedSandboxes: RecentlyDeletedTeamSandboxesFragmentFragment[] ): DashboardGridItem[] => { if (!deletedSandboxes.length) return []; @@ -46,11 +46,11 @@ export const Deleted = () => { ? [ ...getSection( 'Deleted this week', - getFilteredSandboxes(deletedSandboxesByTime.week) + deletedSandboxesByTime.week ), ...getSection( 'Deleted earlier', - getFilteredSandboxes(deletedSandboxesByTime.older) + deletedSandboxesByTime.older ), ] : null; diff --git a/packages/app/src/app/pages/Dashboard/types.ts b/packages/app/src/app/pages/Dashboard/types.ts index d0a94dff15f..dd1780ebc71 100644 --- a/packages/app/src/app/pages/Dashboard/types.ts +++ b/packages/app/src/app/pages/Dashboard/types.ts @@ -4,6 +4,7 @@ import { RepoFragmentDashboardFragment, BranchFragment as Branch, ProjectFragment as Repository, + RecentlyDeletedTeamSandboxesFragmentFragment, } from 'app/graphql/types'; import { Context } from 'app/overmind'; import { @@ -21,10 +22,12 @@ export type DashboardBaseFolder = { export type DashboardSandbox = { type: 'sandbox'; - sandbox: SandboxFragmentDashboardFragment & { - prNumber?: number; - originalGit?: RepoFragmentDashboardFragment['originalGit']; - }; + sandbox: + | (SandboxFragmentDashboardFragment & { + prNumber?: number; + originalGit?: RepoFragmentDashboardFragment['originalGit']; + }) + | RecentlyDeletedTeamSandboxesFragmentFragment; noDrag?: boolean; }; diff --git a/packages/components/src/components/Stats/index.tsx b/packages/components/src/components/Stats/index.tsx index 4163ac77249..8062f3fd32e 100644 --- a/packages/components/src/components/Stats/index.tsx +++ b/packages/components/src/components/Stats/index.tsx @@ -1,7 +1,4 @@ import React from 'react'; -import { Text } from '../Text'; -import { Stack } from '../Stack'; -import { Icon } from '../Icon'; export const HeartIcon = props => ( @@ -40,38 +37,3 @@ export const formatNumber = (count: number): string | number => { return count; }; - -export const Stats = ({ sandbox, ...props }) => ( - - {typeof sandbox.likeCount !== 'undefined' && ( - - - - - - {formatNumber(sandbox.likeCount)} - - - )} - {typeof sandbox.viewCount !== 'undefined' && ( - - - - - - {formatNumber(sandbox.viewCount)} - - - )} - {typeof sandbox.forkCount !== 'undefined' && ( - - - - - - {formatNumber(sandbox.forkCount)} - - - )} - -); diff --git a/packages/components/src/components/Stats/stats.stories.tsx b/packages/components/src/components/Stats/stats.stories.tsx deleted file mode 100644 index 00832a73d18..00000000000 --- a/packages/components/src/components/Stats/stats.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Stats } from '.'; - -export default { - title: 'components/Stats', - component: Stats, -}; - -// replace the text inside with Text variants when available -export const Basic = () => ( - -); - -export const BigNumber = () => ( - -); From ed48726580d63fcb116e6be2a357f9f2e6629314 Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 13:05:42 +0100 Subject: [PATCH 2/9] fix: recover Stats component --- .../components/src/components/Stats/index.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/components/src/components/Stats/index.tsx b/packages/components/src/components/Stats/index.tsx index 8062f3fd32e..cca916fad67 100644 --- a/packages/components/src/components/Stats/index.tsx +++ b/packages/components/src/components/Stats/index.tsx @@ -1,4 +1,7 @@ import React from 'react'; +import { Text } from '../Text'; +import { Stack } from '../Stack'; +import { Icon } from '../Icon'; export const HeartIcon = props => ( @@ -37,3 +40,38 @@ export const formatNumber = (count: number): string | number => { return count; }; + +export const Stats = ({ sandbox, ...props }) => ( + + {typeof sandbox.likeCount !== 'undefined' && ( + + + + + + {formatNumber(sandbox.likeCount)} + + + )} + {typeof sandbox.viewCount !== 'undefined' && ( + + + + + + {formatNumber(sandbox.viewCount)} + + + )} + {typeof sandbox.forkCount !== 'undefined' && ( + + + + + + {formatNumber(sandbox.forkCount)} + + + )} + +); \ No newline at end of file From d94a86e335c4f3beb9ca76b0eb9d74573abe5788 Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 13:18:55 +0100 Subject: [PATCH 3/9] fix: runtime and typechecks for potentially undefined properties --- .../namespaces/dashboard/internalActions.ts | 7 +++ .../Selection/ContextMenus/MultiItemMenu.tsx | 48 ++++++++++++++----- .../Selection/ContextMenus/SandboxMenu.tsx | 22 ++++----- .../Components/Selection/DragPreview.tsx | 31 ++++++++---- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts b/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts index 8e6e9c52b15..b81167d8124 100644 --- a/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts @@ -151,6 +151,13 @@ export const deleteSandboxesFromState = ( repoSandbox.sandboxes = newSandboxes; } }); + } else if (type === 'DELETED') { + const newSandboxes = sandboxStructure[type].filter( + sandbox => !ids.includes(sandbox.id) + ); + if (newSandboxes.length !== sandboxStructure[type].length) { + dashboard.sandboxes[type] = newSandboxes; + } } else if (type !== 'RECENT_BRANCHES') { const newSandboxes = sandboxStructure[type].filter(sandboxFilter); if (newSandboxes.length !== sandboxStructure[type].length) { diff --git a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/MultiItemMenu.tsx b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/MultiItemMenu.tsx index f77fb0da398..b0b50a364aa 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/MultiItemMenu.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/MultiItemMenu.tsx @@ -58,7 +58,11 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { const exportItems = () => { const ids = [ ...sandboxes - .filter(sandbox => !sandbox.sandbox.permissions.preventSandboxExport) + .filter( + s => + 'permissions' in s.sandbox && + !s.sandbox.permissions.preventSandboxExport + ) .map(sandbox => sandbox.sandbox.id), ...templates.map(template => template.sandbox.id), ]; @@ -66,7 +70,8 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { actions.dashboard.downloadSandboxes(ids); const skippedSandboxes = sandboxes.filter( - sandbox => sandbox.sandbox.permissions.preventSandboxExport + s => + 'permissions' in s.sandbox && s.sandbox.permissions.preventSandboxExport ); if (skippedSandboxes.length) { @@ -97,7 +102,7 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { sandboxIds: [...sandboxes, ...templates].map(s => s.sandbox.id), preventSandboxLeaving: Boolean( [...sandboxes, ...templates].find( - s => s.sandbox.permissions.preventSandboxLeaving + s => 'permissions' in s.sandbox && s.sandbox.permissions.preventSandboxLeaving ) ), }); @@ -149,7 +154,7 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { const FROZEN_ITEMS = isInDrafts ? [] : [ - sandboxes.some(s => !s.sandbox.isFrozen) && { + sandboxes.some(s => 'isFrozen' in s.sandbox && !s.sandbox.isFrozen) && { label: 'Protect', fn: () => { actions.dashboard.changeSandboxesFrozen({ @@ -158,7 +163,7 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { }); }, }, - sandboxes.some(s => s.sandbox.isFrozen) && { + sandboxes.some(s => 'isFrozen' in s.sandbox && s.sandbox.isFrozen) && { label: 'Remove protection', fn: () => { actions.dashboard.changeSandboxesFrozen({ @@ -171,7 +176,11 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { const PROTECTED_SANDBOXES_ITEMS = isAdmin ? [ - sandboxes.some(s => !s.sandbox.permissions.preventSandboxLeaving) && { + sandboxes.some( + s => + 'permissions' in s.sandbox && + s.sandbox.permissions.preventSandboxLeaving + ) && { label: 'Prevent leaving workspace', fn: () => { actions.dashboard.setPreventSandboxesLeavingWorkspace({ @@ -180,7 +189,11 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { }); }, }, - sandboxes.some(s => s.sandbox.permissions.preventSandboxLeaving) && { + sandboxes.some( + s => + 'permissions' in s.sandbox && + s.sandbox.permissions.preventSandboxLeaving + ) && { label: 'Allow leaving workspace', fn: () => { actions.dashboard.setPreventSandboxesLeavingWorkspace({ @@ -189,7 +202,11 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { }); }, }, - sandboxes.some(s => !s.sandbox.permissions.preventSandboxExport) && + sandboxes.some( + s => + 'permissions' in s.sandbox && + s.sandbox.permissions.preventSandboxExport + ) && sandboxes.every(s => !s.sandbox.isV2) && { label: 'Prevent export as .zip', fn: () => { @@ -199,7 +216,11 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { }); }, }, - sandboxes.some(s => s.sandbox.permissions.preventSandboxExport) && + sandboxes.some( + s => + 'permissions' in s.sandbox && + s.sandbox.permissions.preventSandboxExport + ) && sandboxes.every(s => !s.sandbox.isV2) && { label: 'Allow export as .zip', fn: () => { @@ -213,8 +234,13 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { : []; const EXPORT = - sandboxes.some(s => !s.sandbox.permissions.preventSandboxExport) && - sandboxes.every(s => !s.sandbox.isV2) + sandboxes.some( + s => + !( + 'permissions' in s.sandbox && + s.sandbox.permissions.preventSandboxExport + ) + ) && sandboxes.every(s => !s.sandbox.isV2) ? [{ label: 'Download', fn: exportItems }] : []; diff --git a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx index 11fd30c2d7b..5d133d1ec28 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx @@ -30,7 +30,7 @@ export const SandboxMenu: React.FC = ({ browser: { copyToClipboard }, } = useEffects(); const { sandbox } = item; - const isTemplate = !!sandbox.customTemplate; + const isTemplate = 'customTemplate' in sandbox && !!sandbox.customTemplate; const { visible, setVisibility, position } = React.useContext(Context); const history = useHistory(); @@ -45,7 +45,7 @@ export const SandboxMenu: React.FC = ({ const restrictedFork = isFrozen; const isInActiveTeam = React.useMemo(() => { - if (item.sandbox.teamId === activeTeam) { + if ('teamId' in item.sandbox && item.sandbox.teamId === activeTeam) { return true; } @@ -82,7 +82,7 @@ export const SandboxMenu: React.FC = ({ } const preventSandboxExport = - !hasWriteAccess || sandbox.permissions.preventSandboxExport; + !hasWriteAccess || ('permissions' in sandbox && sandbox.permissions.preventSandboxExport); // TODO(@CompuIves): refactor this to an array @@ -147,8 +147,8 @@ export const SandboxMenu: React.FC = ({ openInNewWindow: true, redirectAfterFork: true, body: { - privacy: sandbox.privacy as 2 | 1 | 0, - collectionId: sandbox.draft ? undefined : sandbox.collection.id, + privacy: 'privacy' in sandbox ? sandbox.privacy as 2 | 1 | 0 : undefined, + collectionId: 'draft' in sandbox && sandbox.draft ? undefined : 'collection' in sandbox && sandbox.collection.id, }, }); }} @@ -164,7 +164,7 @@ export const SandboxMenu: React.FC = ({ actions.modals.moveSandboxModal.open({ sandboxIds: [item.sandbox.id], preventSandboxLeaving: - item.sandbox.permissions.preventSandboxLeaving, + 'permissions' in item.sandbox && item.sandbox.permissions.preventSandboxLeaving, }); }} > @@ -194,7 +194,7 @@ export const SandboxMenu: React.FC = ({ )} - {hasWriteAccess ? ( + {hasWriteAccess && 'privacy' in sandbox ? ( <> {sandbox.privacy !== 0 && ( @@ -244,7 +244,7 @@ export const SandboxMenu: React.FC = ({ )} {hasWriteAccess && !isTemplate && - (sandbox.isFrozen ? ( + ('isFrozen' in sandbox && sandbox.isFrozen ? ( { actions.dashboard.changeSandboxesFrozen({ @@ -304,7 +304,7 @@ export const SandboxMenu: React.FC = ({ ))} {isPro && hasAdminAccess && - (sandbox.permissions.preventSandboxLeaving ? ( + ('permissions' in sandbox && sandbox.permissions.preventSandboxLeaving ? ( { actions.dashboard.setPreventSandboxesLeavingWorkspace({ @@ -330,7 +330,7 @@ export const SandboxMenu: React.FC = ({ {!sandbox.isV2 && isPro && hasAdminAccess && - (sandbox.permissions.preventSandboxExport ? ( + ('permissions' in sandbox && sandbox.permissions.preventSandboxExport ? ( { actions.dashboard.setPreventSandboxesExport({ @@ -394,7 +394,7 @@ const getFolderUrl = ( if (item.type === 'template') return dashboard.templates(activeTeamId); const path = item.sandbox.collection?.path; - if (path == null || (!item.sandbox.teamId && path === '/')) { + if (path == null || ('teamId' in item.sandbox && !item.sandbox.teamId && path === '/')) { return dashboard.drafts(activeTeamId); } diff --git a/packages/app/src/app/pages/Dashboard/Components/Selection/DragPreview.tsx b/packages/app/src/app/pages/Dashboard/Components/Selection/DragPreview.tsx index b9be2dc9796..cc1598e5e63 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Selection/DragPreview.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Selection/DragPreview.tsx @@ -76,17 +76,30 @@ export const DragPreview: React.FC = React.memo( const sandbox = dashboardEntry.sandbox; - let screenshotUrl = sandbox.screenshotUrl; - // We set a fallback thumbnail in the API which is used for - // both old and new dashboard, we can move this logic to the - // backend when we deprecate the old dashboard - if ( - screenshotUrl === 'https://codesandbox.io/static/img/banner.png' - ) { - screenshotUrl = '/static/img/default-sandbox-thumbnail.png'; + let screenshotUrl; + + // 'screenshotUrl' is not present in: + // - deleted sandboxes + // TODO: Decide if we really want deleted sandboxes to be draggable. + if ('screenshotUrl' in sandbox) { + screenshotUrl = sandbox.screenshotUrl; + // We set a fallback thumbnail in the API which is used for + // both old and new dashboard, we can move this logic to the + // backend when we deprecate the old dashboard + if ( + screenshotUrl === 'https://codesandbox.io/static/img/banner.png' + ) { + screenshotUrl = '/static/img/default-sandbox-thumbnail.png'; + } } - const TemplateIcon = getTemplateIcon(sandbox); + let TemplateIcon: React.ComponentType<{ width: string; height: string }> | undefined; + + // 'source' is not present in: + // - deleted sandboxes + if ('source' in sandbox) { + TemplateIcon = getTemplateIcon(sandbox); + } return { type: 'sandbox', From 9a49333c4bd8c8b6151cec86b4f41e5dc735cdab Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 13:23:06 +0100 Subject: [PATCH 4/9] chore: rename fragment to not include fragment twice --- packages/app/src/app/graphql/types.ts | 2 +- .../src/app/overmind/effects/gql/dashboard/queries.ts | 4 ++-- .../app/src/app/overmind/namespaces/dashboard/state.ts | 8 ++++---- .../pages/Dashboard/Content/routes/Deleted/index.tsx | 10 +++++----- packages/app/src/app/pages/Dashboard/types.ts | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts index f40d5c97a99..983f021e210 100644 --- a/packages/app/src/app/graphql/types.ts +++ b/packages/app/src/app/graphql/types.ts @@ -4636,7 +4636,7 @@ export type JoinEligibleWorkspaceMutation = { joinEligibleWorkspace: { __typename?: 'Team'; id: any }; }; -export type RecentlyDeletedTeamSandboxesFragmentFragment = { +export type RecentlyDeletedTeamSandboxesFragment = { __typename?: 'Sandbox'; id: string; alias: string | null; diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts index e7a6a38bd5f..bb428410c0e 100644 --- a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts +++ b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts @@ -61,7 +61,7 @@ import { } from './fragments'; const RECENTLY_DELETED_TEAM_SANDBOXES_FRAGMENT = gql` -fragment recentlyDeletedTeamSandboxesFragment on Sandbox { +fragment recentlyDeletedTeamSandboxes on Sandbox { id alias @@ -90,7 +90,7 @@ export const deletedTeamSandboxes: Query< showDeleted: true orderBy: { field: "updated_at", direction: DESC } ) { - ...recentlyDeletedTeamSandboxesFragment + ...recentlyDeletedTeamSandboxes } } } diff --git a/packages/app/src/app/overmind/namespaces/dashboard/state.ts b/packages/app/src/app/overmind/namespaces/dashboard/state.ts index 8d006b42cd3..7e481a32eeb 100755 --- a/packages/app/src/app/overmind/namespaces/dashboard/state.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/state.ts @@ -6,7 +6,7 @@ import { BranchFragment as Branch, ProjectFragment as Repository, ProjectWithBranchesFragment as RepositoryWithBranches, - RecentlyDeletedTeamSandboxesFragmentFragment, + RecentlyDeletedTeamSandboxesFragment, } from 'app/graphql/types'; import isSameWeek from 'date-fns/isSameWeek'; import { sortBy } from 'lodash-es'; @@ -18,7 +18,7 @@ import { DELETE_ME_COLLECTION, OrderBy } from './types'; export type DashboardSandboxStructure = { DRAFTS: Sandbox[] | null; TEMPLATES: Template[] | null; - DELETED: RecentlyDeletedTeamSandboxesFragmentFragment[] | null; + DELETED: RecentlyDeletedTeamSandboxesFragment[] | null; RECENT_SANDBOXES: Sandbox[] | null; RECENT_BRANCHES: Branch[] | null; SEARCH: Sandbox[] | null; @@ -51,8 +51,8 @@ export type State = { sandboxes: Array ) => Sandbox[]; deletedSandboxesByTime: { - week: RecentlyDeletedTeamSandboxesFragmentFragment[]; - older: RecentlyDeletedTeamSandboxesFragmentFragment[]; + week: RecentlyDeletedTeamSandboxesFragment[]; + older: RecentlyDeletedTeamSandboxesFragment[]; }; contributions: Branch[] | null; /** diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx index 5c15ce32564..e17a07241f4 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Deleted/index.tsx @@ -6,7 +6,7 @@ import { Header } from 'app/pages/Dashboard/Components/Header'; import { VariableGrid } from 'app/pages/Dashboard/Components/VariableGrid'; import { SelectionProvider } from 'app/pages/Dashboard/Components/Selection'; import { DashboardGridItem, PageTypes } from 'app/pages/Dashboard/types'; -import { RecentlyDeletedTeamSandboxesFragmentFragment } from 'app/graphql/types'; +import { SandboxFragmentDashboardFragment } from 'app/graphql/types'; import { EmptyPage } from 'app/pages/Dashboard/Components/EmptyPage'; import { Loading } from '@codesandbox/components'; @@ -16,7 +16,7 @@ const DESCRIPTION = export const Deleted = () => { const { activeTeam, - dashboard: { deletedSandboxesByTime, sandboxes }, + dashboard: { deletedSandboxesByTime, getFilteredSandboxes, sandboxes }, } = useAppState(); const { dashboard: { getPage }, @@ -29,7 +29,7 @@ export const Deleted = () => { const getSection = ( title: string, - deletedSandboxes: RecentlyDeletedTeamSandboxesFragmentFragment[] + deletedSandboxes: SandboxFragmentDashboardFragment[] ): DashboardGridItem[] => { if (!deletedSandboxes.length) return []; @@ -46,11 +46,11 @@ export const Deleted = () => { ? [ ...getSection( 'Deleted this week', - deletedSandboxesByTime.week + getFilteredSandboxes(deletedSandboxesByTime.week) ), ...getSection( 'Deleted earlier', - deletedSandboxesByTime.older + getFilteredSandboxes(deletedSandboxesByTime.older) ), ] : null; diff --git a/packages/app/src/app/pages/Dashboard/types.ts b/packages/app/src/app/pages/Dashboard/types.ts index dd1780ebc71..d352e4fd880 100644 --- a/packages/app/src/app/pages/Dashboard/types.ts +++ b/packages/app/src/app/pages/Dashboard/types.ts @@ -4,7 +4,7 @@ import { RepoFragmentDashboardFragment, BranchFragment as Branch, ProjectFragment as Repository, - RecentlyDeletedTeamSandboxesFragmentFragment, + RecentlyDeletedTeamSandboxesFragment, } from 'app/graphql/types'; import { Context } from 'app/overmind'; import { @@ -27,7 +27,7 @@ export type DashboardSandbox = { prNumber?: number; originalGit?: RepoFragmentDashboardFragment['originalGit']; }) - | RecentlyDeletedTeamSandboxesFragmentFragment; + | RecentlyDeletedTeamSandboxesFragment; noDrag?: boolean; }; From c12116f0ceca2369278bd7d211387c24f117d786 Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 13:39:34 +0100 Subject: [PATCH 5/9] fix(dashboard): use type guard instead of type assertion in search items filter --- .../Dashboard/Content/routes/Search/searchItems.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Search/searchItems.ts b/packages/app/src/app/pages/Dashboard/Content/routes/Search/searchItems.ts index 9eca0a149f3..b183390c624 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Search/searchItems.ts +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Search/searchItems.ts @@ -13,6 +13,13 @@ type DashboardItem = | SandboxFragmentDashboardFragment | SidebarCollectionDashboardFragment; +// Type guard to check if an item is a sandbox +function isSandbox( + item: DashboardItem | { repository?: any; path?: string } +): item is SandboxFragmentDashboardFragment { + return !('path' in item) && !('repository' in item); +} + // define which fields to search, with per-key thresholds & weights const SEARCH_KEYS = [ { name: 'title', threshold: 0.2, weight: 0.4 }, @@ -99,7 +106,7 @@ export const useGetItems = ({ query: string; username: string; getFilteredSandboxes: ( - list: DashboardItem[] + list: SandboxFragmentDashboardFragment[] ) => SandboxFragmentDashboardFragment[]; }) => { const state = useAppState(); @@ -137,7 +144,7 @@ export const useGetItems = ({ }, [query, searchIndex]); // then the rest is just your existing filtering / mapping logic: - const sandboxesInSearch = foundResults.filter(s => !(s as any).path); + const sandboxesInSearch = foundResults.filter(isSandbox); const foldersInSearch = foundResults.filter(s => (s as any).path); const filteredSandboxes = getFilteredSandboxes(sandboxesInSearch); const isLoadingQuery = query && !searchIndex; From edbf16fd39cb960270f28d1b2f16cdd11332d3d5 Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 13:41:08 +0100 Subject: [PATCH 6/9] fix(dashboard): support filtering deleted sandboxes with reduced field set --- packages/app/src/app/overmind/namespaces/dashboard/state.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/app/overmind/namespaces/dashboard/state.ts b/packages/app/src/app/overmind/namespaces/dashboard/state.ts index 7e481a32eeb..134a9a472e7 100755 --- a/packages/app/src/app/overmind/namespaces/dashboard/state.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/state.ts @@ -48,7 +48,7 @@ export type State = { viewMode: 'grid' | 'list'; orderBy: OrderBy; getFilteredSandboxes: ( - sandboxes: Array + sandboxes: Array ) => Sandbox[]; deletedSandboxesByTime: { week: RecentlyDeletedTeamSandboxesFragment[]; @@ -137,7 +137,7 @@ export const state: State = { }, getFilteredSandboxes: derived( ({ orderBy }: State) => ( - sandboxes: Array + sandboxes: Array ) => { const orderField = orderBy.field; const orderOrder = orderBy.order; @@ -156,7 +156,7 @@ export const state: State = { return field.toLowerCase(); } - if (orderField === 'views') { + if ('viewCount' in sandbox && orderField === 'views') { return sandbox.viewCount; } From d77c675e86e16ceb1b797390c3e6c1f2daa874bc Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 13:53:03 +0100 Subject: [PATCH 7/9] fix: handle null/undefined fields for deleted sandboxes --- .../pages/Dashboard/Components/Sandbox/SandboxCard.tsx | 10 ++++++---- .../Dashboard/Components/Sandbox/SandboxListItem.tsx | 10 +++++++--- .../app/pages/Dashboard/Components/Sandbox/index.tsx | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx index 3b92681c1de..a95368ec7c1 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx @@ -73,9 +73,11 @@ const SandboxTitle: React.FC = React.memo( ) : ( - - - + {TemplateIcon && ( + + + + )} {interaction === 'button' ? ( = React.memo( className="sandbox-stats" > - + {PrivacyIcon && } {isFrozen && ( )} diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx index dbaeaef406a..64ece0735cf 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx @@ -114,7 +114,9 @@ export const SandboxListItem = ({ : null]: `url(${screenshotUrl})`, }} > - {screenshotUrl ? null : } + {!screenshotUrl && TemplateIcon && ( + + )} {editing ? ( @@ -130,7 +132,7 @@ export const SandboxListItem = ({ ) : ( - + {PrivacyIcon ? : null} diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx b/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx index f7e0a495ab7..31987cce629 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx @@ -323,7 +323,7 @@ const GenericSandbox = ({ isScrolling, item, page }: GenericSandboxProps) => { // 'author' is not present in: // - deleted sandboxes - if ('author' in sandbox) { + if ('author' in sandbox && sandbox.author) { username = sandbox.author.username === user?.username ? 'you' From 1934e347675925906340ada71fdc343257742671 Mon Sep 17 00:00:00 2001 From: tristandubbeld Date: Fri, 14 Nov 2025 15:18:28 +0100 Subject: [PATCH 8/9] refactor: create custom sandboxByPath fragment to reduce query payload --- packages/app/src/app/graphql/types.ts | 53 +++++++++++++++---- .../overmind/effects/gql/dashboard/queries.ts | 51 +++++++++++++++++- .../namespaces/dashboard/internalActions.ts | 9 ++-- .../overmind/namespaces/dashboard/state.ts | 5 +- .../app/overmind/namespaces/profile/state.ts | 3 +- .../Profile/SandboxPicker/SandboxCard.tsx | 7 ++- .../app/pages/Profile/SandboxPicker/index.tsx | 6 ++- 7 files changed, 114 insertions(+), 20 deletions(-) diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts index 983f021e210..3e0728d576a 100644 --- a/packages/app/src/app/graphql/types.ts +++ b/packages/app/src/app/graphql/types.ts @@ -4678,6 +4678,46 @@ export type RecentlyDeletedTeamSandboxesQuery = { } | null; }; +export type SandboxByPathFragment = { + __typename?: 'Sandbox'; + id: string; + alias: string | null; + title: string | null; + insertedAt: string; + updatedAt: string; + screenshotUrl: string | null; + isV2: boolean; + isFrozen: boolean; + privacy: number; + restricted: boolean; + draft: boolean; + viewCount: number; + teamId: any | null; + source: { __typename?: 'Source'; template: string | null }; + customTemplate: { + __typename?: 'Template'; + id: any | null; + iconUrl: string | null; + } | null; + forkedTemplate: { + __typename?: 'Template'; + id: any | null; + color: string | null; + iconUrl: string | null; + } | null; + collection: { + __typename?: 'Collection'; + path: string; + id: any | null; + } | null; + author: { __typename?: 'User'; username: string } | null; + permissions: { + __typename?: 'SandboxProtectionSettings'; + preventSandboxLeaving: boolean; + preventSandboxExport: boolean; + } | null; +}; + export type SandboxesByPathQueryVariables = Exact<{ path: Scalars['String']; teamId: InputMaybe; @@ -4703,20 +4743,15 @@ export type SandboxesByPathQuery = { id: string; alias: string | null; title: string | null; - description: string | null; - lastAccessedAt: any; insertedAt: string; updatedAt: string; - removedAt: string | null; - privacy: number; - isFrozen: boolean; screenshotUrl: string | null; - viewCount: number; - likeCount: number; isV2: boolean; - draft: boolean; + isFrozen: boolean; + privacy: number; restricted: boolean; - authorId: any | null; + draft: boolean; + viewCount: number; teamId: any | null; source: { __typename?: 'Source'; template: string | null }; customTemplate: { diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts index bb428410c0e..7df10e8044c 100644 --- a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts +++ b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts @@ -98,6 +98,53 @@ export const deletedTeamSandboxes: Query< ${RECENTLY_DELETED_TEAM_SANDBOXES_FRAGMENT} `; +const SANDBOX_BY_PATH_FRAGMENT = gql` + fragment sandboxByPath on Sandbox { + id + alias + title + insertedAt + updatedAt + screenshotUrl + isV2 + isFrozen + privacy + restricted + draft + viewCount + + source { + template + } + + customTemplate { + id + iconUrl + } + + forkedTemplate { + id + color + iconUrl + } + + collection { + path + id + } + + author { + username + } + teamId + + permissions { + preventSandboxLeaving + preventSandboxExport + } + } +`; + export const sandboxesByPath: Query< SandboxesByPathQuery, SandboxesByPathQueryVariables @@ -113,12 +160,12 @@ export const sandboxesByPath: Query< id path sandboxes { - ...sandboxFragmentDashboard + ...sandboxByPath } } } } - ${sandboxFragmentDashboard} + ${SANDBOX_BY_PATH_FRAGMENT} ${sidebarCollectionDashboard} `; diff --git a/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts b/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts index b81167d8124..e5129441c89 100644 --- a/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts @@ -1,5 +1,8 @@ import { Context } from 'app/overmind'; -import { SandboxFragmentDashboardFragment } from 'app/graphql/types'; +import { + SandboxFragmentDashboardFragment, + SandboxByPathFragment, +} from 'app/graphql/types'; /** * Change sandbox frozen in state and returns the sandboxes that have changed in their old state @@ -15,14 +18,14 @@ export const changeSandboxesInState = ( * The mutation that happens on the sandbox, make sure to return a *new* sandbox here, to make sure * that we can still rollback easily in the future. */ - sandboxMutation: ( + sandboxMutation: ( sandbox: T ) => T; } ) => { const changedSandboxes: Set> = new Set(); - const doMutateSandbox = ( + const doMutateSandbox = ( sandbox: T ): T => { changedSandboxes.add(sandbox); diff --git a/packages/app/src/app/overmind/namespaces/dashboard/state.ts b/packages/app/src/app/overmind/namespaces/dashboard/state.ts index 134a9a472e7..6159c50ab3e 100755 --- a/packages/app/src/app/overmind/namespaces/dashboard/state.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/state.ts @@ -1,5 +1,6 @@ import { SandboxFragmentDashboardFragment as Sandbox, + SandboxByPathFragment, RepoFragmentDashboardFragment as Repo, TemplateFragmentDashboardFragment as Template, TeamFragmentDashboardFragment, @@ -25,7 +26,7 @@ export type DashboardSandboxStructure = { TEMPLATE_HOME: Template[] | null; SHARED: Sandbox[] | null; ALL: { - [path: string]: Sandbox[]; + [path: string]: (Sandbox | SandboxByPathFragment)[]; } | null; REPOS: { [path: string]: { @@ -48,7 +49,7 @@ export type State = { viewMode: 'grid' | 'list'; orderBy: OrderBy; getFilteredSandboxes: ( - sandboxes: Array + sandboxes: Array ) => Sandbox[]; deletedSandboxesByTime: { week: RecentlyDeletedTeamSandboxesFragment[]; diff --git a/packages/app/src/app/overmind/namespaces/profile/state.ts b/packages/app/src/app/overmind/namespaces/profile/state.ts index 80cfbf36959..513a9acd385 100755 --- a/packages/app/src/app/overmind/namespaces/profile/state.ts +++ b/packages/app/src/app/overmind/namespaces/profile/state.ts @@ -2,6 +2,7 @@ import { Profile, Sandbox, UserSandbox } from '@codesandbox/common/lib/types'; import { Collection, SandboxFragmentDashboardFragment as CollectionSandbox, + SandboxByPathFragment, } from 'app/graphql/types'; import { Context } from 'app/overmind'; import { derived } from 'overmind'; @@ -11,7 +12,7 @@ export type ProfileCollection = Pick< Collection, 'id' | 'path' | 'sandboxCount' > & { - sandboxes: CollectionSandbox[]; + sandboxes: (CollectionSandbox | SandboxByPathFragment)[]; }; type State = { diff --git a/packages/app/src/app/pages/Profile/SandboxPicker/SandboxCard.tsx b/packages/app/src/app/pages/Profile/SandboxPicker/SandboxCard.tsx index 7d16f5ce384..ddffe0478eb 100644 --- a/packages/app/src/app/pages/Profile/SandboxPicker/SandboxCard.tsx +++ b/packages/app/src/app/pages/Profile/SandboxPicker/SandboxCard.tsx @@ -13,7 +13,10 @@ import { import designLanguage from '@codesandbox/components/lib/design-language/theme'; import css from '@styled-system/css'; import { Sandbox } from '@codesandbox/common/lib/types'; -import { SandboxFragmentDashboardFragment } from 'app/graphql/types'; +import { + SandboxFragmentDashboardFragment, + SandboxByPathFragment, +} from 'app/graphql/types'; import { SandboxType } from '../constants'; const PrivacyIcons = { @@ -29,7 +32,7 @@ const privacyToName = { }; export const SandboxCard: React.FC<{ - sandbox: Sandbox | SandboxFragmentDashboardFragment; + sandbox: Sandbox | SandboxFragmentDashboardFragment | SandboxByPathFragment; onClick?: (event: React.MouseEvent) => void; }> = ({ sandbox, onClick }) => { const { contextMenu } = useAppState().profile; diff --git a/packages/app/src/app/pages/Profile/SandboxPicker/index.tsx b/packages/app/src/app/pages/Profile/SandboxPicker/index.tsx index 436adc2b617..fb4779256cf 100644 --- a/packages/app/src/app/pages/Profile/SandboxPicker/index.tsx +++ b/packages/app/src/app/pages/Profile/SandboxPicker/index.tsx @@ -10,6 +10,10 @@ import { Column, } from '@codesandbox/components'; import css from '@styled-system/css'; +import { + SandboxFragmentDashboardFragment, + SandboxByPathFragment, +} from 'app/graphql/types'; import { SandboxCard, SkeletonCard } from './SandboxCard'; import { FolderCard } from './FolderCard'; import { ProfileCollectionType } from '../constants'; @@ -53,7 +57,7 @@ export const SandboxPicker: React.FC<{ closeModal?: () => void }> = ({ .filter(collection => collection.parent === selectedPath) .filter(collection => collection.path !== selectedPath); - const sandboxesInPath = + const sandboxesInPath: (SandboxFragmentDashboardFragment | SandboxByPathFragment)[] = collections.find(collection => collection.path === selectedPath) ?.sandboxes || []; From 642d51c77a4a5268e2746e462a44c21837efccfe Mon Sep 17 00:00:00 2001 From: Tristan Date: Tue, 18 Nov 2025 10:22:23 +0100 Subject: [PATCH 9/9] refactor: create custom draftSandbox fragment to reduce query payload (#8831) --- packages/app/src/app/graphql/types.ts | 57 +++++++++++++++---- .../overmind/effects/gql/dashboard/queries.ts | 53 ++++++++++++++++- .../overmind/namespaces/dashboard/actions.ts | 3 +- .../namespaces/dashboard/internalActions.ts | 7 ++- .../overmind/namespaces/dashboard/state.ts | 13 +++-- .../Components/Sandbox/SandboxListItem.tsx | 2 +- packages/app/src/app/pages/Dashboard/types.ts | 4 +- 7 files changed, 113 insertions(+), 26 deletions(-) diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts index 3e0728d576a..506efa19fe5 100644 --- a/packages/app/src/app/graphql/types.ts +++ b/packages/app/src/app/graphql/types.ts @@ -4702,7 +4702,6 @@ export type SandboxByPathFragment = { forkedTemplate: { __typename?: 'Template'; id: any | null; - color: string | null; iconUrl: string | null; } | null; collection: { @@ -4762,7 +4761,6 @@ export type SandboxesByPathQuery = { forkedTemplate: { __typename?: 'Template'; id: any | null; - color: string | null; iconUrl: string | null; } | null; collection: { @@ -4781,6 +4779,47 @@ export type SandboxesByPathQuery = { } | null; }; +export type DraftSandboxFragment = { + __typename?: 'Sandbox'; + id: string; + alias: string | null; + title: string | null; + insertedAt: string; + updatedAt: string; + screenshotUrl: string | null; + isV2: boolean; + isFrozen: boolean; + privacy: number; + restricted: boolean; + draft: boolean; + viewCount: number; + authorId: any | null; + lastAccessedAt: any; + teamId: any | null; + source: { __typename?: 'Source'; template: string | null }; + customTemplate: { + __typename?: 'Template'; + id: any | null; + iconUrl: string | null; + } | null; + forkedTemplate: { + __typename?: 'Template'; + id: any | null; + iconUrl: string | null; + } | null; + collection: { + __typename?: 'Collection'; + path: string; + id: any | null; + } | null; + author: { __typename?: 'User'; username: string } | null; + permissions: { + __typename?: 'SandboxProtectionSettings'; + preventSandboxLeaving: boolean; + preventSandboxExport: boolean; + } | null; +}; + export type TeamDraftsQueryVariables = Exact<{ teamId: Scalars['UUID4']; authorId: InputMaybe; @@ -4798,20 +4837,17 @@ export type TeamDraftsQuery = { id: string; alias: string | null; title: string | null; - description: string | null; - lastAccessedAt: any; insertedAt: string; updatedAt: string; - removedAt: string | null; - privacy: number; - isFrozen: boolean; screenshotUrl: string | null; - viewCount: number; - likeCount: number; isV2: boolean; - draft: boolean; + isFrozen: boolean; + privacy: number; restricted: boolean; + draft: boolean; + viewCount: number; authorId: any | null; + lastAccessedAt: any; teamId: any | null; source: { __typename?: 'Source'; template: string | null }; customTemplate: { @@ -4822,7 +4858,6 @@ export type TeamDraftsQuery = { forkedTemplate: { __typename?: 'Template'; id: any | null; - color: string | null; iconUrl: string | null; } | null; collection: { diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts index 7df10e8044c..441a9a24fe7 100644 --- a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts +++ b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts @@ -124,7 +124,6 @@ const SANDBOX_BY_PATH_FRAGMENT = gql` forkedTemplate { id - color iconUrl } @@ -169,6 +168,54 @@ export const sandboxesByPath: Query< ${sidebarCollectionDashboard} `; +const DRAFT_SANDBOX_FRAGMENT = gql` + fragment draftSandbox on Sandbox { + id + alias + title + insertedAt + updatedAt + screenshotUrl + isV2 + isFrozen + privacy + restricted + draft + viewCount + authorId + lastAccessedAt + + source { + template + } + + customTemplate { + id + iconUrl + } + + forkedTemplate { + id + iconUrl + } + + collection { + path + id + } + + author { + username + } + teamId + + permissions { + preventSandboxLeaving + preventSandboxExport + } + } +`; + export const getTeamDrafts: Query< TeamDraftsQuery, TeamDraftsQueryVariables @@ -179,12 +226,12 @@ export const getTeamDrafts: Query< team(id: $teamId) { drafts(authorId: $authorId) { - ...sandboxFragmentDashboard + ...draftSandbox } } } } - ${sandboxFragmentDashboard} + ${DRAFT_SANDBOX_FRAGMENT} `; export const getCollections: Query< diff --git a/packages/app/src/app/overmind/namespaces/dashboard/actions.ts b/packages/app/src/app/overmind/namespaces/dashboard/actions.ts index 83013d19dd5..5eb692d6e22 100755 --- a/packages/app/src/app/overmind/namespaces/dashboard/actions.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/actions.ts @@ -6,6 +6,7 @@ import { uniq } from 'lodash-es'; import { TemplateFragmentDashboardFragment, SandboxFragmentDashboardFragment, + DraftSandboxFragment, RepoFragmentDashboardFragment, ProjectFragment, } from 'app/graphql/types'; @@ -192,7 +193,7 @@ export const createFolder = async ( export const getDrafts = async ({ state, effects }: Context) => { const { dashboard, activeTeam } = state; try { - let sandboxes: SandboxFragmentDashboardFragment[] = []; + let sandboxes: (SandboxFragmentDashboardFragment | DraftSandboxFragment)[] = []; if (activeTeam) { const data = await effects.gql.queries.getTeamDrafts({ diff --git a/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts b/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts index e5129441c89..8e282535f81 100644 --- a/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/internalActions.ts @@ -2,6 +2,7 @@ import { Context } from 'app/overmind'; import { SandboxFragmentDashboardFragment, SandboxByPathFragment, + DraftSandboxFragment, } from 'app/graphql/types'; /** @@ -18,14 +19,14 @@ export const changeSandboxesInState = ( * The mutation that happens on the sandbox, make sure to return a *new* sandbox here, to make sure * that we can still rollback easily in the future. */ - sandboxMutation: ( + sandboxMutation: ( sandbox: T ) => T; } ) => { const changedSandboxes: Set> = new Set(); - const doMutateSandbox = ( + const doMutateSandbox = ( sandbox: T ): T => { changedSandboxes.add(sandbox); @@ -109,7 +110,7 @@ export const deleteSandboxesFromState = ( ids: string[]; } ) => { - const sandboxFilter = ( + const sandboxFilter = ( sandbox: T ): boolean => !ids.includes(sandbox.id); diff --git a/packages/app/src/app/overmind/namespaces/dashboard/state.ts b/packages/app/src/app/overmind/namespaces/dashboard/state.ts index 6159c50ab3e..d314d957ed5 100755 --- a/packages/app/src/app/overmind/namespaces/dashboard/state.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/state.ts @@ -1,6 +1,7 @@ import { SandboxFragmentDashboardFragment as Sandbox, SandboxByPathFragment, + DraftSandboxFragment, RepoFragmentDashboardFragment as Repo, TemplateFragmentDashboardFragment as Template, TeamFragmentDashboardFragment, @@ -17,16 +18,16 @@ import { derived } from 'overmind'; import { DELETE_ME_COLLECTION, OrderBy } from './types'; export type DashboardSandboxStructure = { - DRAFTS: Sandbox[] | null; + DRAFTS: DraftSandboxFragment[] | null; TEMPLATES: Template[] | null; DELETED: RecentlyDeletedTeamSandboxesFragment[] | null; - RECENT_SANDBOXES: Sandbox[] | null; + RECENT_SANDBOXES: (Sandbox | DraftSandboxFragment)[] | null; RECENT_BRANCHES: Branch[] | null; - SEARCH: Sandbox[] | null; + SEARCH: (Sandbox | DraftSandboxFragment)[] | null; TEMPLATE_HOME: Template[] | null; - SHARED: Sandbox[] | null; + SHARED: (Sandbox | DraftSandboxFragment)[] | null; ALL: { - [path: string]: (Sandbox | SandboxByPathFragment)[]; + [path: string]: (Sandbox | SandboxByPathFragment | DraftSandboxFragment)[]; } | null; REPOS: { [path: string]: { @@ -138,7 +139,7 @@ export const state: State = { }, getFilteredSandboxes: derived( ({ orderBy }: State) => ( - sandboxes: Array + sandboxes: Array ) => { const orderField = orderBy.field; const orderOrder = orderBy.order; diff --git a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx index 64ece0735cf..b6efdec4c22 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxListItem.tsx @@ -162,7 +162,7 @@ export const SandboxListItem = ({
- {sandbox.removedAt ? ( + {'removedAt' in sandbox && sandbox.removedAt ? (