From 866ea3db7fc6a33c88064c844f8d57016db1d7fe Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Tue, 14 Nov 2023 14:35:55 +0000 Subject: [PATCH 01/45] initial commit --- .../app/components/Create/CreateDevbox.tsx | 0 .../app/components/Create/CreateSandbox.tsx | 0 .../app/components/Create/GenericCreate.tsx | 105 ++++++++++ .../components/Create/ImportRepository.tsx | 175 ++++++++++++++++ .../ImportRepository}/AccountSelect.tsx | 0 .../AuthorizeForSuggested.tsx | 0 .../ImportRepository}/FromRepo.tsx | 0 .../ImportRepository}/Import.tsx | 2 +- .../ImportRepository}/ImportInfo.tsx | 0 .../ImportRepository}/PrivateRepoFreeTeam.tsx | 0 .../RestrictedPrivateRepositoriesImport.tsx | 0 .../SuggestedRepositories.tsx | 0 .../ImportRepository}/types.ts | 0 .../ImportRepository}/useGithubRepo.ts | 2 +- .../ImportRepository}/useOrganizationRepos.ts | 2 +- .../useValidateRepoDestination.ts | 0 .../ImportRepository}/utils.test.ts | 0 .../ImportRepository}/utils.ts | 0 .../ImportSandbox/ImportSandbox.tsx | 0 .../ImportSandbox/index.ts | 0 .../src/app/components/Create/elements.tsx | 193 ++++++++++++++++++ .../src/app/components/Create/utils/api.ts | 110 ++++++++++ .../app/components/Create/utils/queries.ts | 184 +++++++++++++++++ .../src/app/components/Create/utils/types.ts | 14 ++ .../CreateSandbox/CreateSandbox.tsx | 78 +------ .../CreateSandbox/Icons/GitHubIcon.tsx | 11 - .../components/CreateSandbox/Icons/index.ts | 1 - .../components/CreateSandbox/Import/index.ts | 1 - .../components/dashboard/LargeCTAButton.tsx | 76 +++++++ packages/app/src/app/overmind/actions.ts | 6 +- .../Components/NewTeamModal/TeamImport.tsx | 2 +- .../SuggestionsRow/SuggestionsRow.tsx | 4 +- .../Content/routes/Recent/RecentHeader.tsx | 128 ++++-------- .../src/app/pages/Dashboard/Header/index.tsx | 4 +- .../app/src/app/pages/Dashboard/index.tsx | 6 +- .../app/src/app/pages/common/Modals/index.tsx | 16 ++ .../components/src/components/Icon/icons.tsx | 51 +++++ 37 files changed, 990 insertions(+), 181 deletions(-) create mode 100644 packages/app/src/app/components/Create/CreateDevbox.tsx create mode 100644 packages/app/src/app/components/Create/CreateSandbox.tsx create mode 100644 packages/app/src/app/components/Create/GenericCreate.tsx create mode 100644 packages/app/src/app/components/Create/ImportRepository.tsx rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/AccountSelect.tsx (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/AuthorizeForSuggested.tsx (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/FromRepo.tsx (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/Import.tsx (99%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/ImportInfo.tsx (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/PrivateRepoFreeTeam.tsx (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/RestrictedPrivateRepositoriesImport.tsx (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/SuggestedRepositories.tsx (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/types.ts (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/useGithubRepo.ts (96%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/useOrganizationRepos.ts (94%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/useValidateRepoDestination.ts (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/utils.test.ts (100%) rename packages/app/src/app/components/{CreateSandbox/Import => Create/ImportRepository}/utils.ts (100%) rename packages/app/src/app/components/{CreateSandbox => Create}/ImportSandbox/ImportSandbox.tsx (100%) rename packages/app/src/app/components/{CreateSandbox => Create}/ImportSandbox/index.ts (100%) create mode 100644 packages/app/src/app/components/Create/elements.tsx create mode 100644 packages/app/src/app/components/Create/utils/api.ts create mode 100644 packages/app/src/app/components/Create/utils/queries.ts create mode 100644 packages/app/src/app/components/Create/utils/types.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/Icons/GitHubIcon.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/Icons/index.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/Import/index.ts create mode 100644 packages/app/src/app/components/dashboard/LargeCTAButton.tsx diff --git a/packages/app/src/app/components/Create/CreateDevbox.tsx b/packages/app/src/app/components/Create/CreateDevbox.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/app/src/app/components/Create/CreateSandbox.tsx b/packages/app/src/app/components/Create/CreateSandbox.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/app/src/app/components/Create/GenericCreate.tsx b/packages/app/src/app/components/Create/GenericCreate.tsx new file mode 100644 index 00000000000..9ab58b47ed5 --- /dev/null +++ b/packages/app/src/app/components/Create/GenericCreate.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { ModalContentProps } from 'app/pages/common/Modals'; + +import { Stack, Text, IconButton, Element } from '@codesandbox/components'; +import track from '@codesandbox/common/lib/utils/analytics'; +import { useActions } from 'app/overmind'; +import { Container, HeaderInformation } from './elements'; +import { LargeCTAButton } from '../dashboard/LargeCTAButton'; + +export const GenericCreate: React.FC = ({ + closeModal, + isModal, +}) => { + const actions = useActions(); + const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); + const mobileScreenSize = mediaQuery.matches; + + return ( + + + + + Create + + + + {/* isModal is undefined on /s/ page */} + {isModal ? ( + closeModal()} + /> + ) : null} + + + + { + track('Create Modal - Import Repository', { + codesandbox: 'V1', + event_source: 'UI', + }); + closeModal(); + actions.modalOpened({ modal: 'importRepository' }); + }} + variant="primary" + alignment="vertical" + /> + + { + track('Create Modal - Create Devbox', { + codesandbox: 'V1', + event_source: 'UI', + }); + closeModal(); + actions.openCreateSandboxModal(); + }} + variant="primary" + alignment="vertical" + /> + + { + track('Create Modal - Create Sandbox', { + codesandbox: 'V1', + event_source: 'UI', + }); + closeModal(); + actions.openCreateSandboxModal(); + }} + variant="secondary" + alignment="vertical" + /> + + + ); +}; diff --git a/packages/app/src/app/components/Create/ImportRepository.tsx b/packages/app/src/app/components/Create/ImportRepository.tsx new file mode 100644 index 00000000000..2f21366132e --- /dev/null +++ b/packages/app/src/app/components/Create/ImportRepository.tsx @@ -0,0 +1,175 @@ +import { + Text, + Stack, + IconButton, + ThemeProvider, +} from '@codesandbox/components'; +import { useAppState } from 'app/overmind'; +import React, { useState } from 'react'; +import { useTabState } from 'reakit/Tab'; + +import track from '@codesandbox/common/lib/utils/analytics'; + +import { ModalContentProps } from 'app/pages/common/Modals'; +import { + Container, + Tab, + Panel, + Tabs, + HeaderInformation, + ModalContent, + ModalSidebar, + ModalBody, +} from './elements'; +import { Import } from './ImportRepository/Import'; +import { GithubRepoToImport } from './ImportRepository/types'; +import { ImportInfo } from './ImportRepository/ImportInfo'; +import { FromRepo } from './ImportRepository/FromRepo'; +import { ImportSandbox } from './ImportSandbox'; + +export const COLUMN_MEDIA_THRESHOLD = 1600; + +export const ImportRepository: React.FC = ({ + isModal, + closeModal, +}) => { + const { environment } = useAppState(); + + const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); + const mobileScreenSize = mediaQuery.matches; + + const tabState = useTabState({ + orientation: mobileScreenSize ? 'horizontal' : 'vertical', + selectedId: 'import', + }); + + const [viewState, setViewState] = useState<'initial' | 'fork'>('initial'); + + const [selectedRepo, setSelectedRepo] = useState(); + + const selectGithubRepo = (repo: GithubRepoToImport) => { + setSelectedRepo(repo); + setViewState('fork'); + }; + + const showImportRepository = !environment.isOnPrem; + + return ( + + + + + {viewState === 'initial' ? ( + + New + + ) : ( + // TODO: add aria-label based on title to IconButton? + { + setViewState('initial'); + }} + /> + )} + + + {/* isModal is undefined on /s/ page */} + {isModal ? ( + // TODO: IconButton doesn't have aria label or visuallyhidden text (reads floating label too late) + closeModal()} + /> + ) : null} + + + + + {viewState === 'initial' ? ( + + + {showImportRepository && ( + { + track('Create New - Click Tab', { + codesandbox: 'V1', + event_source: 'UI', + tab_name: 'Import from Github', + }); + }} + stopId="import" + > + Import repository + + )} + + { + track('Create New - Click Tab', { + codesandbox: 'V1', + event_source: 'UI', + tab_name: 'Import template', + }); + }} + stopId="import-template" + > + Import template + + + + ) : null} + + {viewState === 'fork' ? ( + + ) : null} + + + + {viewState === 'initial' && ( + + + + + + + + + + )} + + {viewState === 'fork' ? ( + { + setViewState('initial'); + }} + /> + ) : null} + + + + + ); +}; diff --git a/packages/app/src/app/components/CreateSandbox/Import/AccountSelect.tsx b/packages/app/src/app/components/Create/ImportRepository/AccountSelect.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/AccountSelect.tsx rename to packages/app/src/app/components/Create/ImportRepository/AccountSelect.tsx diff --git a/packages/app/src/app/components/CreateSandbox/Import/AuthorizeForSuggested.tsx b/packages/app/src/app/components/Create/ImportRepository/AuthorizeForSuggested.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/AuthorizeForSuggested.tsx rename to packages/app/src/app/components/Create/ImportRepository/AuthorizeForSuggested.tsx diff --git a/packages/app/src/app/components/CreateSandbox/Import/FromRepo.tsx b/packages/app/src/app/components/Create/ImportRepository/FromRepo.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/FromRepo.tsx rename to packages/app/src/app/components/Create/ImportRepository/FromRepo.tsx diff --git a/packages/app/src/app/components/CreateSandbox/Import/Import.tsx b/packages/app/src/app/components/Create/ImportRepository/Import.tsx similarity index 99% rename from packages/app/src/app/components/CreateSandbox/Import/Import.tsx rename to packages/app/src/app/components/Create/ImportRepository/Import.tsx index 99cecfdbbbf..e458d11ca57 100644 --- a/packages/app/src/app/components/CreateSandbox/Import/Import.tsx +++ b/packages/app/src/app/components/Create/ImportRepository/Import.tsx @@ -33,7 +33,7 @@ import { GithubRepoToImport } from './types'; import { useGithubRepo } from './useGithubRepo'; import { getOwnerAndNameFromInput } from './utils'; import { SuggestedRepositories } from './SuggestedRepositories'; -import { GET_REPOSITORY_TEAMS } from '../queries'; +import { GET_REPOSITORY_TEAMS } from '../utils/queries'; const UnauthenticatedImport: React.FC = () => { const actions = useActions(); diff --git a/packages/app/src/app/components/CreateSandbox/Import/ImportInfo.tsx b/packages/app/src/app/components/Create/ImportRepository/ImportInfo.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/ImportInfo.tsx rename to packages/app/src/app/components/Create/ImportRepository/ImportInfo.tsx diff --git a/packages/app/src/app/components/CreateSandbox/Import/PrivateRepoFreeTeam.tsx b/packages/app/src/app/components/Create/ImportRepository/PrivateRepoFreeTeam.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/PrivateRepoFreeTeam.tsx rename to packages/app/src/app/components/Create/ImportRepository/PrivateRepoFreeTeam.tsx diff --git a/packages/app/src/app/components/CreateSandbox/Import/RestrictedPrivateRepositoriesImport.tsx b/packages/app/src/app/components/Create/ImportRepository/RestrictedPrivateRepositoriesImport.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/RestrictedPrivateRepositoriesImport.tsx rename to packages/app/src/app/components/Create/ImportRepository/RestrictedPrivateRepositoriesImport.tsx diff --git a/packages/app/src/app/components/CreateSandbox/Import/SuggestedRepositories.tsx b/packages/app/src/app/components/Create/ImportRepository/SuggestedRepositories.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/SuggestedRepositories.tsx rename to packages/app/src/app/components/Create/ImportRepository/SuggestedRepositories.tsx diff --git a/packages/app/src/app/components/CreateSandbox/Import/types.ts b/packages/app/src/app/components/Create/ImportRepository/types.ts similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/types.ts rename to packages/app/src/app/components/Create/ImportRepository/types.ts diff --git a/packages/app/src/app/components/CreateSandbox/Import/useGithubRepo.ts b/packages/app/src/app/components/Create/ImportRepository/useGithubRepo.ts similarity index 96% rename from packages/app/src/app/components/CreateSandbox/Import/useGithubRepo.ts rename to packages/app/src/app/components/Create/ImportRepository/useGithubRepo.ts index de065731af6..454b9e40b8f 100644 --- a/packages/app/src/app/components/CreateSandbox/Import/useGithubRepo.ts +++ b/packages/app/src/app/components/Create/ImportRepository/useGithubRepo.ts @@ -3,7 +3,7 @@ import { GetGithubRepoQuery, GetGithubRepoQueryVariables, } from 'app/graphql/types'; -import { GET_GITHUB_REPO } from '../queries'; +import { GET_GITHUB_REPO } from '../utils/queries'; import { GithubRepoToImport } from './types'; type State = diff --git a/packages/app/src/app/components/CreateSandbox/Import/useOrganizationRepos.ts b/packages/app/src/app/components/Create/ImportRepository/useOrganizationRepos.ts similarity index 94% rename from packages/app/src/app/components/CreateSandbox/Import/useOrganizationRepos.ts rename to packages/app/src/app/components/Create/ImportRepository/useOrganizationRepos.ts index 37db8704674..9e8f99b1ced 100644 --- a/packages/app/src/app/components/CreateSandbox/Import/useOrganizationRepos.ts +++ b/packages/app/src/app/components/Create/ImportRepository/useOrganizationRepos.ts @@ -3,7 +3,7 @@ import { GetGitHubOrganizationReposQuery, GetGitHubOrganizationReposQueryVariables, } from 'app/graphql/types'; -import { GET_GITHUB_ORGANIZATION_REPOS } from '../queries'; +import { GET_GITHUB_ORGANIZATION_REPOS } from '../utils/queries'; /** * The organization property can be undefined because the organization diff --git a/packages/app/src/app/components/CreateSandbox/Import/useValidateRepoDestination.ts b/packages/app/src/app/components/Create/ImportRepository/useValidateRepoDestination.ts similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/useValidateRepoDestination.ts rename to packages/app/src/app/components/Create/ImportRepository/useValidateRepoDestination.ts diff --git a/packages/app/src/app/components/CreateSandbox/Import/utils.test.ts b/packages/app/src/app/components/Create/ImportRepository/utils.test.ts similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/utils.test.ts rename to packages/app/src/app/components/Create/ImportRepository/utils.test.ts diff --git a/packages/app/src/app/components/CreateSandbox/Import/utils.ts b/packages/app/src/app/components/Create/ImportRepository/utils.ts similarity index 100% rename from packages/app/src/app/components/CreateSandbox/Import/utils.ts rename to packages/app/src/app/components/Create/ImportRepository/utils.ts diff --git a/packages/app/src/app/components/CreateSandbox/ImportSandbox/ImportSandbox.tsx b/packages/app/src/app/components/Create/ImportSandbox/ImportSandbox.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/ImportSandbox/ImportSandbox.tsx rename to packages/app/src/app/components/Create/ImportSandbox/ImportSandbox.tsx diff --git a/packages/app/src/app/components/CreateSandbox/ImportSandbox/index.ts b/packages/app/src/app/components/Create/ImportSandbox/index.ts similarity index 100% rename from packages/app/src/app/components/CreateSandbox/ImportSandbox/index.ts rename to packages/app/src/app/components/Create/ImportSandbox/index.ts diff --git a/packages/app/src/app/components/Create/elements.tsx b/packages/app/src/app/components/Create/elements.tsx new file mode 100644 index 00000000000..a3064165081 --- /dev/null +++ b/packages/app/src/app/components/Create/elements.tsx @@ -0,0 +1,193 @@ +import styled from 'styled-components'; +import React, { ReactNode } from 'react'; +import { Tab as BaseTab, TabList, TabPanel, TabStateReturn } from 'reakit/Tab'; +import { Select } from '@codesandbox/components'; + +export const Container = styled.div` + height: 530px; + overflow: hidden; + border-radius: 4px; + background-color: #151515; + color: #e5e5e5; + display: flex; + flex-direction: column; +`; + +export const HeaderInformation = styled.div` + flex-grow: 1; +`; + +export const ModalBody = styled.div` + display: flex; + flex: 1; + overflow: hidden; + + @media screen and (max-width: 950px) { + flex-direction: column; + gap: 16px; + } +`; + +export const ModalSidebar = styled.div` + width: 176px; + flex-shrink: 0; + padding: 0px 24px; + overflow: auto; + + @media screen and (max-width: 950px) { + width: auto; + padding: 8px 8px 0; + } +`; + +export const ModalContent = styled.div` + flex-grow: 1; + padding: 0 24px; + overflow: auto; + + @media screen and (max-width: 950px) { + padding: 0 16px; + } +`; + +interface PanelProps { + tab: TabStateReturn; + id: string; + children: ReactNode; +} + +/** + * The Panel component handles the conditional rendering of the actual panel content. This is + * done with render props as per the Reakit docs. + */ +export const Panel = ({ tab, id, children }: PanelProps) => { + return ( + + {({ hidden, ...rest }) => + hidden ? null :
{children}
+ } +
+ ); +}; + +export const Tabs = styled(TabList)` + display: flex; + flex-direction: column; + + @media screen and (max-width: 950px) { + flex-direction: row; + } +`; + +export const Tab = styled(BaseTab)` + text-align: left; + padding: 8px 0; + margin-bottom: 4px; + + border: none; + background: transparent; + color: #999999; + font-family: 'Inter', sans-serif; + font-size: 13px; + line-height: 16px; + cursor: pointer; + white-space: nowrap; + + &[aria-selected='true'], + :hover { + color: #e5e5e5; + } + + &:focus { + outline: none; + } + + @media screen and (max-width: 950px) { + margin-bottom: 0; + padding: 8px; + } +`; + +export const TabContent = styled(TabPanel)` + width: 100%; + height: 100%; + outline: none; +`; + +export const TemplateButton = styled.button` + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 16px; + background: #1d1d1d; + border: 1px solid transparent; + text-align: left; + font-family: inherit; + border-radius: 2px; + color: #e5e5e5; + transition: background ${props => props.theme.speeds[2]} ease-out; + outline: none; + + &:hover:not(:disabled) { + background: #252525; + } + + &:focus-visible { + border-color: ${props => props.theme.colors.purple}; + } + + &:disabled { + opacity: 0.4; + } +`; + +export const TemplateGrid = styled.div` + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + overflow: auto; + padding-bottom: 12px; + + @media screen and (max-width: 756px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media screen and (max-width: 485px) { + grid-template-columns: 1fr; + } +`; + +// Select component places the content with a fixed padding if it has an icon +// !important here will overule that setting since the new select is bigger +export const StyledSelect = styled(Select)` + height: 48px; + padding-left: 44px !important; + font-family: inherit; + height: 32px; + padding: 8px 16px; + background-color: #2a2a2a; + color: #999999; + border: none; + border-radius: 2px; + font-size: 13px; + line-height: 16px; + font-weight: 500; + &:hover { + color: #e5e5e5; + } + &:focus { + color: #e5e5e5; + } +`; + +export const UnstyledButtonLink = styled.button` + appearance: none; + padding: 0; + background: transparent; + color: inherit; + border: none; + font-size: inherit; + font-family: inherit; + text-decoration: underline; + cursor: pointer; +`; diff --git a/packages/app/src/app/components/Create/utils/api.ts b/packages/app/src/app/components/Create/utils/api.ts new file mode 100644 index 00000000000..266f325e786 --- /dev/null +++ b/packages/app/src/app/components/Create/utils/api.ts @@ -0,0 +1,110 @@ +import { TemplateType } from '@codesandbox/common/lib/templates'; +import { isServer } from '@codesandbox/common/lib/templates/helpers/is-server'; +import { TemplateInfo } from './types'; + +interface IExploreTemplate { + title: string; + sandboxes: { + id: string; + title: string | null; + alias: string | null; + description: string | null; + inserted_at: string; + updated_at: string; + author: { username: string } | null; + environment: TemplateType; + v2?: boolean; + custom_template: { + id: string; + icon_url: string; + color: string; + }; + collection?: { + team: { + name: string; + }; + }; + git: { + id: string; + username: string; + commit_sha: string; + path: string; + repo: string; + branch: string; + }; + }[]; +} + +const mapAPIResponseToTemplateInfo = ( + exploreTemplate: IExploreTemplate +): TemplateInfo => ({ + key: exploreTemplate.title, + title: exploreTemplate.title, + templates: exploreTemplate.sandboxes.map(sandbox => ({ + id: sandbox.custom_template.id, + color: sandbox.custom_template.color, + iconUrl: sandbox.custom_template.icon_url, + published: true, + sandbox: { + id: sandbox.id, + insertedAt: sandbox.inserted_at, + updatedAt: sandbox.updated_at, + alias: sandbox.alias, + title: sandbox.title, + author: sandbox.author, + description: sandbox.description, + source: { + template: sandbox.environment, + }, + // TODO: Update /official and /essential endpoints to return + // team -> name instead of collection -> team -> name + team: { + name: 'CodeSandbox', + }, + isV2: sandbox.v2, + isSse: isServer(sandbox.environment), + git: sandbox.git && { + id: sandbox.git.id, + username: sandbox.git.username, + commitSha: sandbox.git.commit_sha, + path: sandbox.git.path, + repo: sandbox.git.repo, + branch: sandbox.git.branch, + }, + }, + })), +}); + +export const getTemplateInfosFromAPI = (url: string): Promise => + fetch(url) + .then(res => res.json()) + .then((body: IExploreTemplate[]) => body.map(mapAPIResponseToTemplateInfo)); + +type ValidateRepositoryDestinationFn = ( + destination: string +) => Promise<{ valid: boolean; message?: string }>; + +/** + * @param destination In the format of `owner/repo` + */ +export const validateRepositoryDestination: ValidateRepositoryDestinationFn = destination => { + // Get the authentication token from local storage if it exists. + const token = localStorage.getItem('devJwt'); + + return fetch(`/api/beta/repos/validate/github/${destination}`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'x-codesandbox-client': 'legacy-web', + authorization: token ? `Bearer ${token}` : '', + }, + }) + .then(res => { + if (!res.ok) { + throw Error(res.statusText); + } + + return res; + }) + .then(res => res.json()); +}; diff --git a/packages/app/src/app/components/Create/utils/queries.ts b/packages/app/src/app/components/Create/utils/queries.ts new file mode 100644 index 00000000000..a77e23581f9 --- /dev/null +++ b/packages/app/src/app/components/Create/utils/queries.ts @@ -0,0 +1,184 @@ +import gql from 'graphql-tag'; + +const TEMPLATE_FRAGMENT = gql` + fragment Template on Template { + id + color + iconUrl + published + sandbox { + id + alias + title + description + insertedAt + updatedAt + isV2 + isSse + + team { + name + } + + author { + username + } + + source { + template + } + } + } +`; + +export const LIST_PERSONAL_TEMPLATES = gql` + query ListPersonalTemplates { + me { + templates { + ...Template + } + + recentlyUsedTemplates { + ...Template + + sandbox { + git { + id + username + commitSha + path + repo + branch + } + } + } + + bookmarkedTemplates { + ...Template + } + + teams { + id + name + bookmarkedTemplates { + ...Template + } + templates { + ...Template + } + } + } + } + + ${TEMPLATE_FRAGMENT} +`; + +export const GET_GITHUB_REPO = gql` + query GetGithubRepo($owner: String!, $name: String!) { + githubRepo(owner: $owner, repo: $name) { + name + fullName + updatedAt + pushedAt + authorization + private + owner { + id + login + avatarUrl + } + } + } +`; + +const PROFILE_FRAGMENT = gql` + fragment Profile on GithubProfile { + id + login + name + } +`; + +const ORGANIZATION_FRAGMENT = gql` + fragment Organization on GithubOrganization { + id + login + } +`; + +export const GET_GITHUB_ACCOUNTS = gql` + query GetGithubAccounts { + me { + githubProfile { + ...Profile + } + githubOrganizations { + ...Organization + } + } + } + + ${PROFILE_FRAGMENT} + ${ORGANIZATION_FRAGMENT} +`; + +// TODO: Remove unnecessary fields +export const GET_GITHUB_ACCOUNT_REPOS = gql` + query GetGitHubAccountRepos($perPage: Int, $page: Int) { + me { + id + githubRepos(perPage: $perPage, page: $page) { + id + authorization + fullName + name + private + updatedAt + pushedAt + owner { + id + login + avatarUrl + } + } + } + } +`; + +// TODO: Remove unnecessary fields +export const GET_GITHUB_ORGANIZATION_REPOS = gql` + query GetGitHubOrganizationRepos( + $organization: String! + $perPage: Int + $page: Int + ) { + githubOrganizationRepos( + organization: $organization + perPage: $perPage + page: $page + ) { + id + authorization + fullName + name + private + updatedAt + pushedAt + owner { + id + login + } + } + } +`; + +export const GET_REPOSITORY_TEAMS = gql` + query RepositoryTeams($owner: String!, $name: String!) { + projects(owner: $owner, name: $name, provider: GITHUB) { + team { + id + name + } + } + } +`; diff --git a/packages/app/src/app/components/Create/utils/types.ts b/packages/app/src/app/components/Create/utils/types.ts new file mode 100644 index 00000000000..81923ff19aa --- /dev/null +++ b/packages/app/src/app/components/Create/utils/types.ts @@ -0,0 +1,14 @@ +import { TemplateFragment } from 'app/graphql/types'; + +export type CreateSandboxParams = { + name?: string; + githubOwner?: string; + createRepo?: boolean; +}; + +export interface TemplateInfo { + title?: string; + key: string; + templates: TemplateFragment[]; + isOwned?: boolean; +} diff --git a/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx b/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx index c6142a92e0e..964294c8558 100644 --- a/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx +++ b/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx @@ -27,7 +27,6 @@ import { ModalSidebar, ModalBody, } from './elements'; -import { Import } from './Import'; import { TemplateCategoryList } from './TemplateCategoryList'; import { useEssentialTemplates } from './useEssentialTemplates'; import { FromTemplate } from './FromTemplate'; @@ -36,10 +35,6 @@ import { useTeamTemplates } from './useTeamTemplates'; import { CreateSandboxParams } from './types'; import { SearchBox } from './SearchBox'; import { SearchResults } from './SearchResults'; -import { GithubRepoToImport } from './Import/types'; -import { ImportInfo } from './Import/ImportInfo'; -import { FromRepo } from './Import/FromRepo'; -import { ImportSandbox } from './ImportSandbox'; import { ExperimentalBetaEditor } from './ExperimentalBetaEditor'; export const COLUMN_MEDIA_THRESHOLD = 1600; @@ -90,11 +85,7 @@ export const CreateSandbox: React.FC = ({ }) => { const { hasLogIn, activeTeamInfo, user, environment } = useAppState(); const actions = useActions(); - const isUnderRepositoriesSection = - location.pathname.includes('/my-contributions') || - location.pathname.includes('/repositories'); - const defaultSelectedTab = - initialTab || isUnderRepositoriesSection ? 'import' : 'quickstart'; + const isUser = user?.username === activeTeamInfo?.name; const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); const mobileScreenSize = mediaQuery.matches; @@ -153,16 +144,15 @@ export const CreateSandbox: React.FC = ({ const tabState = useTabState({ orientation: mobileScreenSize ? 'horizontal' : 'vertical', - selectedId: defaultSelectedTab, + selectedId: 'quickstart', }); - const [viewState, setViewState] = useState< - 'initial' | 'fromTemplate' | 'fork' - >('initial'); + const [viewState, setViewState] = useState<'initial' | 'fromTemplate'>( + 'initial' + ); // ❗️ We could combine viewState with selectedTemplate // and selectedRepo to limit the amount of states. const [selectedTemplate] = useState(); - const [selectedRepo, setSelectedRepo] = useState(); const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { @@ -227,14 +217,8 @@ export const CreateSandbox: React.FC = ({ window.open(url, '_blank'); }; - const selectGithubRepo = (repo: GithubRepoToImport) => { - setSelectedRepo(repo); - setViewState('fork'); - }; - const showSearch = !environment.isOnPrem; const showCloudTemplates = !environment.isOnPrem; - const showImportRepository = !environment.isOnPrem; return ( @@ -282,7 +266,7 @@ export const CreateSandbox: React.FC = ({ tabState.select(null); } else { // Restore the default tab when search query is removed - tabState.select(defaultSelectedTab); + tabState.select('quickstart'); } setSearchQuery(query); @@ -322,36 +306,6 @@ export const CreateSandbox: React.FC = ({ Quick start - {showImportRepository && ( - { - track('Create New - Click Tab', { - codesandbox: 'V1', - event_source: 'UI', - tab_name: 'Import from Github', - }); - }} - stopId="import" - > - Import repository - - )} - - { - track('Create New - Click Tab', { - codesandbox: 'V1', - event_source: 'UI', - tab_name: 'Import template', - }); - }} - stopId="import-template" - > - Import template - - {showTeamTemplates ? ( @@ -440,9 +394,6 @@ export const CreateSandbox: React.FC = ({ {viewState === 'fromTemplate' ? ( ) : null} - {viewState === 'fork' ? ( - - ) : null} @@ -496,14 +447,6 @@ export const CreateSandbox: React.FC = ({ /> - - - - - - - - {showTeamTemplates ? ( = ({ }} /> ) : null} - - {viewState === 'fork' ? ( - { - setViewState('initial'); - }} - /> - ) : null} diff --git a/packages/app/src/app/components/CreateSandbox/Icons/GitHubIcon.tsx b/packages/app/src/app/components/CreateSandbox/Icons/GitHubIcon.tsx deleted file mode 100644 index 355f30696d5..00000000000 --- a/packages/app/src/app/components/CreateSandbox/Icons/GitHubIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import IconBase from 'react-icons/IconBase'; - -export const GitHubIcon = props => ( - - - -); diff --git a/packages/app/src/app/components/CreateSandbox/Icons/index.ts b/packages/app/src/app/components/CreateSandbox/Icons/index.ts deleted file mode 100644 index f85c70d516e..00000000000 --- a/packages/app/src/app/components/CreateSandbox/Icons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GitHubIcon } from './GitHubIcon'; diff --git a/packages/app/src/app/components/CreateSandbox/Import/index.ts b/packages/app/src/app/components/CreateSandbox/Import/index.ts deleted file mode 100644 index 1c56553c21a..00000000000 --- a/packages/app/src/app/components/CreateSandbox/Import/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Import } from './Import'; diff --git a/packages/app/src/app/components/dashboard/LargeCTAButton.tsx b/packages/app/src/app/components/dashboard/LargeCTAButton.tsx new file mode 100644 index 00000000000..9fc8914cbcb --- /dev/null +++ b/packages/app/src/app/components/dashboard/LargeCTAButton.tsx @@ -0,0 +1,76 @@ +import styled from 'styled-components'; +import React from 'react'; +import { Icon, IconNames, Stack, Text } from '@codesandbox/components'; + +type LargeCTAButtonProps = { + title: string; + subtitle?: string; + icon: IconNames; + variant?: 'primary' | 'secondary'; + alignment?: 'horizontal' | 'vertical'; +} & Pick, 'onClick'>; + +export const LargeCTAButton = ({ + title, + subtitle, + icon, + onClick, + variant = 'secondary', + alignment = 'horizontal', +}: LargeCTAButtonProps) => { + return ( + + + + + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + + ); +}; + +const StyledButton = styled.button` + all: unset; + display: flex; + align-items: stretch; + padding: 0; + border: 0; + border-radius: 4px; + height: 80px; + overflow: hidden; + background-color: #ffffff; + color: #0e0e0e; + font-family: inherit; + font-weight: 500; + line-height: 16px; + + &:hover { + background-color: #ebebeb; + cursor: pointer; + transition: background-color 75ms ease; + } + + &:focus-visible { + outline: 2px solid #9581ff; + } +`; diff --git a/packages/app/src/app/overmind/actions.ts b/packages/app/src/app/overmind/actions.ts index 610b8fa8473..44382484f5e 100755 --- a/packages/app/src/app/overmind/actions.ts +++ b/packages/app/src/app/overmind/actions.ts @@ -264,7 +264,11 @@ type ModalName = | 'addMemberToWorkspace' | 'legacyPayment' | 'editorSeatsUpgrade' - | 'personalSpaceAnnouncement'; + | 'personalSpaceAnnouncement' + | 'importRepository' + | 'createSandbox' + | 'createDevbox' + | 'genericCreate'; export const modalOpened = ( { state, effects }: Context, diff --git a/packages/app/src/app/pages/Dashboard/Components/NewTeamModal/TeamImport.tsx b/packages/app/src/app/pages/Dashboard/Components/NewTeamModal/TeamImport.tsx index 45a2a27615b..a98ea2d5d15 100644 --- a/packages/app/src/app/pages/Dashboard/Components/NewTeamModal/TeamImport.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/NewTeamModal/TeamImport.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Element, Stack, Text, Button } from '@codesandbox/components'; import track from '@codesandbox/common/lib/utils/analytics'; import { RestrictedPublicReposImport } from 'app/pages/Dashboard/Components/shared/RestrictedPublicReposImport'; -import { SuggestedRepositories } from 'app/components/CreateSandbox/Import/SuggestedRepositories'; +import { SuggestedRepositories } from 'app/components/Create/ImportRepository/SuggestedRepositories'; import { useGitHubPermissions } from 'app/hooks/useGitHubPermissions'; export const TeamImport = ({ onComplete }: { onComplete: () => void }) => { diff --git a/packages/app/src/app/pages/Dashboard/Components/SuggestionsRow/SuggestionsRow.tsx b/packages/app/src/app/pages/Dashboard/Components/SuggestionsRow/SuggestionsRow.tsx index ee100bd3112..7f1b7b1116a 100644 --- a/packages/app/src/app/pages/Dashboard/Components/SuggestionsRow/SuggestionsRow.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/SuggestionsRow/SuggestionsRow.tsx @@ -18,11 +18,11 @@ import { useActions, useAppState } from 'app/overmind'; import { useGithubAccounts } from 'app/hooks/useGithubOrganizations'; import { useGitHubAccountRepositories } from 'app/hooks/useGitHubAccountRepositories'; import { fuzzyMatchGithubToCsb } from 'app/utils/fuzzyMatchGithubToCsb'; -import { AccountSelect } from 'app/components/CreateSandbox/Import/AccountSelect'; +import { AccountSelect } from 'app/components/Create/ImportRepository/AccountSelect'; import { StyledCard } from 'app/pages/Dashboard/Components/shared/StyledCard'; import { SolidSkeleton } from 'app/pages/Dashboard/Components/Skeleton'; import { ProjectFragment as Repository } from 'app/graphql/types'; -import { AuthorizeForSuggested } from 'app/components/CreateSandbox/Import/AuthorizeForSuggested'; +import { AuthorizeForSuggested } from 'app/components/Create/ImportRepository/AuthorizeForSuggested'; import { useGitHubPermissions } from 'app/hooks/useGitHubPermissions'; import { useWorkspaceSubscription } from 'app/hooks/useWorkspaceSubscription'; diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx index 819cd825595..63acfcb0ea0 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx @@ -1,21 +1,13 @@ import track from '@codesandbox/common/lib/utils/analytics'; -import { Stack, Text, Icon } from '@codesandbox/components'; -import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization'; -import { useActions, useAppState } from 'app/overmind'; +import { Stack, Text } from '@codesandbox/components'; +import { LargeCTAButton } from 'app/components/dashboard/LargeCTAButton'; +import { useActions } from 'app/overmind'; import { EmptyPage } from 'app/pages/Dashboard/Components/EmptyPage'; import { UpgradeBanner } from 'app/pages/Dashboard/Components/UpgradeBanner'; import React from 'react'; -import styled from 'styled-components'; -import { useWorkspaceSubscription } from 'app/hooks/useWorkspaceSubscription'; export const RecentHeader: React.FC<{ title: string }> = ({ title }) => { const actions = useActions(); - const { environment } = useAppState(); - - const { isLegacyPersonalPro } = useWorkspaceSubscription(); - const { isTeamViewer } = useWorkspaceAuthorization(); - - const showRepositoryImport = !environment.isOnPrem; return ( @@ -33,88 +25,54 @@ export const RecentHeader: React.FC<{ title: string }> = ({ title }) => { > {title} - - + { + track('Recent Page - Import Repository', { + codesandbox: 'V1', + event_source: 'UI', + }); + actions.modalOpened({ modal: 'importRepository' }); + }} + variant="primary" + /> + + { - track('Empty State Card - Open create modal', { + track('Recent Page - Create Devbox', { codesandbox: 'V1', event_source: 'UI', - card_type: 'get-started-action', - tab: 'default', }); actions.openCreateSandboxModal(); }} - > - New sandbox - - {showRepositoryImport && ( - { - track('Empty State Card - Open create modal', { - codesandbox: 'V1', - event_source: 'UI', - card_type: 'get-started-action', - tab: 'github', - }); - actions.openCreateSandboxModal({ initialTab: 'import' }); - }} - > - Import repository - - )} + variant="primary" + /> - {!isLegacyPersonalPro && !isTeamViewer ? ( - { - track('Empty State Card - Invite members', { - codesandbox: 'V1', - event_source: 'UI', - card_type: 'get-started-action', - }); - actions.openCreateTeamModal({ - step: 'members', - hasNextStep: false, - }); - }} - > - Invite team members - - ) : null} + { + track('Recent Page - Create Sandbox', { + codesandbox: 'V1', + event_source: 'UI', + }); + actions.openCreateSandboxModal(); + }} + variant="secondary" + /> ); }; - -type ButtonInverseLargeProps = { - children: React.ReactNode; -} & Pick, 'onClick'>; - -// TODO: Add the Button component variant below to the design system. -// naming: [component][variant][size] -const ButtonInverseLarge = ({ children, onClick }: ButtonInverseLargeProps) => { - return {children}; -}; - -const StyledButton = styled.button` - all: unset; - display: flex; - align-items: center; - gap: 12px; // In case of icons - padding: 20px 24px; - border-radius: 4px; - background-color: #ffffff; - color: #0e0e0e; - font-size: 13px; - font-weight: 500; - line-height: 16px; - - &:hover { - background-color: #ebebeb; - cursor: pointer; - transition: background-color 75ms ease; - } - - &:focus-visible { - outline: 2px solid #9581ff; - } -`; diff --git a/packages/app/src/app/pages/Dashboard/Header/index.tsx b/packages/app/src/app/pages/Dashboard/Header/index.tsx index 023e6c659a5..dd54175c4b7 100644 --- a/packages/app/src/app/pages/Dashboard/Header/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Header/index.tsx @@ -36,7 +36,7 @@ const SHOW_COMMUNITY_SEARCH = localStorage.SHOW_COMMUNITY_SEARCH; export const Header: React.FC = React.memo( ({ onSidebarToggle }) => { - const { openCreateSandboxModal } = useActions(); + const { modalOpened } = useActions(); const { activeWorkspaceAuthorization, hasLogIn, @@ -114,7 +114,7 @@ export const Header: React.FC = React.memo( css={css({ width: 'auto' })} disabled={activeWorkspaceAuthorization === 'READ'} onClick={() => { - openCreateSandboxModal({}); + modalOpened({ modal: 'genericCreate' }); }} > { const searchParams = new URLSearchParams(location.search); if (JSON.parse(searchParams.get('import_repo'))) { - actions.openCreateSandboxModal({ initialTab: 'import' }); + actions.modalOpened({ modal: 'importRepository' }); } else if (JSON.parse(searchParams.get('create_sandbox'))) { - actions.openCreateSandboxModal(); + actions.openCreateSandboxModal(); // will change + } else if (JSON.parse(searchParams.get('create_devbox'))) { + actions.openCreateSandboxModal(); // will change } else if (searchParams.get('preferences')) { const toToOpen = searchParams.get('preferences'); actions.preferences.openPreferencesModal(toToOpen); diff --git a/packages/app/src/app/pages/common/Modals/index.tsx b/packages/app/src/app/pages/common/Modals/index.tsx index 62e019a0d9a..d7a39968ce1 100644 --- a/packages/app/src/app/pages/common/Modals/index.tsx +++ b/packages/app/src/app/pages/common/Modals/index.tsx @@ -10,6 +10,8 @@ import { useAppState, useActions } from 'app/overmind'; import getVSCodeTheme from 'app/src/app/pages/Sandbox/Editor/utils/get-vscode-theme'; import React, { FunctionComponent, useEffect, useState } from 'react'; +import { ImportRepository } from 'app/components/Create/ImportRepository'; +import { GenericCreate } from 'app/components/Create/GenericCreate'; import { AddPreset } from './AddPreset'; import { DeleteDeploymentModal } from './DeleteDeploymentModal'; import { DeletePreset } from './DeletePreset'; @@ -65,6 +67,14 @@ const modals = { Component: CreateSandbox, width: () => (window.outerWidth > COLUMN_MEDIA_THRESHOLD ? 1200 : 950), }, + genericCreate: { + Component: GenericCreate, + width: 950, + }, + importRepository: { + Component: ImportRepository, + width: 950, + }, share: { Component: ShareModal, width: 1200, @@ -279,6 +289,7 @@ const Modals: FunctionComponent = () => { {modal ? React.createElement(modal.Component, { closeModal: () => modalClosed(), + isModal: true, }) : null} @@ -289,3 +300,8 @@ const Modals: FunctionComponent = () => { }; export { Modals }; + +export interface ModalContentProps { + closeModal: () => void; + isModal: true; +} diff --git a/packages/components/src/components/Icon/icons.tsx b/packages/components/src/components/Icon/icons.tsx index d8f1aefc5b6..520942f5639 100644 --- a/packages/components/src/components/Icon/icons.tsx +++ b/packages/components/src/components/Icon/icons.tsx @@ -1366,3 +1366,54 @@ export const circleBang = props => ( /> ); + +export const boxSandbox = props => ( + + + +); + +export const boxDevbox = props => ( + + + +); + +export const boxRepository = props => ( + + + +); From dfea468cea3237fc881a3e271cb8735066daf45a Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Tue, 14 Nov 2023 16:08:55 +0000 Subject: [PATCH 02/45] feat: Update modals and actions Update modals and actions in multiple components --- .../ImportRepository/PrivateRepoFreeTeam.tsx | 4 +- .../SuggestedRepositories.tsx | 4 +- .../CreateSandbox/CreateSandbox.tsx | 4 -- .../CreateSandbox/ExperimentalBetaEditor.tsx | 59 ------------------- .../components/dashboard/LargeCTAButton.tsx | 5 +- .../Dashboard/Components/Header/index.tsx | 4 +- .../routes/Repositories/EmptyRepositories.tsx | 2 +- .../Content/routes/Repositories/index.tsx | 2 +- .../src/app/pages/Dashboard/Header/index.tsx | 2 +- packages/app/src/app/pages/Profile/Header.tsx | 18 ++++-- .../pages/Sandbox/Editor/Header/Actions.tsx | 16 +++-- .../Workspace/screens/GitHub/CreateRepo.tsx | 4 +- .../src/app/pages/common/Navigation/index.tsx | 17 ++++-- 13 files changed, 49 insertions(+), 92 deletions(-) delete mode 100644 packages/app/src/app/components/CreateSandbox/ExperimentalBetaEditor.tsx diff --git a/packages/app/src/app/components/Create/ImportRepository/PrivateRepoFreeTeam.tsx b/packages/app/src/app/components/Create/ImportRepository/PrivateRepoFreeTeam.tsx index c2d2361c8dd..5cdb6fd6fd7 100644 --- a/packages/app/src/app/components/Create/ImportRepository/PrivateRepoFreeTeam.tsx +++ b/packages/app/src/app/components/Create/ImportRepository/PrivateRepoFreeTeam.tsx @@ -10,7 +10,7 @@ import { getEventName } from './utils'; export const PrivateRepoFreeTeam: React.FC = () => { const { isEligibleForTrial } = useWorkspaceSubscription(); const { isBillingManager } = useWorkspaceAuthorization(); - const { modals } = useActions(); + const { modalClosed } = useActions(); const [, createCheckout, canCheckout] = useCreateCheckout(); @@ -38,7 +38,7 @@ export const PrivateRepoFreeTeam: React.FC = () => { color="#FFFFFF" onClick={() => { if (ctaURL) { - modals.newSandboxModal.close(); + modalClosed(); } else { createCheckout({ trackingLocation: 'dashboard_import_limits', diff --git a/packages/app/src/app/components/Create/ImportRepository/SuggestedRepositories.tsx b/packages/app/src/app/components/Create/ImportRepository/SuggestedRepositories.tsx index 220734aca5c..de85587affa 100644 --- a/packages/app/src/app/components/Create/ImportRepository/SuggestedRepositories.tsx +++ b/packages/app/src/app/components/Create/ImportRepository/SuggestedRepositories.tsx @@ -36,7 +36,7 @@ export const SuggestedRepositories = ({ activeTeamInfo, dashboard: { repositoriesByTeamId }, } = useAppState(); - const { modals, dashboard: dashboardActions } = useActions(); + const { modalClosed, dashboard: dashboardActions } = useActions(); const { restrictsPrivateRepos } = useGitHubPermissions(); const { isFree, isEligibleForTrial } = useWorkspaceSubscription(); const [importsInProgress, setImportsInProgress] = useState< @@ -277,7 +277,7 @@ export const SuggestedRepositories = ({ event_source: 'UI', } ); - modals.newSandboxModal.close(); + modalClosed(); }} > = ({ )) : null} - {tabState.selectedId === 'quickstart' && ( - - )} ))} diff --git a/packages/app/src/app/components/CreateSandbox/ExperimentalBetaEditor.tsx b/packages/app/src/app/components/CreateSandbox/ExperimentalBetaEditor.tsx deleted file mode 100644 index 96e28ff83c7..00000000000 --- a/packages/app/src/app/components/CreateSandbox/ExperimentalBetaEditor.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { - Badge, - MessageStripe, - Stack, - Text, - Switch, -} from '@codesandbox/components'; -import { useGlobalPersistedState } from 'app/hooks/usePersistedState'; -import { useActions } from 'app/overmind'; -import track from '@codesandbox/common/lib/utils/analytics'; -import { UnstyledButtonLink } from './elements'; - -export const ExperimentalBetaEditor = () => { - const actions = useActions(); - const [betaSandboxEditor, setBetaSandboxEditor] = useGlobalPersistedState( - 'BETA_SANDBOX_EDITOR', - false - ); - - if (betaSandboxEditor) { - return ( - - - If you wish to disable the new sandbox editor, open{' '} - { - actions.modals.newSandboxModal.close(); - actions.preferences.openPreferencesModal('experiments'); - }} - > - Preferences - - . - - - ); - } - - return ( - - - - - Beta - - Try the new sandbox editor. - Get a faster and more stable prototyping experience. - - { - setBetaSandboxEditor(true); - track('Enable new sandbox editor - Create modal'); - }} - /> - - - ); -}; diff --git a/packages/app/src/app/components/dashboard/LargeCTAButton.tsx b/packages/app/src/app/components/dashboard/LargeCTAButton.tsx index 9fc8914cbcb..045cff84c58 100644 --- a/packages/app/src/app/components/dashboard/LargeCTAButton.tsx +++ b/packages/app/src/app/components/dashboard/LargeCTAButton.tsx @@ -63,14 +63,15 @@ const StyledButton = styled.button` font-family: inherit; font-weight: 500; line-height: 16px; + transition: background-color 75ms ease; &:hover { - background-color: #ebebeb; + background-color: #ededed; cursor: pointer; - transition: background-color 75ms ease; } &:focus-visible { + background-color: #ededed; outline: 2px solid #9581ff; } `; diff --git a/packages/app/src/app/pages/Dashboard/Components/Header/index.tsx b/packages/app/src/app/pages/Dashboard/Components/Header/index.tsx index ac2e32b7bdf..01485aaf951 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Header/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Header/index.tsx @@ -60,7 +60,7 @@ export const Header = ({ readOnly = false, }: Props) => { const location = useLocation(); - const { modals, dashboard: dashboardActions } = useActions(); + const { modalOpened, dashboard: dashboardActions } = useActions(); const { dashboard } = useAppState(); const repositoriesListPage = @@ -135,7 +135,7 @@ export const Header = ({ {repositoriesListPage && dashboard.viewMode === 'list' && ( {user && } diff --git a/packages/app/src/app/pages/Sandbox/Editor/Header/Actions.tsx b/packages/app/src/app/pages/Sandbox/Editor/Header/Actions.tsx index 0798ead969b..f5dd9f00b4a 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Header/Actions.tsx +++ b/packages/app/src/app/pages/Sandbox/Editor/Header/Actions.tsx @@ -1,5 +1,5 @@ import Tooltip from '@codesandbox/common/lib/components/Tooltip'; -import { Avatar, Button, Stack } from '@codesandbox/components'; +import { Avatar, Button, Icon, Stack } from '@codesandbox/components'; import css from '@styled-system/css'; import { useAppState, useActions } from 'app/overmind'; import { UserMenu } from 'app/pages/common/UserMenu'; @@ -92,7 +92,6 @@ export const Actions = () => { const { signInClicked, modalOpened, - openCreateSandboxModal, editor: { likeSandboxToggled, forkSandboxClicked }, explore: { pickSandboxModal }, } = useActions(); @@ -239,16 +238,23 @@ export const Actions = () => { diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/GitHub/CreateRepo.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/GitHub/CreateRepo.tsx index 8e412a3040e..21cdc1958a8 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/GitHub/CreateRepo.tsx +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/GitHub/CreateRepo.tsx @@ -23,7 +23,7 @@ type CreateRepoProps = { export const CreateRepo: React.FC = ({ disabled }) => { const { git: { createRepoClicked, repoTitleChanged }, - openCreateSandboxModal, + modalOpened, } = useActions(); const { editor: { isAllModulesSynced, currentSandbox }, @@ -65,7 +65,7 @@ export const CreateRepo: React.FC = ({ disabled }) => { want to rather import an existing repository,{' '} openCreateSandboxModal({ initialTab: 'import' })} + onClick={() => modalOpened({ modal: 'importRepository' })} > open the GitHub import diff --git a/packages/app/src/app/pages/common/Navigation/index.tsx b/packages/app/src/app/pages/common/Navigation/index.tsx index 1fda8c08e3a..bcd52ae7534 100644 --- a/packages/app/src/app/pages/common/Navigation/index.tsx +++ b/packages/app/src/app/pages/common/Navigation/index.tsx @@ -7,6 +7,7 @@ import { Avatar, Text, Link, + Icon, } from '@codesandbox/components'; import { LogoFull } from '@codesandbox/common/lib/components/Logo'; import { @@ -24,7 +25,7 @@ type Props = { } & RouteComponentProps; const NavigationComponent = ({ title, match, showActions = true }: Props) => { - const { signInClicked, openCreateSandboxModal } = useActions(); + const { signInClicked, modalOpened } = useActions(); const { isLoggedIn, isAuthenticating, user } = useAppState(); const link = isLoggedIn ? '/dashboard' : '/'; @@ -83,13 +84,19 @@ const NavigationComponent = ({ title, match, showActions = true }: Props) => { ) : null} {showActions && ( )} {isLoggedIn ? ( From 030e215a9f629a28e55ad77fd87f9cc0559bd9f6 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Tue, 14 Nov 2023 16:36:49 +0000 Subject: [PATCH 03/45] feat: Add 'Start something new' heading Added 'Start something new' heading in RecentHeader component. --- .../app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx index 63acfcb0ea0..295320fb706 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx @@ -25,6 +25,9 @@ export const RecentHeader: React.FC<{ title: string }> = ({ title }) => { > {title} + + Start something new + Date: Wed, 15 Nov 2023 08:59:13 +0000 Subject: [PATCH 04/45] Update 11 files --- .../src/app/components/Create/CreateBox.tsx | 476 ++++++++++++++++++ .../app/components/Create/CreateDevbox.tsx | 0 .../app/components/Create/CreateSandbox.tsx | 0 .../app/components/Create/GenericCreate.tsx | 2 +- .../app/components/Create/TemplateCard.tsx | 149 ++++++ .../app/components/Create/TemplateList.tsx | 89 ++++ .../Create/hooks/useEssentialTemplates.ts | 48 ++ .../Create/hooks/useOfficialTemplates.ts | 48 ++ .../Create/hooks/useTeamTemplates.ts | 78 +++ .../Content/routes/Recent/RecentHeader.tsx | 2 +- .../app/src/app/pages/common/Modals/index.tsx | 5 + 11 files changed, 895 insertions(+), 2 deletions(-) create mode 100644 packages/app/src/app/components/Create/CreateBox.tsx delete mode 100644 packages/app/src/app/components/Create/CreateDevbox.tsx delete mode 100644 packages/app/src/app/components/Create/CreateSandbox.tsx create mode 100644 packages/app/src/app/components/Create/TemplateCard.tsx create mode 100644 packages/app/src/app/components/Create/TemplateList.tsx create mode 100644 packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts create mode 100644 packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts create mode 100644 packages/app/src/app/components/Create/hooks/useTeamTemplates.ts diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx new file mode 100644 index 00000000000..72240524b99 --- /dev/null +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -0,0 +1,476 @@ +import { + Text, + Stack, + Element, + IconButton, + SkeletonText, + ThemeProvider, +} from '@codesandbox/components'; +import { useActions, useAppState } from 'app/overmind'; +import React, { ReactNode, useState, useEffect } from 'react'; +import { TabStateReturn, useTabState } from 'reakit/Tab'; +import slugify from '@codesandbox/common/lib/utils/slugify'; +import { getTemplateIcon } from '@codesandbox/common/lib/utils/getTemplateIcon'; +import { TemplateFragment } from 'app/graphql/types'; +import track from '@codesandbox/common/lib/utils/analytics'; +import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator'; + +import { useGlobalPersistedState } from 'app/hooks/usePersistedState'; +import { + Container, + Tab, + TabContent, + Tabs, + HeaderInformation, + ModalContent, + ModalSidebar, + ModalBody, +} from './elements'; +import { TemplateList } from './TemplateList'; +import { useEssentialTemplates } from './hooks/useEssentialTemplates'; +import { useOfficialTemplates } from './hooks/useOfficialTemplates'; +import { useTeamTemplates } from './hooks/useTeamTemplates'; +import { CreateSandboxParams } from './utils/types'; + +export const COLUMN_MEDIA_THRESHOLD = 1600; + +const FEATURED_IDS = [ + 'new', + 'vanilla', + 'vue', + 'hsd8ke', // docker starter v2 + 'fxis37', // next v2 + '9qputt', // vite + react v2 + 'prp60l', // remix v2 + 'angular', + 'react-ts', + 'rjk9n4zj7m', // static v1 +]; + +interface PanelProps { + tab: TabStateReturn; + id: string; + children: ReactNode; +} + +/** + * The Panel component handles the conditional rendering of the actual panel content. This is + * done with render props as per the Reakit docs. + */ +const Panel = ({ tab, id, children }: PanelProps) => { + return ( + + {({ hidden, ...rest }) => + hidden ? null :
{children}
+ } +
+ ); +}; + +interface CreateBoxProps { + collectionId?: string; + isModal?: boolean; + type?: 'devbox' | 'sandbox'; +} + +export const CreateBox: React.FC = ({ + collectionId, + type = 'devbox', + isModal, +}) => { + const { hasLogIn, activeTeamInfo, user } = useAppState(); + const actions = useActions(); + + const isUser = user?.username === activeTeamInfo?.name; + const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); + const mobileScreenSize = mediaQuery.matches; + + const essentialState = useEssentialTemplates(); + const officialTemplatesData = useOfficialTemplates(); + const teamTemplatesData = useTeamTemplates({ + isUser, + teamId: activeTeamInfo?.id, + hasLogIn, + }); + + const officialTemplates = + officialTemplatesData.state === 'ready' + ? officialTemplatesData.templates + : []; + + /** + * Checking for user because user is undefined when landing on /s/, even though + * hasLogIn is true. Only show the team/my templates if the list is populated. + */ + const showTeamTemplates = + hasLogIn && + user && + teamTemplatesData.state === 'ready' && + teamTemplatesData.teamTemplates.length > 0; + + /** + * For the quick start we show: + * - 3 most recently used templates (if they exist) + * - 6 to 9 templates selected from the official list, ensuring the total number + * of unique templates is 9 (recent + official) + */ + const recentlyUsedTemplates = + teamTemplatesData.state === 'ready' + ? teamTemplatesData.recentTemplates.slice(0, 3) + : []; + + const featuredOfficialTemplates = + officialTemplatesData.state === 'ready' + ? FEATURED_IDS.map( + id => + // If the template is already in recently used, don't add it twice + !recentlyUsedTemplates.find(t => t.sandbox.id === id) && + officialTemplates.find(t => t.sandbox.id === id) + ).filter(Boolean) + : []; + + const featuredTemplates = featuredOfficialTemplates.slice( + 0, + featuredOfficialTemplates.length > 0 ? 6 : 9 + ); + + const teamTemplates = + teamTemplatesData.state === 'ready' ? teamTemplatesData.teamTemplates : []; + + const tabState = useTabState({ + orientation: mobileScreenSize ? 'horizontal' : 'vertical', + selectedId: 'featured', + }); + + const [viewState, setViewState] = useState<'initial' | 'fromTemplate'>( + 'initial' + ); + + const [searchQuery, setSearchQuery] = useState(''); + + useEffect(() => { + if (searchQuery) { + track('Create New - Search Templates', { + query: searchQuery, + codesandbox: 'V1', + event_source: 'UI', + }); + } + }, [searchQuery]); + + useEffect(() => { + if (searchQuery && tabState.selectedId) { + setSearchQuery(''); + } + }, [tabState.selectedId]); + + const [hasBetaEditorExperiment] = useGlobalPersistedState( + 'BETA_SANDBOX_EDITOR', + false + ); + + const createFromTemplate = ( + template: TemplateFragment, + { name }: CreateSandboxParams + ) => { + const { sandbox } = template; + + actions.editor.forkExternalSandbox({ + sandboxId: sandbox.id, + openInNewWindow: false, + hasBetaEditorExperiment, + body: { + alias: name, + collectionId, + }, + }); + + actions.modals.newSandboxModal.close(); + }; + + const selectTemplate = ( + template: TemplateFragment, + trackingSource: string + ) => { + track('Create New - Select template', { + codesandbox: 'V1', + event_source: 'UI', + type: 'fork', + tab_name: trackingSource, + template_name: + template.sandbox.title || template.sandbox.alias || template.sandbox.id, + }); + + createFromTemplate(template, {}); + + // Temporarily disable the second screen until we have more functionality on it + // setSelectedTemplate(template); + // setViewState('fromTemplate'); + }; + + const openTemplate = (template: TemplateFragment, trackingSource: string) => { + const { sandbox } = template; + const url = sandboxUrl(sandbox, hasBetaEditorExperiment); + window.open(url, '_blank'); + + track('Create New - Select template', { + codesandbox: 'V1', + event_source: 'UI', + type: 'open', + tab_name: trackingSource, + template_name: + template.sandbox.title || template.sandbox.alias || template.sandbox.id, + }); + }; + + const trackTabClick = (tab: string) => { + track('Create New - Click Tab', { + codesandbox: 'V1', + event_source: 'UI', + tab_name: tab, + }); + }; + + return ( + + + + + {viewState === 'initial' ? ( + + Create {type === 'devbox' ? 'Devbox' : 'Sandbox'} + + ) : ( + // TODO: add aria-label based on title to IconButton? + { + setViewState('initial'); + }} + /> + )} + + + {/* isModal is undefined on /s/ page */} + {isModal ? ( + // TODO: IconButton doesn't have aria label or visuallyhidden text (reads floating label too late) + actions.modals.newSandboxModal.close()} + /> + ) : null} + + + + + {viewState === 'initial' ? ( + + + trackTabClick('featured')} + stopId="featured" + > + Featured templates + + + + + {showTeamTemplates ? ( + trackTabClick('team-templates')} + stopId="team-templates" + > + {`${isUser ? 'My' : 'Team'} templates`} + + ) : null} + + trackTabClick('official-templates')} + stopId="official-templates" + > + Official templates + + + {essentialState.state === 'success' + ? essentialState.essentials.map(essential => ( + trackTabClick(essential.title)} + > + {essential.title} + + )) + : null} + + {!mobileScreenSize && essentialState.state === 'loading' ? ( + + + + + + + + + ) : null} + + {essentialState.state === 'error' ? ( +
{essentialState.error}
+ ) : null} +
+
+ ) : null} + + {/* {viewState === 'fromTemplate' ? ( + + ) : null} */} +
+ + + {viewState === 'initial' && ( + + + { + selectTemplate(template, 'Featured templates'); + }} + onOpenTemplate={template => { + openTemplate(template, 'Featured templates'); + }} + /> + { + selectTemplate(template, 'Featured templates'); + }} + onOpenTemplate={template => { + openTemplate(template, 'Featured templates'); + }} + /> + + + {showTeamTemplates ? ( + + { + selectTemplate(template, 'team-templates'); + }} + onOpenTemplate={template => { + openTemplate(template, 'team-templates'); + }} + /> + + ) : null} + + + { + selectTemplate(template, 'official-templates'); + }} + onOpenTemplate={template => { + openTemplate(template, 'official-templates'); + }} + /> + + + {essentialState.state === 'success' + ? essentialState.essentials.map(essential => ( + + { + selectTemplate(template, essential.title); + }} + onOpenTemplate={template => { + openTemplate(template, essential.title); + }} + /> + + )) + : null} + + )} + + {/* {viewState === 'fromTemplate' ? ( + { + setViewState('initial'); + }} + onSubmit={params => { + createFromTemplate(selectedTemplate, params); + }} + /> + ) : null} */} + +
+
+
+ ); +}; + +interface TemplateInfoProps { + template: TemplateFragment; +} + +const TemplateInfo = ({ template }: TemplateInfoProps) => { + const { UserIcon } = getTemplateIcon( + template.sandbox.title, + template.iconUrl, + template.sandbox?.source?.template + ); + + return ( + + + + + {template.sandbox.title} + + + {template.sandbox?.team?.name} + + + + {template.sandbox.description} + + + ); +}; diff --git a/packages/app/src/app/components/Create/CreateDevbox.tsx b/packages/app/src/app/components/Create/CreateDevbox.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/app/src/app/components/Create/CreateSandbox.tsx b/packages/app/src/app/components/Create/CreateSandbox.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/app/src/app/components/Create/GenericCreate.tsx b/packages/app/src/app/components/Create/GenericCreate.tsx index 9ab58b47ed5..2d9d4f847fd 100644 --- a/packages/app/src/app/components/Create/GenericCreate.tsx +++ b/packages/app/src/app/components/Create/GenericCreate.tsx @@ -78,7 +78,7 @@ export const GenericCreate: React.FC = ({ event_source: 'UI', }); closeModal(); - actions.openCreateSandboxModal(); + actions.modalOpened({ modal: 'createDevbox' }); }} variant="primary" alignment="vertical" diff --git a/packages/app/src/app/components/Create/TemplateCard.tsx b/packages/app/src/app/components/Create/TemplateCard.tsx new file mode 100644 index 00000000000..d8c5293709f --- /dev/null +++ b/packages/app/src/app/components/Create/TemplateCard.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { + Badge, + formatNumber, + Icon, + Stack, + Text, +} from '@codesandbox/components'; +import { getTemplateIcon } from '@codesandbox/common/lib/utils/getTemplateIcon'; +import { docsUrl } from '@codesandbox/common/lib/utils/url-generator'; +import Tooltip from '@codesandbox/common/lib/components/Tooltip'; +import { TemplateFragment } from 'app/graphql/types'; +import { VisuallyHidden } from 'reakit/VisuallyHidden'; +import { useAppState } from 'app/overmind'; +import { TemplateButton } from './elements'; + +interface TemplateCardProps { + disabled?: boolean; + template: TemplateFragment; + onSelectTemplate: (template: TemplateFragment) => void; + onOpenTemplate: (template: TemplateFragment) => void; + padding?: number | string; + forks?: number; + isOfficial?: boolean; +} + +export const TemplateCard = ({ + disabled, + template, + onSelectTemplate, + onOpenTemplate, + padding, + forks, + isOfficial, +}: TemplateCardProps) => { + const { isLoggedIn } = useAppState(); + const { UserIcon } = getTemplateIcon( + template.sandbox.title, + template.iconUrl, + template.sandbox?.source?.template + ); + + const sandboxTitle = template.sandbox?.title || template.sandbox?.alias; + const isV2 = template.sandbox?.isV2; + + const teamName = + template.sandbox?.team?.name || + template.sandbox?.author?.username || + 'GitHub'; + + return ( + { + if (disabled) { + return; + } + + if (evt.metaKey || evt.ctrlKey || !isLoggedIn) { + onOpenTemplate(template); + } else { + onSelectTemplate(template); + } + }} + onKeyDown={evt => { + if (disabled) { + return; + } + + if (evt.key === 'Enter') { + evt.preventDefault(); + if (evt.metaKey || evt.ctrlKey) { + onOpenTemplate(template); + } else { + onSelectTemplate(template); + } + } + }} + disabled={disabled} + > + + + + {isOfficial && ( + Official + )} + {!isOfficial && isV2 && ( + + This is a cloud sandbox that runs in a microVM, learn more{' '} + + here + + . + + } + interactive + > +
+ Cloud +
+
+ )} +
+ + + {sandboxTitle} + + + + + by + {isOfficial ? 'CodeSandbox' : teamName} + + {forks ? ( + + + {formatNumber(forks)} + + ) : null} + + +
+
+ ); +}; diff --git a/packages/app/src/app/components/Create/TemplateList.tsx b/packages/app/src/app/components/Create/TemplateList.tsx new file mode 100644 index 00000000000..bb1b4c322e8 --- /dev/null +++ b/packages/app/src/app/components/Create/TemplateList.tsx @@ -0,0 +1,89 @@ +import React, { useEffect } from 'react'; +import track from '@codesandbox/common/lib/utils/analytics'; +import { Button, Text, Stack } from '@codesandbox/components'; +import { css } from '@styled-system/css'; +import { useAppState, useActions } from 'app/overmind'; +import { TemplateFragment } from 'app/graphql/types'; +import { TemplateCard } from './TemplateCard'; +import { TemplateGrid } from './elements'; + +interface TemplateListProps { + title: string; + isCloudTemplateList?: boolean; + templates: TemplateFragment[]; + onSelectTemplate: (template: TemplateFragment) => void; + onOpenTemplate: (template: TemplateFragment) => void; +} + +export const TemplateList = ({ + title, + isCloudTemplateList, + templates, + onSelectTemplate, + onOpenTemplate, +}: TemplateListProps) => { + const { hasLogIn } = useAppState(); + const actions = useActions(); + + useEffect(() => { + track('Create Sandbox Tab Open', { tab: title }); + }, [title]); + + return ( + + + + {title} + + + + {!hasLogIn && isCloudTemplateList ? ( + + + You need to be signed in to fork a cloud template. + + + + ) : null} + + {templates.length > 0 ? ( + templates.map(template => ( + + )) + ) : ( +
No templates for this category.
+ )} +
+
+ ); +}; diff --git a/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts b/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts new file mode 100644 index 00000000000..ad504215541 --- /dev/null +++ b/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react'; +import { TemplateInfo } from '../utils/types'; +import { getTemplateInfosFromAPI } from '../utils/api'; + +type EssentialsState = + | { + state: 'loading'; + } + | { + state: 'success'; + essentials: TemplateInfo[]; + } + | { + state: 'error'; + error: string; + }; + +export const useEssentialTemplates = () => { + const [essentialState, setEssentialState] = useState({ + state: 'loading', + }); + + useEffect(() => { + async function getEssentials() { + try { + const result = await getTemplateInfosFromAPI( + '/api/v1/sandboxes/templates/explore' + ); + + setEssentialState({ + state: 'success', + essentials: result, + }); + } catch { + setEssentialState({ + state: 'error', + error: 'Something went wrong when fetching more templates, sorry!', + }); + } + } + + if (essentialState.state === 'loading') { + getEssentials(); + } + }, [essentialState.state]); + + return essentialState; +}; diff --git a/packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts b/packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts new file mode 100644 index 00000000000..83d3e7760d8 --- /dev/null +++ b/packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts @@ -0,0 +1,48 @@ +import { TemplateFragment } from 'app/graphql/types'; +import { useEffect, useState } from 'react'; +import { getTemplateInfosFromAPI } from '../utils/api'; + +type State = + | { + state: 'loading'; + } + | { + state: 'ready'; + templates: TemplateFragment[]; + } + | { + state: 'error'; + error: string; + }; + +export const useOfficialTemplates = (): State => { + const [officialTemplates, setOfficialTemplates] = useState({ + state: 'loading', + }); + + useEffect(() => { + async function fetchTemplates() { + try { + const response = await getTemplateInfosFromAPI( + '/api/v1/sandboxes/templates/official' + ); + + setOfficialTemplates({ + state: 'ready', + templates: response[0].templates, + }); + } catch { + setOfficialTemplates({ + state: 'error', + error: 'Something went wrong when fetching more templates, sorry!', + }); + } + } + + if (officialTemplates.state === 'loading') { + fetchTemplates(); + } + }, [officialTemplates.state]); + + return officialTemplates; +}; diff --git a/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts b/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts new file mode 100644 index 00000000000..a847a6e511b --- /dev/null +++ b/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts @@ -0,0 +1,78 @@ +import { useQuery } from '@apollo/react-hooks'; +import { + ListPersonalTemplatesQuery, + ListPersonalTemplatesQueryVariables, + TemplateFragment, +} from 'app/graphql/types'; +import { LIST_PERSONAL_TEMPLATES } from '../utils/queries'; + +type State = + | { state: 'loading' } + | { + state: 'ready'; + recentTemplates: TemplateFragment[]; + teamTemplates: TemplateFragment[]; + } + | { + state: 'error'; + error: string; + }; + +function getUserTemplates(data: ListPersonalTemplatesQuery) { + return data.me.templates; +} + +function getTeamTemplates(data: ListPersonalTemplatesQuery, teamId: string) { + return data.me.teams.find(team => team.id === teamId)?.templates || []; +} + +type UseTeamTemplatesParams = { + isUser: boolean; + teamId?: string; + hasLogIn: boolean; +}; + +export const useTeamTemplates = ({ + isUser, + teamId, + hasLogIn, +}: UseTeamTemplatesParams): State => { + const { data, error } = useQuery< + ListPersonalTemplatesQuery, + ListPersonalTemplatesQueryVariables + >(LIST_PERSONAL_TEMPLATES, { + /** + * With LIST_PERSONAL_TEMPLATES we're also fetching team templates. We're reusing + * this query here because it has already been preloaded and cached by overmind. We're + * filtering what we need later. + */ + variables: {}, + fetchPolicy: 'cache-and-network', + skip: !hasLogIn, + }); + + if (error) { + return { + state: 'error', + error: error.message, + }; + } + + // Instead of checking the loading var we check this. Apollo sets the loading + // var to true even if we still have cached data that we can use. We also need to + // check if `data.me` isnt undefined before getting templates. + if (typeof data?.me === 'undefined') { + return { + state: 'loading', + }; + } + + const teamTemplates = + isUser || !teamId ? getUserTemplates(data) : getTeamTemplates(data, teamId); + + return { + state: 'ready', + recentTemplates: data.me.recentlyUsedTemplates, + teamTemplates, + }; +}; diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx index 295320fb706..6154cf50bcc 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx @@ -57,7 +57,7 @@ export const RecentHeader: React.FC<{ title: string }> = ({ title }) => { codesandbox: 'V1', event_source: 'UI', }); - actions.openCreateSandboxModal(); + actions.modalOpened({ modal: 'createDevbox' }); }} variant="primary" /> diff --git a/packages/app/src/app/pages/common/Modals/index.tsx b/packages/app/src/app/pages/common/Modals/index.tsx index b7912223ce6..25374c6f1e0 100644 --- a/packages/app/src/app/pages/common/Modals/index.tsx +++ b/packages/app/src/app/pages/common/Modals/index.tsx @@ -11,6 +11,7 @@ import getVSCodeTheme from 'app/src/app/pages/Sandbox/Editor/utils/get-vscode-th import React, { FunctionComponent, useEffect, useState } from 'react'; import { ImportRepository } from 'app/components/Create/ImportRepository'; +import { CreateBox } from 'app/components/Create/CreateBox'; import { GenericCreate } from 'app/components/Create/GenericCreate'; import { AddPreset } from './AddPreset'; import { DeleteDeploymentModal } from './DeleteDeploymentModal'; @@ -67,6 +68,10 @@ const modals = { Component: CreateSandbox, width: () => (window.outerWidth > COLUMN_MEDIA_THRESHOLD ? 1200 : 950), }, + createDevbox: { + Component: CreateBox, + width: 950, + }, genericCreate: { Component: GenericCreate, width: 950, From ad929e8d546f0eae1899132535eba739bc19d7ff Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Wed, 15 Nov 2023 11:13:06 +0000 Subject: [PATCH 05/45] feat: Update CreateBox component Update CreateBox component by removing unused imports, refactoring panel component, and updating template rendering logic. --- .../src/app/components/Create/CreateBox.tsx | 160 +++++++++--------- .../app/components/Create/GenericCreate.tsx | 2 +- .../app/components/Create/TemplateList.tsx | 2 +- .../Create/hooks/useEssentialTemplates.ts | 6 +- .../Create/hooks/useTeamTemplates.ts | 11 +- .../Content/routes/Recent/RecentHeader.tsx | 2 +- .../app/src/app/pages/Dashboard/index.tsx | 4 +- .../app/src/app/pages/common/Modals/index.tsx | 11 ++ 8 files changed, 99 insertions(+), 99 deletions(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 72240524b99..77eb2ab546f 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -3,12 +3,11 @@ import { Stack, Element, IconButton, - SkeletonText, ThemeProvider, } from '@codesandbox/components'; import { useActions, useAppState } from 'app/overmind'; -import React, { ReactNode, useState, useEffect } from 'react'; -import { TabStateReturn, useTabState } from 'reakit/Tab'; +import React, { useState, useEffect } from 'react'; +import { useTabState } from 'reakit/Tab'; import slugify from '@codesandbox/common/lib/utils/slugify'; import { getTemplateIcon } from '@codesandbox/common/lib/utils/getTemplateIcon'; import { TemplateFragment } from 'app/graphql/types'; @@ -19,8 +18,8 @@ import { useGlobalPersistedState } from 'app/hooks/usePersistedState'; import { Container, Tab, - TabContent, Tabs, + Panel, HeaderInformation, ModalContent, ModalSidebar, @@ -47,26 +46,6 @@ const FEATURED_IDS = [ 'rjk9n4zj7m', // static v1 ]; -interface PanelProps { - tab: TabStateReturn; - id: string; - children: ReactNode; -} - -/** - * The Panel component handles the conditional rendering of the actual panel content. This is - * done with render props as per the Reakit docs. - */ -const Panel = ({ tab, id, children }: PanelProps) => { - return ( - - {({ hidden, ...rest }) => - hidden ? null :
{children}
- } -
- ); -}; - interface CreateBoxProps { collectionId?: string; isModal?: boolean; @@ -78,47 +57,46 @@ export const CreateBox: React.FC = ({ type = 'devbox', isModal, }) => { - const { hasLogIn, activeTeamInfo, user } = useAppState(); + const { hasLogIn, activeTeam } = useAppState(); const actions = useActions(); - const isUser = user?.username === activeTeamInfo?.name; const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); const mobileScreenSize = mediaQuery.matches; - const essentialState = useEssentialTemplates(); + const showFeaturedTemplates = type === 'devbox'; + const showEssentialTemplates = type === 'devbox'; + + const noDevboxesWhenListingSandboxes = (t: TemplateFragment) => + type === 'sandbox' ? !t.sandbox.isV2 : true; + + const essentialState = useEssentialTemplates(showEssentialTemplates); const officialTemplatesData = useOfficialTemplates(); const teamTemplatesData = useTeamTemplates({ - isUser, - teamId: activeTeamInfo?.id, + teamId: activeTeam, hasLogIn, }); const officialTemplates = officialTemplatesData.state === 'ready' - ? officialTemplatesData.templates + ? officialTemplatesData.templates.filter(noDevboxesWhenListingSandboxes) : []; /** - * Checking for user because user is undefined when landing on /s/, even though - * hasLogIn is true. Only show the team/my templates if the list is populated. + * Only show the team templates if the list is populated. */ const showTeamTemplates = hasLogIn && - user && + activeTeam && teamTemplatesData.state === 'ready' && teamTemplatesData.teamTemplates.length > 0; - /** - * For the quick start we show: - * - 3 most recently used templates (if they exist) - * - 6 to 9 templates selected from the official list, ensuring the total number - * of unique templates is 9 (recent + official) - */ const recentlyUsedTemplates = teamTemplatesData.state === 'ready' ? teamTemplatesData.recentTemplates.slice(0, 3) : []; + const hasRecentlyUsedTemplates = recentlyUsedTemplates.length > 0; + const featuredOfficialTemplates = officialTemplatesData.state === 'ready' ? FEATURED_IDS.map( @@ -131,15 +109,19 @@ export const CreateBox: React.FC = ({ const featuredTemplates = featuredOfficialTemplates.slice( 0, - featuredOfficialTemplates.length > 0 ? 6 : 9 + hasRecentlyUsedTemplates ? 6 : 9 ); const teamTemplates = - teamTemplatesData.state === 'ready' ? teamTemplatesData.teamTemplates : []; + teamTemplatesData.state === 'ready' + ? teamTemplatesData.teamTemplates.filter(noDevboxesWhenListingSandboxes) + : []; + + const allTemplates = [...officialTemplates, ...teamTemplates]; const tabState = useTabState({ orientation: mobileScreenSize ? 'horizontal' : 'vertical', - selectedId: 'featured', + selectedId: type === 'devbox' ? 'featured' : 'all', }); const [viewState, setViewState] = useState<'initial' | 'fromTemplate'>( @@ -285,35 +267,47 @@ export const CreateBox: React.FC = ({ {viewState === 'initial' ? ( + {showFeaturedTemplates && ( + trackTabClick('featured')} + stopId="featured" + > + Featured templates + + )} + trackTabClick('featured')} - stopId="featured" + onClick={() => trackTabClick('all')} + stopId="all" > - Featured templates + All templates - + {showTeamTemplates ? ( trackTabClick('team-templates')} - stopId="team-templates" + onClick={() => trackTabClick('workspace')} + stopId="workspace" > - {`${isUser ? 'My' : 'Team'} templates`} + Workspace templates ) : null} trackTabClick('official-templates')} - stopId="official-templates" + onClick={() => trackTabClick('official')} + stopId="official" > Official templates - {essentialState.state === 'success' + + + {showEssentialTemplates && essentialState.state === 'success' ? essentialState.essentials.map(essential => ( = ({ )) : null} - - {!mobileScreenSize && essentialState.state === 'loading' ? ( - - - - - - - - - ) : null} - - {essentialState.state === 'error' ? ( -
{essentialState.error}
- ) : null}
) : null} @@ -353,54 +332,67 @@ export const CreateBox: React.FC = ({ {viewState === 'initial' && ( + {hasRecentlyUsedTemplates && ( + { + selectTemplate(template, 'featured'); + }} + onOpenTemplate={template => { + openTemplate(template, 'featured'); + }} + /> + )} { - selectTemplate(template, 'Featured templates'); + selectTemplate(template, 'featured'); }} onOpenTemplate={template => { - openTemplate(template, 'Featured templates'); + openTemplate(template, 'featured'); }} /> + + + { - selectTemplate(template, 'Featured templates'); + selectTemplate(template, 'all'); }} onOpenTemplate={template => { - openTemplate(template, 'Featured templates'); + openTemplate(template, 'all'); }} /> {showTeamTemplates ? ( - + { - selectTemplate(template, 'team-templates'); + selectTemplate(template, 'workspace'); }} onOpenTemplate={template => { - openTemplate(template, 'team-templates'); + openTemplate(template, 'workspace'); }} /> ) : null} - + { - selectTemplate(template, 'official-templates'); + selectTemplate(template, 'official'); }} onOpenTemplate={template => { - openTemplate(template, 'official-templates'); + openTemplate(template, 'official'); }} /> diff --git a/packages/app/src/app/components/Create/GenericCreate.tsx b/packages/app/src/app/components/Create/GenericCreate.tsx index 2d9d4f847fd..4fcc8bfee08 100644 --- a/packages/app/src/app/components/Create/GenericCreate.tsx +++ b/packages/app/src/app/components/Create/GenericCreate.tsx @@ -94,7 +94,7 @@ export const GenericCreate: React.FC = ({ event_source: 'UI', }); closeModal(); - actions.openCreateSandboxModal(); + actions.modalOpened({ modal: 'createSandbox' }); }} variant="secondary" alignment="vertical" diff --git a/packages/app/src/app/components/Create/TemplateList.tsx b/packages/app/src/app/components/Create/TemplateList.tsx index bb1b4c322e8..755f8c76829 100644 --- a/packages/app/src/app/components/Create/TemplateList.tsx +++ b/packages/app/src/app/components/Create/TemplateList.tsx @@ -81,7 +81,7 @@ export const TemplateList = ({ /> )) ) : ( -
No templates for this category.
+ No templates for this category. )}
diff --git a/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts b/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts index ad504215541..0ab89b36c01 100644 --- a/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts +++ b/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts @@ -15,12 +15,16 @@ type EssentialsState = error: string; }; -export const useEssentialTemplates = () => { +export const useEssentialTemplates = (showEssentialTemplates: boolean) => { const [essentialState, setEssentialState] = useState({ state: 'loading', }); useEffect(() => { + if (!showEssentialTemplates) { + return; + } + async function getEssentials() { try { const result = await getTemplateInfosFromAPI( diff --git a/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts b/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts index a847a6e511b..067c4ec69f4 100644 --- a/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts +++ b/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts @@ -18,22 +18,16 @@ type State = error: string; }; -function getUserTemplates(data: ListPersonalTemplatesQuery) { - return data.me.templates; -} - function getTeamTemplates(data: ListPersonalTemplatesQuery, teamId: string) { return data.me.teams.find(team => team.id === teamId)?.templates || []; } type UseTeamTemplatesParams = { - isUser: boolean; - teamId?: string; + teamId: string; hasLogIn: boolean; }; export const useTeamTemplates = ({ - isUser, teamId, hasLogIn, }: UseTeamTemplatesParams): State => { @@ -67,8 +61,7 @@ export const useTeamTemplates = ({ }; } - const teamTemplates = - isUser || !teamId ? getUserTemplates(data) : getTeamTemplates(data, teamId); + const teamTemplates = getTeamTemplates(data, teamId); return { state: 'ready', diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx index 6154cf50bcc..4d74dedc188 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx @@ -71,7 +71,7 @@ export const RecentHeader: React.FC<{ title: string }> = ({ title }) => { codesandbox: 'V1', event_source: 'UI', }); - actions.openCreateSandboxModal(); + actions.modalOpened({ modal: 'createSandbox' }); }} variant="secondary" /> diff --git a/packages/app/src/app/pages/Dashboard/index.tsx b/packages/app/src/app/pages/Dashboard/index.tsx index feade2df06d..a8929dace45 100644 --- a/packages/app/src/app/pages/Dashboard/index.tsx +++ b/packages/app/src/app/pages/Dashboard/index.tsx @@ -117,9 +117,9 @@ export const Dashboard: FunctionComponent = () => { if (JSON.parse(searchParams.get('import_repo'))) { actions.modalOpened({ modal: 'importRepository' }); } else if (JSON.parse(searchParams.get('create_sandbox'))) { - actions.openCreateSandboxModal(); // will change + actions.modalOpened({ modal: 'createSandbox' }); } else if (JSON.parse(searchParams.get('create_devbox'))) { - actions.openCreateSandboxModal(); // will change + actions.modalOpened({ modal: 'createDevbox' }); } else if (searchParams.get('preferences')) { const toToOpen = searchParams.get('preferences'); actions.preferences.openPreferencesModal(toToOpen); diff --git a/packages/app/src/app/pages/common/Modals/index.tsx b/packages/app/src/app/pages/common/Modals/index.tsx index 25374c6f1e0..0d152178f0d 100644 --- a/packages/app/src/app/pages/common/Modals/index.tsx +++ b/packages/app/src/app/pages/common/Modals/index.tsx @@ -71,6 +71,16 @@ const modals = { createDevbox: { Component: CreateBox, width: 950, + props: { + type: 'devbox', + }, + }, + createSandbox: { + Component: CreateBox, + width: 950, + props: { + type: 'sandbox', + }, }, genericCreate: { Component: GenericCreate, @@ -293,6 +303,7 @@ const Modals: FunctionComponent = () => { > {modal ? React.createElement(modal.Component, { + ...(modal.props || {}), closeModal: () => modalClosed(), isModal: true, }) From f2e2e3d31eec41c992cab995fb1e16c4d964778e Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Wed, 15 Nov 2023 13:27:57 +0000 Subject: [PATCH 06/45] implement search --- .../src/app/components/Create/CreateBox.tsx | 70 ++++++++++++++----- .../SearchBox/SearchBox.tsx | 0 .../SearchBox/elements.ts | 5 +- .../SearchBox/index.ts | 0 .../app/components/Create/TemplateCard.tsx | 42 +---------- .../app/components/Create/TemplateList.tsx | 45 +++++++++--- .../CreateSandbox/CreateSandbox.tsx | 2 +- 7 files changed, 93 insertions(+), 71 deletions(-) rename packages/app/src/app/components/{CreateSandbox => Create}/SearchBox/SearchBox.tsx (100%) rename packages/app/src/app/components/{CreateSandbox => Create}/SearchBox/elements.ts (97%) rename packages/app/src/app/components/{CreateSandbox => Create}/SearchBox/index.ts (100%) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 77eb2ab546f..864112cce58 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -15,6 +15,8 @@ import track from '@codesandbox/common/lib/utils/analytics'; import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator'; import { useGlobalPersistedState } from 'app/hooks/usePersistedState'; +import { pluralize } from 'app/utils/pluralize'; +import { ModalContentProps } from 'app/pages/common/Modals'; import { Container, Tab, @@ -30,6 +32,7 @@ import { useEssentialTemplates } from './hooks/useEssentialTemplates'; import { useOfficialTemplates } from './hooks/useOfficialTemplates'; import { useTeamTemplates } from './hooks/useTeamTemplates'; import { CreateSandboxParams } from './utils/types'; +import { SearchBox } from './SearchBox'; export const COLUMN_MEDIA_THRESHOLD = 1600; @@ -46,15 +49,15 @@ const FEATURED_IDS = [ 'rjk9n4zj7m', // static v1 ]; -interface CreateBoxProps { +type CreateBoxProps = ModalContentProps & { collectionId?: string; - isModal?: boolean; type?: 'devbox' | 'sandbox'; -} +}; export const CreateBox: React.FC = ({ collectionId, type = 'devbox', + closeModal, isModal, }) => { const { hasLogIn, activeTeam } = useAppState(); @@ -66,6 +69,17 @@ export const CreateBox: React.FC = ({ const showFeaturedTemplates = type === 'devbox'; const showEssentialTemplates = type === 'devbox'; + const tabState = useTabState({ + orientation: mobileScreenSize ? 'horizontal' : 'vertical', + selectedId: type === 'devbox' ? 'featured' : 'all', + }); + + const [viewState, setViewState] = useState<'initial' | 'fromTemplate'>( + 'initial' + ); + const [selectedTemplate] = useState(); + const [searchQuery, setSearchQuery] = useState(''); + const noDevboxesWhenListingSandboxes = (t: TemplateFragment) => type === 'sandbox' ? !t.sandbox.isV2 : true; @@ -117,18 +131,15 @@ export const CreateBox: React.FC = ({ ? teamTemplatesData.teamTemplates.filter(noDevboxesWhenListingSandboxes) : []; - const allTemplates = [...officialTemplates, ...teamTemplates]; - - const tabState = useTabState({ - orientation: mobileScreenSize ? 'horizontal' : 'vertical', - selectedId: type === 'devbox' ? 'featured' : 'all', - }); - - const [viewState, setViewState] = useState<'initial' | 'fromTemplate'>( - 'initial' - ); - - const [searchQuery, setSearchQuery] = useState(''); + const allTemplates = [...officialTemplates, ...teamTemplates] + .filter(noDevboxesWhenListingSandboxes) + .filter(t => + searchQuery + ? (t.sandbox.alias || t.sandbox.alias || '') + .toLowerCase() + .includes(searchQuery.toLowerCase()) + : true + ); useEffect(() => { if (searchQuery) { @@ -141,10 +152,10 @@ export const CreateBox: React.FC = ({ }, [searchQuery]); useEffect(() => { - if (searchQuery && tabState.selectedId) { + if (searchQuery && tabState.selectedId !== 'all') { setSearchQuery(''); } - }, [tabState.selectedId]); + }, [searchQuery, tabState.selectedId]); const [hasBetaEditorExperiment] = useGlobalPersistedState( 'BETA_SANDBOX_EDITOR', @@ -257,7 +268,7 @@ export const CreateBox: React.FC = ({ variant="square" size={16} title="Close modal" - onClick={() => actions.modals.newSandboxModal.close()} + onClick={() => closeModal()} /> ) : null} @@ -266,6 +277,17 @@ export const CreateBox: React.FC = ({ {viewState === 'initial' ? ( + { + const query = e.target.value; + tabState.select('all'); + setSearchQuery(query); + }} + /> + + + {showFeaturedTemplates && ( = ({ { selectTemplate(template, 'all'); }} diff --git a/packages/app/src/app/components/CreateSandbox/SearchBox/SearchBox.tsx b/packages/app/src/app/components/Create/SearchBox/SearchBox.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/SearchBox/SearchBox.tsx rename to packages/app/src/app/components/Create/SearchBox/SearchBox.tsx diff --git a/packages/app/src/app/components/CreateSandbox/SearchBox/elements.ts b/packages/app/src/app/components/Create/SearchBox/elements.ts similarity index 97% rename from packages/app/src/app/components/CreateSandbox/SearchBox/elements.ts rename to packages/app/src/app/components/Create/SearchBox/elements.ts index 063fb5ecf4d..fefd858a42f 100644 --- a/packages/app/src/app/components/CreateSandbox/SearchBox/elements.ts +++ b/packages/app/src/app/components/Create/SearchBox/elements.ts @@ -15,9 +15,8 @@ export const SearchElement = styled.input` border-radius: 2px; padding-top: 4px; padding-bottom: 4px; - padding-left: 30px; - padding-right: 30px; - width: 176px; + padding-left: 25px; + padding-right: 25px; font-style: normal; font-weight: 400; font-size: 12px; diff --git a/packages/app/src/app/components/CreateSandbox/SearchBox/index.ts b/packages/app/src/app/components/Create/SearchBox/index.ts similarity index 100% rename from packages/app/src/app/components/CreateSandbox/SearchBox/index.ts rename to packages/app/src/app/components/Create/SearchBox/index.ts diff --git a/packages/app/src/app/components/Create/TemplateCard.tsx b/packages/app/src/app/components/Create/TemplateCard.tsx index d8c5293709f..24124002045 100644 --- a/packages/app/src/app/components/Create/TemplateCard.tsx +++ b/packages/app/src/app/components/Create/TemplateCard.tsx @@ -1,14 +1,6 @@ import React from 'react'; -import { - Badge, - formatNumber, - Icon, - Stack, - Text, -} from '@codesandbox/components'; +import { formatNumber, Icon, Stack, Text } from '@codesandbox/components'; import { getTemplateIcon } from '@codesandbox/common/lib/utils/getTemplateIcon'; -import { docsUrl } from '@codesandbox/common/lib/utils/url-generator'; -import Tooltip from '@codesandbox/common/lib/components/Tooltip'; import { TemplateFragment } from 'app/graphql/types'; import { VisuallyHidden } from 'reakit/VisuallyHidden'; import { useAppState } from 'app/overmind'; @@ -21,7 +13,6 @@ interface TemplateCardProps { onOpenTemplate: (template: TemplateFragment) => void; padding?: number | string; forks?: number; - isOfficial?: boolean; } export const TemplateCard = ({ @@ -31,7 +22,6 @@ export const TemplateCard = ({ onOpenTemplate, padding, forks, - isOfficial, }: TemplateCardProps) => { const { isLoggedIn } = useAppState(); const { UserIcon } = getTemplateIcon( @@ -41,12 +31,11 @@ export const TemplateCard = ({ ); const sandboxTitle = template.sandbox?.title || template.sandbox?.alias; - const isV2 = template.sandbox?.isV2; const teamName = template.sandbox?.team?.name || template.sandbox?.author?.username || - 'GitHub'; + 'CodeSandbox'; return ( - {isOfficial && ( - Official - )} - {!isOfficial && isV2 && ( - - This is a cloud sandbox that runs in a microVM, learn more{' '} - - here - - . - - } - interactive - > -
- Cloud -
-
- )}
by - {isOfficial ? 'CodeSandbox' : teamName} + {teamName} {forks ? ( diff --git a/packages/app/src/app/components/Create/TemplateList.tsx b/packages/app/src/app/components/Create/TemplateList.tsx index 755f8c76829..dc6d3658d9a 100644 --- a/packages/app/src/app/components/Create/TemplateList.tsx +++ b/packages/app/src/app/components/Create/TemplateList.tsx @@ -10,6 +10,8 @@ import { TemplateGrid } from './elements'; interface TemplateListProps { title: string; isCloudTemplateList?: boolean; + showEmptyState?: boolean; + searchQuery?: string; templates: TemplateFragment[]; onSelectTemplate: (template: TemplateFragment) => void; onOpenTemplate: (template: TemplateFragment) => void; @@ -21,6 +23,8 @@ export const TemplateList = ({ templates, onSelectTemplate, onOpenTemplate, + showEmptyState = false, + searchQuery, }: TemplateListProps) => { const { hasLogIn } = useAppState(); const actions = useActions(); @@ -49,7 +53,7 @@ export const TemplateList = ({ margin: 0, }} > - {title} + {showEmptyState && templates.length === 0 ? 'No results' : title} @@ -70,20 +74,43 @@ export const TemplateList = ({ ) : null} - - {templates.length > 0 ? ( - templates.map(template => ( + + {templates.length > 0 && ( + + {templates.map(template => ( - )) - ) : ( - No templates for this category. - )} - + ))} + + )} + + {showEmptyState && searchQuery && templates.length === 0 && ( + + + Not finding what you need? + + + Browse more than 3 million community-made templates{' '} + + on our Discover + {' '} + page. + + + )} ); }; diff --git a/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx b/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx index c1a99b34696..daa4deba81c 100644 --- a/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx +++ b/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx @@ -33,7 +33,7 @@ import { FromTemplate } from './FromTemplate'; import { useOfficialTemplates } from './useOfficialTemplates'; import { useTeamTemplates } from './useTeamTemplates'; import { CreateSandboxParams } from './types'; -import { SearchBox } from './SearchBox'; +import { SearchBox } from '../Create/SearchBox'; import { SearchResults } from './SearchResults'; export const COLUMN_MEDIA_THRESHOLD = 1600; From 0505d167599d038733350cd5155b13a3c6ca91ad Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Wed, 15 Nov 2023 14:10:15 +0000 Subject: [PATCH 07/45] feat: Update CreateBox, GenericCreate, ImportRepository, and Editor Update CreateBox, GenericCreate, ImportRepository, and Editor components with new code --- .../src/app/components/Create/CreateBox.tsx | 2 +- .../app/components/Create/GenericCreate.tsx | 25 ++++++----- .../components/Create/ImportRepository.tsx | 4 +- .../src/app/pages/Sandbox/Editor/index.tsx | 43 ++++++++----------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 864112cce58..2a8c6874eb4 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -178,7 +178,7 @@ export const CreateBox: React.FC = ({ }, }); - actions.modals.newSandboxModal.close(); + closeModal(); }; const selectTemplate = ( diff --git a/packages/app/src/app/components/Create/GenericCreate.tsx b/packages/app/src/app/components/Create/GenericCreate.tsx index 4fcc8bfee08..1308a126e91 100644 --- a/packages/app/src/app/components/Create/GenericCreate.tsx +++ b/packages/app/src/app/components/Create/GenericCreate.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { ModalContentProps } from 'app/pages/common/Modals'; import { Stack, Text, IconButton, Element } from '@codesandbox/components'; import track from '@codesandbox/common/lib/utils/analytics'; @@ -7,16 +6,16 @@ import { useActions } from 'app/overmind'; import { Container, HeaderInformation } from './elements'; import { LargeCTAButton } from '../dashboard/LargeCTAButton'; -export const GenericCreate: React.FC = ({ - closeModal, - isModal, -}) => { +export const GenericCreate: React.FC<{ + closeModal?: () => void; + isModal?: boolean; +}> = ({ closeModal, isModal }) => { const actions = useActions(); const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); const mobileScreenSize = mediaQuery.matches; return ( - + = ({ paddingX={6} css={{ display: 'grid', - gridTemplateColumns: 'repeat(3, 1fr)', + gridTemplateColumns: `repeat(${mobileScreenSize ? 1 : 3}, 1fr)`, gap: '16px', }} > @@ -61,7 +60,9 @@ export const GenericCreate: React.FC = ({ codesandbox: 'V1', event_source: 'UI', }); - closeModal(); + if (closeModal) { + closeModal(); + } actions.modalOpened({ modal: 'importRepository' }); }} variant="primary" @@ -77,7 +78,9 @@ export const GenericCreate: React.FC = ({ codesandbox: 'V1', event_source: 'UI', }); - closeModal(); + if (closeModal) { + closeModal(); + } actions.modalOpened({ modal: 'createDevbox' }); }} variant="primary" @@ -93,7 +96,9 @@ export const GenericCreate: React.FC = ({ codesandbox: 'V1', event_source: 'UI', }); - closeModal(); + if (closeModal) { + closeModal(); + } actions.modalOpened({ modal: 'createSandbox' }); }} variant="secondary" diff --git a/packages/app/src/app/components/Create/ImportRepository.tsx b/packages/app/src/app/components/Create/ImportRepository.tsx index 2f21366132e..b7a73d29693 100644 --- a/packages/app/src/app/components/Create/ImportRepository.tsx +++ b/packages/app/src/app/components/Create/ImportRepository.tsx @@ -68,7 +68,7 @@ export const ImportRepository: React.FC = ({ {viewState === 'initial' ? ( - New + Connect a repository ) : ( // TODO: add aria-label based on title to IconButton? @@ -107,7 +107,7 @@ export const ImportRepository: React.FC = ({ {viewState === 'initial' ? ( - + {showImportRepository && ( { width: '100vw', height: '100vh', position: 'fixed', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', })} - /> - - - - + - - - + '@media screen and (max-width: 950)': { + width: 'auto', + margin: 0, + }, + })} + > + From a2de02aa0d37503dd470fca2a39f7706a2a1e2b3 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Wed, 15 Nov 2023 15:05:48 +0000 Subject: [PATCH 08/45] feat: Updated Badge component Added 'boxDevbox' icon to Badge component and updated import path for TemplateCard --- .../Create/ImportRepository/FromRepo.tsx | 2 -- .../components/CreateSandbox/FromTemplate.tsx | 2 -- .../Dashboard/Components/Header/index.tsx | 24 +++++++------------ .../Components/Sandbox/SandboxCard.tsx | 2 +- .../Components/TemplatesRow/TemplatesRow.tsx | 2 +- .../Content/routes/MyContributions/index.tsx | 1 - 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/app/src/app/components/Create/ImportRepository/FromRepo.tsx b/packages/app/src/app/components/Create/ImportRepository/FromRepo.tsx index 219acf0144e..7324d7fd6d9 100644 --- a/packages/app/src/app/components/Create/ImportRepository/FromRepo.tsx +++ b/packages/app/src/app/components/Create/ImportRepository/FromRepo.tsx @@ -1,7 +1,6 @@ import { useActions, useAppState } from 'app/overmind'; import React, { useEffect } from 'react'; import { - Badge, Button, Element, Icon, @@ -122,7 +121,6 @@ export const FromRepo: React.FC = ({ repository, onCancel }) => { > Create new fork - Cloud = ({ > New from template - {isV2 && Cloud} {loading ? ( + ) : title ? ( + {title} ) : ( - <> - {title ? ( - {title} - ) : ( - - )} - {showBetaBadge && Cloud} - + )} 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 6944c5dcfd1..470b1ec1f71 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx @@ -165,7 +165,7 @@ const SandboxStats: React.FC = React.memo( } if (showBetaBadge) { - return Cloud; + return Devbox; } return null; diff --git a/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx b/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx index 1fa9a334efa..3f9e516c997 100644 --- a/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { CreateCard, SkeletonText, Stack } from '@codesandbox/components'; import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator'; import { useOfficialTemplates } from 'app/components/CreateSandbox/useOfficialTemplates'; -import { TemplateCard } from 'app/components/CreateSandbox/TemplateCard'; +import { TemplateCard } from 'app/components/Create/TemplateCard'; import { useActions, useAppState } from 'app/overmind'; import { TemplateFragment } from 'app/graphql/types'; import track from '@codesandbox/common/lib/utils/analytics'; diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/MyContributions/index.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/MyContributions/index.tsx index 316cfb70683..822be535dbb 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/MyContributions/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/MyContributions/index.tsx @@ -50,7 +50,6 @@ export const MyContributionsPage = () => { title="My contributions" path={param} showViewOptions={!isEmpty} - showBetaBadge showFilters={!isEmpty && Boolean(param)} showSortOptions={!isEmpty && Boolean(param)} /> From c7cc2fd868854db7073861e553a61cda7d37389f Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Thu, 16 Nov 2023 13:29:46 +0000 Subject: [PATCH 09/45] cleanup --- .../src/app/components/Create/CreateBox.tsx | 1 + .../FromTemplate.tsx | 0 .../app/components/Create/GenericCreate.tsx | 2 +- .../components/Create/ImportRepository.tsx | 2 +- .../CreateSandbox/CreateSandbox.tsx | 667 ------------------ .../CreateSandbox/CreateSandboxModal.tsx | 65 -- .../CreateSandbox/Loader/elements.ts | 11 - .../components/CreateSandbox/Loader/index.tsx | 58 -- .../SearchResults/SearchResultList.tsx | 94 --- .../SearchResults/SearchResults.tsx | 83 --- .../CreateSandbox/SearchResults/index.ts | 1 - .../components/CreateSandbox/TemplateCard.tsx | 149 ---- .../CreateSandbox/TemplateCategoryList.tsx | 95 --- .../app/components/CreateSandbox/elements.ts | 172 ----- .../src/app/components/CreateSandbox/index.ts | 1 - .../app/components/CreateSandbox/queries.ts | 184 ----- .../src/app/components/CreateSandbox/types.ts | 14 - .../CreateSandbox/useEssentialTemplates.ts | 48 -- .../CreateSandbox/useOfficialTemplates.ts | 48 -- .../CreateSandbox/useTeamTemplates.ts | 78 -- .../app/components/CreateSandbox/utils/api.ts | 110 --- .../app/hooks/useGitHubAccountRepositories.ts | 2 +- .../src/app/hooks/useGithubOrganizations.ts | 2 +- packages/app/src/app/overmind/actions.ts | 10 - .../app/src/app/overmind/effects/api/index.ts | 2 +- packages/app/src/app/overmind/modals.ts | 8 - .../Components/TemplatesRow/TemplatesRow.tsx | 22 +- .../Content/routes/Recent/EmptyRecent.tsx | 2 - .../Content/routes/Recent/RecentContent.tsx | 2 - .../Content/routes/Recent/RecentHeader.tsx | 2 +- .../Content/routes/Sandboxes/index.tsx | 23 +- .../app/src/app/pages/Standalone/index.tsx | 9 +- .../app/src/app/pages/common/Modals/index.tsx | 8 - packages/app/src/app/pages/index.tsx | 2 - 34 files changed, 34 insertions(+), 1943 deletions(-) rename packages/app/src/app/components/{CreateSandbox => Create}/FromTemplate.tsx (100%) delete mode 100644 packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/CreateSandboxModal.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/Loader/elements.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/Loader/index.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/SearchResults/SearchResultList.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/SearchResults/SearchResults.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/SearchResults/index.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/TemplateCard.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/TemplateCategoryList.tsx delete mode 100644 packages/app/src/app/components/CreateSandbox/elements.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/index.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/queries.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/types.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/useEssentialTemplates.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/useOfficialTemplates.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/useTeamTemplates.ts delete mode 100644 packages/app/src/app/components/CreateSandbox/utils/api.ts diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 2a8c6874eb4..c38808101f3 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -52,6 +52,7 @@ const FEATURED_IDS = [ type CreateBoxProps = ModalContentProps & { collectionId?: string; type?: 'devbox' | 'sandbox'; + isModal: boolean; }; export const CreateBox: React.FC = ({ diff --git a/packages/app/src/app/components/CreateSandbox/FromTemplate.tsx b/packages/app/src/app/components/Create/FromTemplate.tsx similarity index 100% rename from packages/app/src/app/components/CreateSandbox/FromTemplate.tsx rename to packages/app/src/app/components/Create/FromTemplate.tsx diff --git a/packages/app/src/app/components/Create/GenericCreate.tsx b/packages/app/src/app/components/Create/GenericCreate.tsx index 1308a126e91..85aac458a9d 100644 --- a/packages/app/src/app/components/Create/GenericCreate.tsx +++ b/packages/app/src/app/components/Create/GenericCreate.tsx @@ -53,7 +53,7 @@ export const GenericCreate: React.FC<{ > { track('Create Modal - Import Repository', { diff --git a/packages/app/src/app/components/Create/ImportRepository.tsx b/packages/app/src/app/components/Create/ImportRepository.tsx index b7a73d29693..2a06b779e38 100644 --- a/packages/app/src/app/components/Create/ImportRepository.tsx +++ b/packages/app/src/app/components/Create/ImportRepository.tsx @@ -68,7 +68,7 @@ export const ImportRepository: React.FC = ({ {viewState === 'initial' ? ( - Connect a repository + Import ) : ( // TODO: add aria-label based on title to IconButton? diff --git a/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx b/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx deleted file mode 100644 index daa4deba81c..00000000000 --- a/packages/app/src/app/components/CreateSandbox/CreateSandbox.tsx +++ /dev/null @@ -1,667 +0,0 @@ -import { - Text, - Stack, - Element, - IconButton, - SkeletonText, - ThemeProvider, -} from '@codesandbox/components'; -import { useActions, useAppState } from 'app/overmind'; -import React, { ReactNode, useState, useEffect } from 'react'; -import { TabStateReturn, useTabState } from 'reakit/Tab'; -import slugify from '@codesandbox/common/lib/utils/slugify'; -import { getTemplateIcon } from '@codesandbox/common/lib/utils/getTemplateIcon'; -import { TemplateFragment } from 'app/graphql/types'; -import track from '@codesandbox/common/lib/utils/analytics'; -import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator'; - -import { useCreateCheckout } from 'app/hooks'; -import { useGlobalPersistedState } from 'app/hooks/usePersistedState'; -import { - Container, - Tab, - TabContent, - Tabs, - HeaderInformation, - ModalContent, - ModalSidebar, - ModalBody, -} from './elements'; -import { TemplateCategoryList } from './TemplateCategoryList'; -import { useEssentialTemplates } from './useEssentialTemplates'; -import { FromTemplate } from './FromTemplate'; -import { useOfficialTemplates } from './useOfficialTemplates'; -import { useTeamTemplates } from './useTeamTemplates'; -import { CreateSandboxParams } from './types'; -import { SearchBox } from '../Create/SearchBox'; -import { SearchResults } from './SearchResults'; - -export const COLUMN_MEDIA_THRESHOLD = 1600; - -const QUICK_START_IDS = [ - 'new', - 'vanilla', - 'vue', - 'hsd8ke', // docker starter v2 - 'fxis37', // next v2 - '9qputt', // vite + react v2 - 'prp60l', // remix v2 - 'angular', - 'react-ts', - 'rjk9n4zj7m', // static v1 -]; - -interface PanelProps { - tab: TabStateReturn; - id: string; - children: ReactNode; -} - -/** - * The Panel component handles the conditional rendering of the actual panel content. This is - * done with render props as per the Reakit docs. - */ -const Panel = ({ tab, id, children }: PanelProps) => { - return ( - - {({ hidden, ...rest }) => - hidden ? null :
{children}
- } -
- ); -}; - -interface CreateSandboxProps { - collectionId?: string; - initialTab?: 'import'; - isModal?: boolean; -} - -export const CreateSandbox: React.FC = ({ - collectionId, - initialTab, - isModal, -}) => { - const { hasLogIn, activeTeamInfo, user, environment } = useAppState(); - const actions = useActions(); - - const isUser = user?.username === activeTeamInfo?.name; - const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); - const mobileScreenSize = mediaQuery.matches; - - const essentialState = useEssentialTemplates(); - const officialTemplatesData = useOfficialTemplates(); - const teamTemplatesData = useTeamTemplates({ - isUser, - teamId: activeTeamInfo?.id, - hasLogIn, - }); - - const officialTemplates = - officialTemplatesData.state === 'ready' - ? officialTemplatesData.templates - : []; - - /** - * Checking for user because user is undefined when landing on /s/, even though - * hasLogIn is true. Only show the team/my templates if the list is populated. - */ - const showTeamTemplates = - hasLogIn && - user && - teamTemplatesData.state === 'ready' && - teamTemplatesData.teamTemplates.length > 0; - - /** - * For the quick start we show: - * - 3 most recently used templates (if they exist) - * - 6 to 9 templates selected from the official list, ensuring the total number - * of unique templates is 9 (recent + official) - */ - const recentlyUsedTemplates = - teamTemplatesData.state === 'ready' - ? teamTemplatesData.recentTemplates.slice(0, 3) - : []; - - const quickStartOfficialTemplates = - officialTemplatesData.state === 'ready' - ? QUICK_START_IDS.map( - id => - // If the template is already in recently used, don't add it twice - !recentlyUsedTemplates.find(t => t.sandbox.id === id) && - officialTemplates.find(t => t.sandbox.id === id) - ).filter(Boolean) - : []; - - const quickStartTemplates = [ - ...recentlyUsedTemplates, - ...quickStartOfficialTemplates, - ].slice(0, 9); - - const teamTemplates = - teamTemplatesData.state === 'ready' ? teamTemplatesData.teamTemplates : []; - - const tabState = useTabState({ - orientation: mobileScreenSize ? 'horizontal' : 'vertical', - selectedId: 'quickstart', - }); - - const [viewState, setViewState] = useState<'initial' | 'fromTemplate'>( - 'initial' - ); - // ❗️ We could combine viewState with selectedTemplate - // and selectedRepo to limit the amount of states. - const [selectedTemplate] = useState(); - const [searchQuery, setSearchQuery] = useState(''); - - useEffect(() => { - if (searchQuery) { - track('Create New - Search Templates', { - query: searchQuery, - codesandbox: 'V1', - event_source: 'UI', - }); - } - }, [searchQuery]); - - useEffect(() => { - if (searchQuery && tabState.selectedId) { - setSearchQuery(''); - } - }, [tabState.selectedId]); - - const [, createCheckout, canCheckout] = useCreateCheckout(); - - const onCreateCheckout = () => { - createCheckout({ - trackingLocation: 'dashboard_upgrade_banner', - }); - }; - - const [hasBetaEditorExperiment] = useGlobalPersistedState( - 'BETA_SANDBOX_EDITOR', - false - ); - - const createFromTemplate = ( - template: TemplateFragment, - { name }: CreateSandboxParams - ) => { - const { sandbox } = template; - - actions.editor.forkExternalSandbox({ - sandboxId: sandbox.id, - openInNewWindow: false, - hasBetaEditorExperiment, - body: { - alias: name, - collectionId, - }, - }); - - actions.modals.newSandboxModal.close(); - }; - - const selectTemplate = (template: TemplateFragment) => { - createFromTemplate(template, {}); - - // Temporarily disable the second screen until we have more functionality on it - // setSelectedTemplate(template); - // setViewState('fromTemplate'); - }; - - const openTemplate = (template: TemplateFragment) => { - const { sandbox } = template; - const url = sandboxUrl(sandbox, hasBetaEditorExperiment); - window.open(url, '_blank'); - }; - - const showSearch = !environment.isOnPrem; - const showCloudTemplates = !environment.isOnPrem; - - return ( - - - - - {viewState === 'initial' ? ( - - New - - ) : ( - // TODO: add aria-label based on title to IconButton? - { - setViewState('initial'); - }} - /> - )} - - - {showSearch && viewState === 'initial' ? ( - { - const query = e.target.value; - if (query) { - // Reset tab panel when typing in the search query box - tabState.select(null); - } else { - // Restore the default tab when search query is removed - tabState.select('quickstart'); - } - - setSearchQuery(query); - }} - /> - ) : null} - - {/* isModal is undefined on /s/ page */} - {isModal ? ( - // TODO: IconButton doesn't have aria label or visuallyhidden text (reads floating label too late) - actions.modals.newSandboxModal.close()} - /> - ) : null} - - - - - {viewState === 'initial' ? ( - - - { - track('Create New - Click Tab', { - codesandbox: 'V1', - event_source: 'UI', - tab_name: 'Quick Start', - }); - }} - stopId="quickstart" - > - Quick start - - - - - {showTeamTemplates ? ( - { - track(`Create New - Click Tab`, { - codesandbox: 'V1', - event_source: 'UI', - tab_name: `${isUser ? 'My' : 'Team'} Templates`, - }); - }} - stopId="team-templates" - > - {`${isUser ? 'My' : 'Team'} templates`} - - ) : null} - - {showCloudTemplates && ( - { - track('Create New - Click Tab', { - codesandbox: 'V1', - event_source: 'UI', - tab_name: `Cloud templates`, - }); - }} - stopId="cloud-templates" - > - Cloud templates - - )} - - { - track('Create New - Click Tab', { - codesandbox: 'V1', - event_source: 'UI', - tab_name: `Official Templates`, - }); - }} - stopId="official-templates" - > - Official templates - - - {essentialState.state === 'success' - ? essentialState.essentials.map(essential => ( - { - track(`Create New - Click Tab`, { - codesandbox: 'V1', - event_source: 'UI', - tab_name: essential.title, - }); - }} - > - {essential.title} - - )) - : null} - - {!mobileScreenSize && essentialState.state === 'loading' ? ( - - - - - - - - - ) : null} - - {essentialState.state === 'error' ? ( -
{essentialState.error}
- ) : null} -
-
- ) : null} - - {viewState === 'fromTemplate' ? ( - - ) : null} -
- - - {viewState === 'initial' && - (searchQuery ? ( - - ) : ( - - - { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'fork', - tab_name: 'Quick Start', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - }); - - selectTemplate(template); - }} - onOpenTemplate={template => { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'open', - tab_name: 'Quick Start', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - }); - - openTemplate(template); - }} - /> - - - {showTeamTemplates ? ( - - { - track(`Create New - Select template`, { - codesandbox: 'V1', - event_source: 'UI', - type: 'fork', - tab_name: `${ - isUser ? 'My' : activeTeamInfo?.name || 'Team' - } templates`, - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - }); - - selectTemplate(template); - }} - onOpenTemplate={template => { - track(`Create New - Select template`, { - codesandbox: 'V1', - event_source: 'UI', - type: 'open', - tab_name: `${ - isUser ? 'My' : activeTeamInfo?.name || 'Team' - } templates`, - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - }); - - openTemplate(template); - }} - /> - - ) : null} - - - template.sandbox.isV2 - )} - onSelectTemplate={template => { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'fork', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - tab_name: 'Cloud templates', - }); - - selectTemplate(template); - }} - onOpenTemplate={template => { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'open', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - tab_name: 'Cloud templates', - }); - - openTemplate(template); - }} - isCloudTemplateList - /> - - - - { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'fork', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - tab_name: 'Official Templates', - }); - - selectTemplate(template); - }} - onOpenTemplate={template => { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'open', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - tab_name: 'Official Templates', - }); - - openTemplate(template); - }} - /> - - - {essentialState.state === 'success' - ? essentialState.essentials.map(essential => ( - - { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'fork', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - tab_name: essential.title, - }); - - selectTemplate(template); - }} - onOpenTemplate={template => { - track('Create New - Select template', { - codesandbox: 'V1', - event_source: 'UI', - type: 'open', - template_name: - template.sandbox.title || - template.sandbox.alias || - template.sandbox.id, - tab_name: essential.title, - }); - - openTemplate(template); - }} - /> - - )) - : null} - - ))} - - {viewState === 'fromTemplate' ? ( - { - setViewState('initial'); - }} - onSubmit={params => { - createFromTemplate(selectedTemplate, params); - }} - /> - ) : null} - -
-
-
- ); -}; - -interface TemplateInfoProps { - template: TemplateFragment; -} - -const TemplateInfo = ({ template }: TemplateInfoProps) => { - const { UserIcon } = getTemplateIcon( - template.sandbox.title, - template.iconUrl, - template.sandbox?.source?.template - ); - - return ( - - - - - {template.sandbox.title} - - - {template.sandbox?.team?.name} - - - - {template.sandbox.description} - - - ); -}; diff --git a/packages/app/src/app/components/CreateSandbox/CreateSandboxModal.tsx b/packages/app/src/app/components/CreateSandbox/CreateSandboxModal.tsx deleted file mode 100644 index c945437a913..00000000000 --- a/packages/app/src/app/components/CreateSandbox/CreateSandboxModal.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { ThemeProvider } from '@codesandbox/components'; -import Modal from 'app/components/Modal'; -import { useAppState, useActions } from 'app/overmind'; -import { DELETE_ME_COLLECTION } from 'app/overmind/namespaces/dashboard/types'; -import React from 'react'; -import { useLocation } from 'react-router-dom'; - -import { COLUMN_MEDIA_THRESHOLD, CreateSandbox } from './CreateSandbox'; - -/** - * If you have the dashboard open, in a collection path, we want to create new sandboxes - * in that folder. That's why we get that path from the url. - */ -function getImplicitCollectionIdFromFolder( - pathname: string, - collections: DELETE_ME_COLLECTION[] | null -): DELETE_ME_COLLECTION | null { - if (!collectionPathRegex.test(location.pathname)) { - return null; - } - - if (!collections) { - return null; - } - - const collectionPath = decodeURIComponent( - pathname.replace(collectionPathRegex, '') - ); - - const collection = collections.find(c => c.path === collectionPath); - - return collection; -} - -const collectionPathRegex = /^.*dashboard\/sandboxes/; - -export const CreateSandboxModal = () => { - const { modals, dashboard } = useAppState(); - const { modals: modalsActions } = useActions(); - const location = useLocation(); - - const implicitCollection = getImplicitCollectionIdFromFolder( - location.pathname, - dashboard.allCollections - ); - - return ( - - modalsActions.newSandboxModal.close()} - width={window.outerWidth > COLUMN_MEDIA_THRESHOLD ? 1200 : 950} - fullWidth={window.screen.availWidth < 800} - > - - - - ); -}; diff --git a/packages/app/src/app/components/CreateSandbox/Loader/elements.ts b/packages/app/src/app/components/CreateSandbox/Loader/elements.ts deleted file mode 100644 index ef287416a19..00000000000 --- a/packages/app/src/app/components/CreateSandbox/Loader/elements.ts +++ /dev/null @@ -1,11 +0,0 @@ -import styled from 'styled-components'; - -export const LoadingWrapper = styled.div` - overflow: hidden; - height: calc(100%); -`; - -export const Individual = styled.div` - margin: 1.5rem; - margin-bottom: 0; -`; diff --git a/packages/app/src/app/components/CreateSandbox/Loader/index.tsx b/packages/app/src/app/components/CreateSandbox/Loader/index.tsx deleted file mode 100644 index d697c4b54a9..00000000000 --- a/packages/app/src/app/components/CreateSandbox/Loader/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import ContentLoader from 'react-content-loader'; -import { LoadingWrapper, Individual } from './elements'; - -const Loading = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export const Loader = () => ( - - - - - - - - -); diff --git a/packages/app/src/app/components/CreateSandbox/SearchResults/SearchResultList.tsx b/packages/app/src/app/components/CreateSandbox/SearchResults/SearchResultList.tsx deleted file mode 100644 index 37b1ccfc2cf..00000000000 --- a/packages/app/src/app/components/CreateSandbox/SearchResults/SearchResultList.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useEffect } from 'react'; -import { connectInfiniteHits } from 'react-instantsearch-dom'; -import { InfiniteHitsProvided } from 'react-instantsearch-core'; -import { AlgoliaSandboxHit } from '@codesandbox/common/lib/types/algolia'; -import { isServer } from '@codesandbox/common/lib/templates/helpers/is-server'; -import type { TemplateType } from '@codesandbox/common/lib/templates'; -import { TemplateFragment } from 'app/graphql/types'; -import { TemplateGrid } from '../elements'; -import { TemplateCard } from '../TemplateCard'; - -type ResultsProps = InfiniteHitsProvided & { - disableTemplates?: boolean; - onSelectTemplate: (template: TemplateFragment) => void; - onOpenTemplate: (template: TemplateFragment) => void; - officialTemplates: TemplateFragment[]; -}; - -const Results = (props: ResultsProps) => { - const { disableTemplates, hits } = props; - const bottomDetectionEl = React.useRef(); - - useEffect(() => { - let observer: IntersectionObserver; - const onSentinelIntersection = (entries: IntersectionObserverEntry[]) => { - const { hasMore, refineNext } = props; - - entries.forEach(entry => { - if (entry.isIntersecting && hasMore) { - refineNext(); - } - }); - }; - - if (bottomDetectionEl.current) { - observer = new IntersectionObserver(onSentinelIntersection); - observer.observe(bottomDetectionEl.current); - } - - return () => { - if (observer) { - observer.disconnect(); - } - }; - }, [props]); - - const templates = hits.map(hit => ({ - id: hit.custom_template.id, - color: hit.custom_template.color, - iconUrl: hit.custom_template.icon_url, - published: hit.custom_template.published, - forks: hit.fork_count, - sandbox: { - id: hit.objectID, - alias: hit.alias, - title: hit.title, - description: hit.description, - insertedAt: new Date(hit.inserted_at).toString(), - updatedAt: new Date(hit.updated_at).toString(), - author: hit.author, - isV2: hit.custom_template.v2, - source: { - template: hit.template, - }, - team: hit.team, - isSse: isServer(hit.template as TemplateType), - }, - })); - - return ( - - {templates.map(template => ( - t.id === template.id)} - /> - ))} -
- - ); -}; - -export const SearchResultList = connectInfiniteHits< - ResultsProps, - AlgoliaSandboxHit ->(Results); diff --git a/packages/app/src/app/components/CreateSandbox/SearchResults/SearchResults.tsx b/packages/app/src/app/components/CreateSandbox/SearchResults/SearchResults.tsx deleted file mode 100644 index 17d94e505c6..00000000000 --- a/packages/app/src/app/components/CreateSandbox/SearchResults/SearchResults.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { - ALGOLIA_API_KEY, - ALGOLIA_APPLICATION_ID, - ALGOLIA_DEFAULT_INDEX, // eslint-disable-line -} from '@codesandbox/common/lib/utils/config'; -import { InstantSearch, Configure, Stats } from 'react-instantsearch/dom'; -import { connectStateResults } from 'react-instantsearch-dom'; -import { createGlobalStyle } from 'styled-components'; -import { Text, Stack } from '@codesandbox/components'; -import { TemplateFragment } from 'app/graphql/types'; -import { SearchResultList } from './SearchResultList'; -import { Loader } from '../Loader'; - -const LoadingIndicator = connectStateResults(({ isSearchStalled }) => - isSearchStalled ? : null -); - -const GlobalSearchStyles = createGlobalStyle` -.ais-InstantSearch__root { - display: flex; - flex-direction: column; - height: 100%; -} -`; - -export const SearchResults = ({ - onCreateCheckout, - isInCollection, - search, - onSelectTemplate, - onOpenTemplate, - officialTemplates, -}: { - onCreateCheckout: () => void; - isInCollection: boolean; - search: string; - onSelectTemplate: (template: TemplateFragment) => void; - onOpenTemplate: (template: TemplateFragment) => void; - officialTemplates: TemplateFragment[]; -}) => { - return ( - <> - - - - - - - `${nbHits.toLocaleString()} results found`, - }} - /> - - - - - - - - ); -}; diff --git a/packages/app/src/app/components/CreateSandbox/SearchResults/index.ts b/packages/app/src/app/components/CreateSandbox/SearchResults/index.ts deleted file mode 100644 index c185070d81a..00000000000 --- a/packages/app/src/app/components/CreateSandbox/SearchResults/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SearchResults } from './SearchResults'; diff --git a/packages/app/src/app/components/CreateSandbox/TemplateCard.tsx b/packages/app/src/app/components/CreateSandbox/TemplateCard.tsx deleted file mode 100644 index d8c5293709f..00000000000 --- a/packages/app/src/app/components/CreateSandbox/TemplateCard.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React from 'react'; -import { - Badge, - formatNumber, - Icon, - Stack, - Text, -} from '@codesandbox/components'; -import { getTemplateIcon } from '@codesandbox/common/lib/utils/getTemplateIcon'; -import { docsUrl } from '@codesandbox/common/lib/utils/url-generator'; -import Tooltip from '@codesandbox/common/lib/components/Tooltip'; -import { TemplateFragment } from 'app/graphql/types'; -import { VisuallyHidden } from 'reakit/VisuallyHidden'; -import { useAppState } from 'app/overmind'; -import { TemplateButton } from './elements'; - -interface TemplateCardProps { - disabled?: boolean; - template: TemplateFragment; - onSelectTemplate: (template: TemplateFragment) => void; - onOpenTemplate: (template: TemplateFragment) => void; - padding?: number | string; - forks?: number; - isOfficial?: boolean; -} - -export const TemplateCard = ({ - disabled, - template, - onSelectTemplate, - onOpenTemplate, - padding, - forks, - isOfficial, -}: TemplateCardProps) => { - const { isLoggedIn } = useAppState(); - const { UserIcon } = getTemplateIcon( - template.sandbox.title, - template.iconUrl, - template.sandbox?.source?.template - ); - - const sandboxTitle = template.sandbox?.title || template.sandbox?.alias; - const isV2 = template.sandbox?.isV2; - - const teamName = - template.sandbox?.team?.name || - template.sandbox?.author?.username || - 'GitHub'; - - return ( - { - if (disabled) { - return; - } - - if (evt.metaKey || evt.ctrlKey || !isLoggedIn) { - onOpenTemplate(template); - } else { - onSelectTemplate(template); - } - }} - onKeyDown={evt => { - if (disabled) { - return; - } - - if (evt.key === 'Enter') { - evt.preventDefault(); - if (evt.metaKey || evt.ctrlKey) { - onOpenTemplate(template); - } else { - onSelectTemplate(template); - } - } - }} - disabled={disabled} - > - - - - {isOfficial && ( - Official - )} - {!isOfficial && isV2 && ( - - This is a cloud sandbox that runs in a microVM, learn more{' '} - - here - - . -
- } - interactive - > -
- Cloud -
- - )} -
- - - {sandboxTitle} - - - - - by - {isOfficial ? 'CodeSandbox' : teamName} - - {forks ? ( - - - {formatNumber(forks)} - - ) : null} - - - - - ); -}; diff --git a/packages/app/src/app/components/CreateSandbox/TemplateCategoryList.tsx b/packages/app/src/app/components/CreateSandbox/TemplateCategoryList.tsx deleted file mode 100644 index 9fdcf629af5..00000000000 --- a/packages/app/src/app/components/CreateSandbox/TemplateCategoryList.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useEffect } from 'react'; -import track from '@codesandbox/common/lib/utils/analytics'; -import { Button, Text, Stack } from '@codesandbox/components'; -import { css } from '@styled-system/css'; -import { useAppState, useActions } from 'app/overmind'; -import { TemplateFragment } from 'app/graphql/types'; -import { TemplateCard } from './TemplateCard'; -import { TemplateGrid } from './elements'; - -interface TemplateCategoryListProps { - title: string; - onCreateCheckout: () => void; - isCloudTemplateList?: boolean; - isInCollection: boolean; - templates: TemplateFragment[]; - onSelectTemplate: (template: TemplateFragment) => void; - onOpenTemplate: (template: TemplateFragment) => void; - canCheckout: boolean; -} - -export const TemplateCategoryList = ({ - title, - onCreateCheckout, - isCloudTemplateList, - isInCollection, - templates, - onSelectTemplate, - onOpenTemplate, - canCheckout, -}: TemplateCategoryListProps) => { - const { hasLogIn } = useAppState(); - const actions = useActions(); - - useEffect(() => { - track('Create Sandbox Tab Open', { tab: title }); - }, [title]); - - return ( - - - - {title} - - - - {!hasLogIn && isCloudTemplateList ? ( - - - You need to be signed in to fork a cloud template. - - - - ) : null} - - {templates.length > 0 ? ( - templates.map(template => ( - - )) - ) : ( -
No templates for this category.
- )} -
-
- ); -}; diff --git a/packages/app/src/app/components/CreateSandbox/elements.ts b/packages/app/src/app/components/CreateSandbox/elements.ts deleted file mode 100644 index 75c6507d9cd..00000000000 --- a/packages/app/src/app/components/CreateSandbox/elements.ts +++ /dev/null @@ -1,172 +0,0 @@ -import styled from 'styled-components'; -import { Tab as BaseTab, TabList, TabPanel } from 'reakit/Tab'; -import { Select } from '@codesandbox/components'; - -export const Container = styled.div` - height: 530px; - overflow: hidden; - border-radius: 4px; - background-color: #151515; - color: #e5e5e5; - display: flex; - flex-direction: column; -`; - -export const HeaderInformation = styled.div` - flex-grow: 1; -`; - -export const ModalBody = styled.div` - display: flex; - flex: 1; - overflow: hidden; - - @media screen and (max-width: 950px) { - flex-direction: column; - gap: 16px; - } -`; - -export const ModalSidebar = styled.div` - width: 176px; - flex-shrink: 0; - padding: 0px 24px; - overflow: auto; - - @media screen and (max-width: 950px) { - width: auto; - padding: 8px 8px 0; - } -`; - -export const ModalContent = styled.div` - flex-grow: 1; - padding: 0 24px; - overflow: auto; - - @media screen and (max-width: 950px) { - padding: 0 16px; - } -`; - -export const Tabs = styled(TabList)` - display: flex; - flex-direction: column; - - @media screen and (max-width: 950px) { - flex-direction: row; - } -`; - -export const Tab = styled(BaseTab)` - text-align: left; - padding: 8px 0; - margin-bottom: 4px; - - border: none; - background: transparent; - color: #999999; - font-family: 'Inter', sans-serif; - font-size: 13px; - line-height: 16px; - cursor: pointer; - white-space: nowrap; - - &[aria-selected='true'], - :hover { - color: #e5e5e5; - } - - &:focus { - outline: none; - } - - @media screen and (max-width: 950px) { - margin-bottom: 0; - padding: 8px; - } -`; - -export const TabContent = styled(TabPanel)` - width: 100%; - height: 100%; - outline: none; -`; - -export const TemplateButton = styled.button` - box-sizing: border-box; - width: 100%; - height: 100%; - padding: 16px; - background: #1d1d1d; - border: 1px solid transparent; - text-align: left; - font-family: inherit; - border-radius: 2px; - color: #e5e5e5; - transition: background ${props => props.theme.speeds[2]} ease-out; - outline: none; - - &:hover:not(:disabled) { - background: #252525; - } - - &:focus-visible { - border-color: ${props => props.theme.colors.purple}; - } - - &:disabled { - opacity: 0.4; - } -`; - -export const TemplateGrid = styled.div` - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; - overflow: auto; - padding-bottom: 12px; - - @media screen and (max-width: 756px) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - @media screen and (max-width: 485px) { - grid-template-columns: 1fr; - } -`; - -// Select component places the content with a fixed padding if it has an icon -// !important here will overule that setting since the new select is bigger -export const StyledSelect = styled(Select)` - height: 48px; - padding-left: 44px !important; - font-family: inherit; - height: 32px; - padding: 8px 16px; - background-color: #2a2a2a; - color: #999999; - border: none; - border-radius: 2px; - font-size: 13px; - line-height: 16px; - font-weight: 500; - &:hover { - color: #e5e5e5; - } - &:focus { - color: #e5e5e5; - } -`; - -export const UnstyledButtonLink = styled.button` - appearance: none; - padding: 0; - background: transparent; - color: inherit; - border: none; - font-size: inherit; - font-family: inherit; - text-decoration: underline; - cursor: pointer; -`; diff --git a/packages/app/src/app/components/CreateSandbox/index.ts b/packages/app/src/app/components/CreateSandbox/index.ts deleted file mode 100644 index ab9c4b4e24c..00000000000 --- a/packages/app/src/app/components/CreateSandbox/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CreateSandbox, COLUMN_MEDIA_THRESHOLD } from './CreateSandbox'; diff --git a/packages/app/src/app/components/CreateSandbox/queries.ts b/packages/app/src/app/components/CreateSandbox/queries.ts deleted file mode 100644 index a77e23581f9..00000000000 --- a/packages/app/src/app/components/CreateSandbox/queries.ts +++ /dev/null @@ -1,184 +0,0 @@ -import gql from 'graphql-tag'; - -const TEMPLATE_FRAGMENT = gql` - fragment Template on Template { - id - color - iconUrl - published - sandbox { - id - alias - title - description - insertedAt - updatedAt - isV2 - isSse - - team { - name - } - - author { - username - } - - source { - template - } - } - } -`; - -export const LIST_PERSONAL_TEMPLATES = gql` - query ListPersonalTemplates { - me { - templates { - ...Template - } - - recentlyUsedTemplates { - ...Template - - sandbox { - git { - id - username - commitSha - path - repo - branch - } - } - } - - bookmarkedTemplates { - ...Template - } - - teams { - id - name - bookmarkedTemplates { - ...Template - } - templates { - ...Template - } - } - } - } - - ${TEMPLATE_FRAGMENT} -`; - -export const GET_GITHUB_REPO = gql` - query GetGithubRepo($owner: String!, $name: String!) { - githubRepo(owner: $owner, repo: $name) { - name - fullName - updatedAt - pushedAt - authorization - private - owner { - id - login - avatarUrl - } - } - } -`; - -const PROFILE_FRAGMENT = gql` - fragment Profile on GithubProfile { - id - login - name - } -`; - -const ORGANIZATION_FRAGMENT = gql` - fragment Organization on GithubOrganization { - id - login - } -`; - -export const GET_GITHUB_ACCOUNTS = gql` - query GetGithubAccounts { - me { - githubProfile { - ...Profile - } - githubOrganizations { - ...Organization - } - } - } - - ${PROFILE_FRAGMENT} - ${ORGANIZATION_FRAGMENT} -`; - -// TODO: Remove unnecessary fields -export const GET_GITHUB_ACCOUNT_REPOS = gql` - query GetGitHubAccountRepos($perPage: Int, $page: Int) { - me { - id - githubRepos(perPage: $perPage, page: $page) { - id - authorization - fullName - name - private - updatedAt - pushedAt - owner { - id - login - avatarUrl - } - } - } - } -`; - -// TODO: Remove unnecessary fields -export const GET_GITHUB_ORGANIZATION_REPOS = gql` - query GetGitHubOrganizationRepos( - $organization: String! - $perPage: Int - $page: Int - ) { - githubOrganizationRepos( - organization: $organization - perPage: $perPage - page: $page - ) { - id - authorization - fullName - name - private - updatedAt - pushedAt - owner { - id - login - } - } - } -`; - -export const GET_REPOSITORY_TEAMS = gql` - query RepositoryTeams($owner: String!, $name: String!) { - projects(owner: $owner, name: $name, provider: GITHUB) { - team { - id - name - } - } - } -`; diff --git a/packages/app/src/app/components/CreateSandbox/types.ts b/packages/app/src/app/components/CreateSandbox/types.ts deleted file mode 100644 index 81923ff19aa..00000000000 --- a/packages/app/src/app/components/CreateSandbox/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TemplateFragment } from 'app/graphql/types'; - -export type CreateSandboxParams = { - name?: string; - githubOwner?: string; - createRepo?: boolean; -}; - -export interface TemplateInfo { - title?: string; - key: string; - templates: TemplateFragment[]; - isOwned?: boolean; -} diff --git a/packages/app/src/app/components/CreateSandbox/useEssentialTemplates.ts b/packages/app/src/app/components/CreateSandbox/useEssentialTemplates.ts deleted file mode 100644 index bff393d564d..00000000000 --- a/packages/app/src/app/components/CreateSandbox/useEssentialTemplates.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useEffect, useState } from 'react'; -import { TemplateInfo } from './types'; -import { getTemplateInfosFromAPI } from './utils/api'; - -type EssentialsState = - | { - state: 'loading'; - } - | { - state: 'success'; - essentials: TemplateInfo[]; - } - | { - state: 'error'; - error: string; - }; - -export const useEssentialTemplates = () => { - const [essentialState, setEssentialState] = useState({ - state: 'loading', - }); - - useEffect(() => { - async function getEssentials() { - try { - const result = await getTemplateInfosFromAPI( - '/api/v1/sandboxes/templates/explore' - ); - - setEssentialState({ - state: 'success', - essentials: result, - }); - } catch { - setEssentialState({ - state: 'error', - error: 'Something went wrong when fetching more templates, sorry!', - }); - } - } - - if (essentialState.state === 'loading') { - getEssentials(); - } - }, [essentialState.state]); - - return essentialState; -}; diff --git a/packages/app/src/app/components/CreateSandbox/useOfficialTemplates.ts b/packages/app/src/app/components/CreateSandbox/useOfficialTemplates.ts deleted file mode 100644 index d1fed1a1332..00000000000 --- a/packages/app/src/app/components/CreateSandbox/useOfficialTemplates.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { TemplateFragment } from 'app/graphql/types'; -import { useEffect, useState } from 'react'; -import { getTemplateInfosFromAPI } from './utils/api'; - -type State = - | { - state: 'loading'; - } - | { - state: 'ready'; - templates: TemplateFragment[]; - } - | { - state: 'error'; - error: string; - }; - -export const useOfficialTemplates = (): State => { - const [officialTemplates, setOfficialTemplates] = useState({ - state: 'loading', - }); - - useEffect(() => { - async function fetchTemplates() { - try { - const response = await getTemplateInfosFromAPI( - '/api/v1/sandboxes/templates/official' - ); - - setOfficialTemplates({ - state: 'ready', - templates: response[0].templates, - }); - } catch { - setOfficialTemplates({ - state: 'error', - error: 'Something went wrong when fetching more templates, sorry!', - }); - } - } - - if (officialTemplates.state === 'loading') { - fetchTemplates(); - } - }, [officialTemplates.state]); - - return officialTemplates; -}; diff --git a/packages/app/src/app/components/CreateSandbox/useTeamTemplates.ts b/packages/app/src/app/components/CreateSandbox/useTeamTemplates.ts deleted file mode 100644 index 7ea54f72910..00000000000 --- a/packages/app/src/app/components/CreateSandbox/useTeamTemplates.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useQuery } from '@apollo/react-hooks'; -import { - ListPersonalTemplatesQuery, - ListPersonalTemplatesQueryVariables, - TemplateFragment, -} from 'app/graphql/types'; -import { LIST_PERSONAL_TEMPLATES } from './queries'; - -type State = - | { state: 'loading' } - | { - state: 'ready'; - recentTemplates: TemplateFragment[]; - teamTemplates: TemplateFragment[]; - } - | { - state: 'error'; - error: string; - }; - -function getUserTemplates(data: ListPersonalTemplatesQuery) { - return data.me.templates; -} - -function getTeamTemplates(data: ListPersonalTemplatesQuery, teamId: string) { - return data.me.teams.find(team => team.id === teamId)?.templates || []; -} - -type UseTeamTemplatesParams = { - isUser: boolean; - teamId?: string; - hasLogIn: boolean; -}; - -export const useTeamTemplates = ({ - isUser, - teamId, - hasLogIn, -}: UseTeamTemplatesParams): State => { - const { data, error } = useQuery< - ListPersonalTemplatesQuery, - ListPersonalTemplatesQueryVariables - >(LIST_PERSONAL_TEMPLATES, { - /** - * With LIST_PERSONAL_TEMPLATES we're also fetching team templates. We're reusing - * this query here because it has already been preloaded and cached by overmind. We're - * filtering what we need later. - */ - variables: {}, - fetchPolicy: 'cache-and-network', - skip: !hasLogIn, - }); - - if (error) { - return { - state: 'error', - error: error.message, - }; - } - - // Instead of checking the loading var we check this. Apollo sets the loading - // var to true even if we still have cached data that we can use. We also need to - // check if `data.me` isnt undefined before getting templates. - if (typeof data?.me === 'undefined') { - return { - state: 'loading', - }; - } - - const teamTemplates = - isUser || !teamId ? getUserTemplates(data) : getTeamTemplates(data, teamId); - - return { - state: 'ready', - recentTemplates: data.me.recentlyUsedTemplates, - teamTemplates, - }; -}; diff --git a/packages/app/src/app/components/CreateSandbox/utils/api.ts b/packages/app/src/app/components/CreateSandbox/utils/api.ts deleted file mode 100644 index a0c1587e05e..00000000000 --- a/packages/app/src/app/components/CreateSandbox/utils/api.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { TemplateType } from '@codesandbox/common/lib/templates'; -import { isServer } from '@codesandbox/common/lib/templates/helpers/is-server'; -import { TemplateInfo } from '../types'; - -interface IExploreTemplate { - title: string; - sandboxes: { - id: string; - title: string | null; - alias: string | null; - description: string | null; - inserted_at: string; - updated_at: string; - author: { username: string } | null; - environment: TemplateType; - v2?: boolean; - custom_template: { - id: string; - icon_url: string; - color: string; - }; - collection?: { - team: { - name: string; - }; - }; - git: { - id: string; - username: string; - commit_sha: string; - path: string; - repo: string; - branch: string; - }; - }[]; -} - -const mapAPIResponseToTemplateInfo = ( - exploreTemplate: IExploreTemplate -): TemplateInfo => ({ - key: exploreTemplate.title, - title: exploreTemplate.title, - templates: exploreTemplate.sandboxes.map(sandbox => ({ - id: sandbox.custom_template.id, - color: sandbox.custom_template.color, - iconUrl: sandbox.custom_template.icon_url, - published: true, - sandbox: { - id: sandbox.id, - insertedAt: sandbox.inserted_at, - updatedAt: sandbox.updated_at, - alias: sandbox.alias, - title: sandbox.title, - author: sandbox.author, - description: sandbox.description, - source: { - template: sandbox.environment, - }, - // TODO: Update /official and /essential endpoints to return - // team -> name instead of collection -> team -> name - team: { - name: 'CodeSandbox', - }, - isV2: sandbox.v2, - isSse: isServer(sandbox.environment), - git: sandbox.git && { - id: sandbox.git.id, - username: sandbox.git.username, - commitSha: sandbox.git.commit_sha, - path: sandbox.git.path, - repo: sandbox.git.repo, - branch: sandbox.git.branch, - }, - }, - })), -}); - -export const getTemplateInfosFromAPI = (url: string): Promise => - fetch(url) - .then(res => res.json()) - .then((body: IExploreTemplate[]) => body.map(mapAPIResponseToTemplateInfo)); - -type ValidateRepositoryDestinationFn = ( - destination: string -) => Promise<{ valid: boolean; message?: string }>; - -/** - * @param destination In the format of `owner/repo` - */ -export const validateRepositoryDestination: ValidateRepositoryDestinationFn = destination => { - // Get the authentication token from local storage if it exists. - const token = localStorage.getItem('devJwt'); - - return fetch(`/api/beta/repos/validate/github/${destination}`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'x-codesandbox-client': 'legacy-web', - authorization: token ? `Bearer ${token}` : '', - }, - }) - .then(res => { - if (!res.ok) { - throw Error(res.statusText); - } - - return res; - }) - .then(res => res.json()); -}; diff --git a/packages/app/src/app/hooks/useGitHubAccountRepositories.ts b/packages/app/src/app/hooks/useGitHubAccountRepositories.ts index b2a2e4085a8..87d36348116 100644 --- a/packages/app/src/app/hooks/useGitHubAccountRepositories.ts +++ b/packages/app/src/app/hooks/useGitHubAccountRepositories.ts @@ -9,7 +9,7 @@ import { import { GET_GITHUB_ACCOUNT_REPOS, GET_GITHUB_ORGANIZATION_REPOS, -} from '../components/CreateSandbox/queries'; +} from '../components/Create/utils/queries'; // GitHub makes a distinction between personal and organization accounts // both are accounts, so I'm calling them that. diff --git a/packages/app/src/app/hooks/useGithubOrganizations.ts b/packages/app/src/app/hooks/useGithubOrganizations.ts index 084421e941c..af1926a0803 100644 --- a/packages/app/src/app/hooks/useGithubOrganizations.ts +++ b/packages/app/src/app/hooks/useGithubOrganizations.ts @@ -5,7 +5,7 @@ import { ProfileFragment, OrganizationFragment, } from 'app/graphql/types'; -import { GET_GITHUB_ACCOUNTS } from '../components/CreateSandbox/queries'; +import { GET_GITHUB_ACCOUNTS } from '../components/Create/utils/queries'; export const useGithubAccounts = (): { state: 'error' | 'loading' | 'ready'; diff --git a/packages/app/src/app/overmind/actions.ts b/packages/app/src/app/overmind/actions.ts index 44382484f5e..a3f105f9471 100755 --- a/packages/app/src/app/overmind/actions.ts +++ b/packages/app/src/app/overmind/actions.ts @@ -591,16 +591,6 @@ export const getActiveTeamInfo = async ({ return currentTeam; }; -export const openCreateSandboxModal = ( - { actions }: Context, - props: { - collectionId?: string; - initialTab?: 'import'; - } -) => { - actions.modals.newSandboxModal.open(props); -}; - type OpenCreateTeamModalParams = { step: TeamStep; hasNextStep?: boolean; diff --git a/packages/app/src/app/overmind/effects/api/index.ts b/packages/app/src/app/overmind/effects/api/index.ts index 34a04d35e43..94b72806c24 100755 --- a/packages/app/src/app/overmind/effects/api/index.ts +++ b/packages/app/src/app/overmind/effects/api/index.ts @@ -25,7 +25,7 @@ import { SettingsSync, ForkSandboxBody, } from '@codesandbox/common/lib/types'; -import { LIST_PERSONAL_TEMPLATES } from 'app/components/CreateSandbox/queries'; +import { LIST_PERSONAL_TEMPLATES } from 'app/components/Create/utils/queries'; import { client } from 'app/graphql/client'; import { PendingUserType } from 'app/overmind/state'; diff --git a/packages/app/src/app/overmind/modals.ts b/packages/app/src/app/overmind/modals.ts index 342bb0f43b1..96cb9759698 100644 --- a/packages/app/src/app/overmind/modals.ts +++ b/packages/app/src/app/overmind/modals.ts @@ -14,14 +14,6 @@ export const forkFrozenModal = { result: 'fork' as 'fork' | 'cancel' | 'unfreeze', }; -export const newSandboxModal: { - state: { collectionId?: null | string; initialTab?: 'import' | null }; - result: undefined; -} = { - state: { collectionId: null, initialTab: null }, - result: undefined, -}; - export const moveSandboxModal: { state: { sandboxIds: string[]; diff --git a/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx b/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx index 3f9e516c997..ea0dd79f0a8 100644 --- a/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/TemplatesRow/TemplatesRow.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { CreateCard, SkeletonText, Stack } from '@codesandbox/components'; +import { SkeletonText, Stack } from '@codesandbox/components'; import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator'; -import { useOfficialTemplates } from 'app/components/CreateSandbox/useOfficialTemplates'; +import { useOfficialTemplates } from 'app/components/Create/hooks/useOfficialTemplates'; import { TemplateCard } from 'app/components/Create/TemplateCard'; import { useActions, useAppState } from 'app/overmind'; import { TemplateFragment } from 'app/graphql/types'; @@ -100,24 +100,6 @@ export const TemplatesRow: React.FC = () => { )) : null} - - {filteredTemplates?.length === 0 || - officialTemplates.state === 'error' ? ( - { - track('Empty State Card - Open create modal', { - codesandbox: 'V1', - event_source: 'UI', - card_type: 'get-started-action', - tab: 'default', - }); - - actions.openCreateSandboxModal(); - }} - /> - ) : null}
); diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/EmptyRecent.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/EmptyRecent.tsx index 630b17f1dfb..db1a28d594e 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/EmptyRecent.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/EmptyRecent.tsx @@ -1,5 +1,4 @@ import { EmptyPage } from 'app/pages/Dashboard/Components/EmptyPage'; -import { TemplatesRow } from 'app/pages/Dashboard/Components/TemplatesRow'; import React from 'react'; import { useAppState } from 'app/overmind'; import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization'; @@ -23,7 +22,6 @@ export const EmptyRecent: React.FC = () => { > {!environment.isOnPrem && } - {!environment.isOnPrem && } {!environment.isOnPrem && isPrimarySpace ? : null} diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentContent.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentContent.tsx index 9675d44838f..060414e5092 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentContent.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentContent.tsx @@ -7,7 +7,6 @@ import { Branch } from 'app/pages/Dashboard/Components/Branch'; import { ViewOptions } from 'app/pages/Dashboard/Components/Filters/ViewOptions'; import { Sandbox } from 'app/pages/Dashboard/Components/Sandbox'; import { SelectionProvider } from 'app/pages/Dashboard/Components/Selection'; -import { TemplatesRow } from 'app/pages/Dashboard/Components/TemplatesRow'; import { SuggestionsRow } from 'app/pages/Dashboard/Components/SuggestionsRow/SuggestionsRow'; import { GRID_MAX_WIDTH, @@ -121,7 +120,6 @@ export const RecentContent: React.FC = ({ {showDocsLine && } - {showRepositoryImport && } ); diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx index 4d74dedc188..c2b42d15a5a 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/RecentHeader.tsx @@ -36,7 +36,7 @@ export const RecentHeader: React.FC<{ title: string }> = ({ title }) => { > { track('Recent Page - Import Repository', { diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx index 67177ddc5f6..82b5c02cdb5 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx @@ -62,7 +62,8 @@ export const SandboxesPage = () => { createNewSandbox={ currentCollection ? () => { - actions.modals.newSandboxModal.open({ + actions.modalOpened({ + modal: 'createSandbox', collectionId: currentCollection.id, }); } @@ -89,17 +90,29 @@ export const SandboxesPage = () => { { - track('Empty State Card - Open create modal', { + track('Empty Folder - Create devbox', { + codesandbox: 'V1', + event_source: 'UI', + }); + + actions.modalOpened({ modal: 'createDevbox' }); + }} + /> + { + track('Empty Folder - Create sandbox', { codesandbox: 'V1', event_source: 'UI', card_type: 'get-started-action', tab: 'default', }); - actions.openCreateSandboxModal(); + actions.modalOpened({ modal: 'createSandbox' }); }} /> diff --git a/packages/app/src/app/pages/Standalone/index.tsx b/packages/app/src/app/pages/Standalone/index.tsx index 02ac5226a9a..9c86943bd96 100644 --- a/packages/app/src/app/pages/Standalone/index.tsx +++ b/packages/app/src/app/pages/Standalone/index.tsx @@ -3,13 +3,18 @@ import { createGlobalStyle, withTheme } from 'styled-components'; import { ThemeProvider, Element } from '@codesandbox/components'; import { useActions } from 'app/overmind'; import { useParams } from 'react-router-dom'; -import { CreateSandbox } from 'app/components/CreateSandbox'; +import { CreateBox } from 'app/components/Create/CreateBox'; +import { ImportRepository } from 'app/components/Create/ImportRepository'; +import { GenericCreate } from 'app/components/Create/GenericCreate'; import { Preferences } from '../common/Modals/PreferencesModal'; import { NotFound } from '../common/NotFound'; const COMPONENT_MAP = { preferences: Preferences, - create: CreateSandbox, + create: GenericCreate, + createSandbox: () => , + createDevbox: () => , + import: ImportRepository, }; export const StandalonePage = withTheme(({ theme }) => { diff --git a/packages/app/src/app/pages/common/Modals/index.tsx b/packages/app/src/app/pages/common/Modals/index.tsx index 0d152178f0d..5fc3c44c454 100644 --- a/packages/app/src/app/pages/common/Modals/index.tsx +++ b/packages/app/src/app/pages/common/Modals/index.tsx @@ -1,9 +1,5 @@ import codesandbox from '@codesandbox/common/lib/themes/codesandbox.json'; import { ThemeProvider } from '@codesandbox/components'; -import { - COLUMN_MEDIA_THRESHOLD, - CreateSandbox, -} from 'app/components/CreateSandbox'; import { useLocation } from 'react-router-dom'; import Modal from 'app/components/Modal'; import { useAppState, useActions } from 'app/overmind'; @@ -64,10 +60,6 @@ const modals = { Component: LegacyPaymentModal, width: 600, }, - newSandbox: { - Component: CreateSandbox, - width: () => (window.outerWidth > COLUMN_MEDIA_THRESHOLD ? 1200 : 950), - }, createDevbox: { Component: CreateBox, width: 950, diff --git a/packages/app/src/app/pages/index.tsx b/packages/app/src/app/pages/index.tsx index 8a81899a050..19e25ebb29d 100644 --- a/packages/app/src/app/pages/index.tsx +++ b/packages/app/src/app/pages/index.tsx @@ -7,7 +7,6 @@ import { Loadable } from 'app/utils/Loadable'; import React, { useEffect } from 'react'; import { SignInModal } from 'app/components/SignInModal'; import { Redirect, Route, Switch, withRouter } from 'react-router-dom'; -import { CreateSandboxModal } from 'app/components/CreateSandbox/CreateSandboxModal'; import { Debug } from 'app/components/Debug'; import { ErrorBoundary } from './common/ErrorBoundary'; import { Modals } from './common/Modals'; @@ -225,7 +224,6 @@ const RoutesComponent: React.FC = () => { - {modals.moveSandboxModal.isCurrent && activeTeamInfo && ( )} From d98bb8afda920da51685bfcaf76803167073fd61 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Thu, 16 Nov 2023 13:48:39 +0000 Subject: [PATCH 10/45] refactor: Change showBetaBadge to showDevboxBadge in SandboxCard.tsx Refactored the code in SandboxCard.tsx to replace instances of showBetaBadge with showDevboxBadge. --- .../Components/Sandbox/SandboxCard.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 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 470b1ec1f71..8c372b30839 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Sandbox/SandboxCard.tsx @@ -139,14 +139,14 @@ type SandboxStatsProps = { isFrozen?: boolean; prNumber?: number; restricted: boolean; - showBetaBadge: boolean; + showDevboxBadge: boolean; originalGit?: RepoFragmentDashboardFragment['originalGit']; } & Pick; const SandboxStats: React.FC = React.memo( ({ isFrozen, restricted, - showBetaBadge, + showDevboxBadge, noDrag, lastUpdated, PrivacyIcon, @@ -164,12 +164,24 @@ const SandboxStats: React.FC = React.memo( return Restricted; } - if (showBetaBadge) { - return Devbox; + if (showDevboxBadge) { + return ( + + + + Devbox + + + ); } - return null; - }, [restricted, showBetaBadge]); + return ( + + + Sandbox + + ); + }, [restricted, showDevboxBadge]); return ( = React.memo( align="center" css={{ height: '16px', + color: '#A6A6A6', }} className="sandbox-stats" > @@ -321,7 +334,7 @@ export const SandboxCard = ({ isFrozen={sandbox.isFrozen && !sandbox.customTemplate} PrivacyIcon={PrivacyIcon} restricted={restricted} - showBetaBadge={sandbox.isV2} + showDevboxBadge={sandbox.isV2} /> From 4206fe9934ed9ca5398d05473c1b446e71f0c339 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Thu, 16 Nov 2023 15:09:18 +0000 Subject: [PATCH 11/45] feat: Add Devbox and Sandbox alternatives Added DevboxAlternative and SandboxAlternative components to the CreateBox and TemplateList files. --- .../src/app/components/Create/CreateBox.tsx | 143 +++++++++++------- .../app/components/Create/TemplateList.tsx | 28 ++-- .../src/app/components/Create/elements.tsx | 38 +++++ 3 files changed, 145 insertions(+), 64 deletions(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index c38808101f3..3dcd87ceb2d 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -26,6 +26,8 @@ import { ModalContent, ModalSidebar, ModalBody, + DevboxAlternative, + SandboxAlternative, } from './elements'; import { TemplateList } from './TemplateList'; import { useEssentialTemplates } from './hooks/useEssentialTemplates'; @@ -277,72 +279,99 @@ export const CreateBox: React.FC = ({ {viewState === 'initial' ? ( - - { - const query = e.target.value; - tabState.select('all'); - setSearchQuery(query); - }} - /> - - - - - {showFeaturedTemplates && ( + + + { + const query = e.target.value; + tabState.select('all'); + setSearchQuery(query); + }} + /> + + + + + {showFeaturedTemplates && ( + trackTabClick('featured')} + stopId="featured" + > + Featured templates + + )} + trackTabClick('featured')} - stopId="featured" + onClick={() => trackTabClick('all')} + stopId="all" > - Featured templates + All templates - )} - trackTabClick('all')} - stopId="all" - > - All templates - + - + {showTeamTemplates ? ( + trackTabClick('workspace')} + stopId="workspace" + > + Workspace templates + + ) : null} - {showTeamTemplates ? ( trackTabClick('workspace')} - stopId="workspace" + onClick={() => trackTabClick('official')} + stopId="official" > - Workspace templates + Official templates - ) : null} - - trackTabClick('official')} - stopId="official" - > - Official templates - - - - {showEssentialTemplates && essentialState.state === 'success' - ? essentialState.essentials.map(essential => ( - trackTabClick(essential.title)} - > - {essential.title} - - )) - : null} - + + + {showEssentialTemplates && + essentialState.state === 'success' + ? essentialState.essentials.map(essential => ( + trackTabClick(essential.title)} + > + {essential.title} + + )) + : null} + + + + + {type === 'devbox' + ? "There's even more" + : 'Do more with Devboxes'} + + + {type === 'devbox' ? ( + + ) : ( + { + actions.modalOpened({ + modal: 'createDevbox', + }); + }} + /> + )} + + ) : null} @@ -359,6 +388,7 @@ export const CreateBox: React.FC = ({ { selectTemplate(template, 'featured'); }} @@ -370,6 +400,7 @@ export const CreateBox: React.FC = ({ { selectTemplate(template, 'featured'); }} @@ -392,6 +423,7 @@ export const CreateBox: React.FC = ({ } templates={allTemplates} searchQuery={searchQuery} + type={type} showEmptyState onSelectTemplate={template => { selectTemplate(template, 'all'); @@ -407,6 +439,7 @@ export const CreateBox: React.FC = ({ { selectTemplate(template, 'workspace'); }} @@ -421,6 +454,7 @@ export const CreateBox: React.FC = ({ { selectTemplate(template, 'official'); }} @@ -440,6 +474,7 @@ export const CreateBox: React.FC = ({ { selectTemplate(template, essential.title); }} diff --git a/packages/app/src/app/components/Create/TemplateList.tsx b/packages/app/src/app/components/Create/TemplateList.tsx index dc6d3658d9a..f4ea8abe527 100644 --- a/packages/app/src/app/components/Create/TemplateList.tsx +++ b/packages/app/src/app/components/Create/TemplateList.tsx @@ -5,13 +5,18 @@ import { css } from '@styled-system/css'; import { useAppState, useActions } from 'app/overmind'; import { TemplateFragment } from 'app/graphql/types'; import { TemplateCard } from './TemplateCard'; -import { TemplateGrid } from './elements'; +import { + DevboxAlternative, + SandboxAlternative, + TemplateGrid, +} from './elements'; interface TemplateListProps { title: string; isCloudTemplateList?: boolean; showEmptyState?: boolean; searchQuery?: string; + type: 'sandbox' | 'devbox'; templates: TemplateFragment[]; onSelectTemplate: (template: TemplateFragment) => void; onOpenTemplate: (template: TemplateFragment) => void; @@ -25,6 +30,7 @@ export const TemplateList = ({ onOpenTemplate, showEmptyState = false, searchQuery, + type, }: TemplateListProps) => { const { hasLogIn } = useAppState(); const actions = useActions(); @@ -99,15 +105,17 @@ export const TemplateList = ({ Not finding what you need? - Browse more than 3 million community-made templates{' '} - - on our Discover - {' '} - page. + {type === 'devbox' ? ( + + ) : ( + { + actions.modalOpened({ + modal: 'createDevbox', + }); + }} + /> + )} )} diff --git a/packages/app/src/app/components/Create/elements.tsx b/packages/app/src/app/components/Create/elements.tsx index a3064165081..5efa54ce15c 100644 --- a/packages/app/src/app/components/Create/elements.tsx +++ b/packages/app/src/app/components/Create/elements.tsx @@ -191,3 +191,41 @@ export const UnstyledButtonLink = styled.button` text-decoration: underline; cursor: pointer; `; + +export const DevboxAlternative = ({ + searchQuery, +}: { + searchQuery?: string; +}) => { + return ( + <> + Browse more than 3 million community-made templates{' '} + + on our Discover + {' '} + page. + + ); +}; + +export const SandboxAlternative = ({ onClick }: { onClick: () => void }) => { + return ( + <> + Devboxes support many more technologies and frameworks, including + back-end. +
+ + Browse Devbox templates + + + ); +}; From f7b4dfde96cefc26da11b40ce59e350fd313a916 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Thu, 16 Nov 2023 15:22:11 +0000 Subject: [PATCH 12/45] style: Update text color and underline style Update text color and underline style in CreateBox and elements components --- packages/app/src/app/components/Create/CreateBox.tsx | 2 +- packages/app/src/app/components/Create/elements.tsx | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 3dcd87ceb2d..6fa800e360f 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -358,7 +358,7 @@ export const CreateBox: React.FC = ({ ? "There's even more" : 'Do more with Devboxes'} - + {type === 'devbox' ? ( ) : ( diff --git a/packages/app/src/app/components/Create/elements.tsx b/packages/app/src/app/components/Create/elements.tsx index 5efa54ce15c..d21d9aec34c 100644 --- a/packages/app/src/app/components/Create/elements.tsx +++ b/packages/app/src/app/components/Create/elements.tsx @@ -184,11 +184,10 @@ export const UnstyledButtonLink = styled.button` appearance: none; padding: 0; background: transparent; - color: inherit; + color: #e4fc82; border: none; font-size: inherit; font-family: inherit; - text-decoration: underline; cursor: pointer; `; @@ -201,7 +200,7 @@ export const DevboxAlternative = ({ <> Browse more than 3 million community-made templates{' '} - on our Discover - {' '} - page. + on our Discover page + + . ); }; @@ -222,7 +221,6 @@ export const SandboxAlternative = ({ onClick }: { onClick: () => void }) => { <> Devboxes support many more technologies and frameworks, including back-end. -
Browse Devbox templates From b4192bbe9f304e7800447c41326b6cbecd58c93a Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Thu, 16 Nov 2023 15:30:38 +0000 Subject: [PATCH 13/45] feat: Add optional v2 parameter to body object Update CreateBox and actions.ts to include optional v2 parameter in body object. --- packages/app/src/app/components/Create/CreateBox.tsx | 1 + packages/app/src/app/overmind/namespaces/editor/actions.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 6fa800e360f..f460fbbc9d3 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -178,6 +178,7 @@ export const CreateBox: React.FC = ({ body: { alias: name, collectionId, + v2: type === 'devbox', }, }); diff --git a/packages/app/src/app/overmind/namespaces/editor/actions.ts b/packages/app/src/app/overmind/namespaces/editor/actions.ts index 823f4276c5b..41328c099f6 100755 --- a/packages/app/src/app/overmind/namespaces/editor/actions.ts +++ b/packages/app/src/app/overmind/namespaces/editor/actions.ts @@ -739,7 +739,7 @@ export const forkExternalSandbox = async ( sandboxId: string; openInNewWindow?: boolean; hasBetaEditorExperiment?: boolean; - body?: { collectionId: string; alias?: string }; + body?: { collectionId: string; alias?: string; v2?: boolean }; } ) => { effects.analytics.track('Fork Sandbox', { type: 'external' }); From d4f43152261db313913842591185a87d19521e87 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Fri, 17 Nov 2023 09:01:06 +0000 Subject: [PATCH 14/45] feat: Update UI text and functionality --- .../src/app/components/Create/CreateBox.tsx | 3 +- packages/app/src/app/overmind/actions.ts | 3 ++ packages/app/src/app/overmind/state.ts | 2 ++ .../Components/Breadcrumbs/index.tsx | 4 +-- .../Components/Folder/FolderCard.tsx | 2 +- .../Components/Folder/FolderListItem.tsx | 2 +- .../Components/Selection/ContextMenu.tsx | 5 +++- .../Selection/ContextMenus/ContainerMenu.tsx | 28 ++++++++++++------- .../Selection/ContextMenus/MultiItemMenu.tsx | 8 +++--- .../Selection/ContextMenus/SandboxMenu.tsx | 13 +++++---- .../Dashboard/Components/Selection/index.tsx | 11 +++++--- .../SyncedSandbox/SyncedSandboxCard.tsx | 11 +------- .../SyncedSandbox/SyncedSandboxListItem.tsx | 6 +--- .../Content/routes/Drafts/EmptyDrafts.tsx | 7 +++-- .../Content/routes/Sandboxes/index.tsx | 14 ++++++++-- .../src/app/pages/Dashboard/Sidebar/index.tsx | 20 ++++++------- .../common/Modals/DeleteWorkspace/index.tsx | 2 +- .../pages/common/Modals/EmptyTrash/index.tsx | 2 +- .../DirectoryPicker/SandboxesItem/index.tsx | 2 +- .../app/src/app/pages/common/Modals/index.tsx | 12 ++++++-- 20 files changed, 91 insertions(+), 66 deletions(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index f460fbbc9d3..eae0ae976e9 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -54,7 +54,6 @@ const FEATURED_IDS = [ type CreateBoxProps = ModalContentProps & { collectionId?: string; type?: 'devbox' | 'sandbox'; - isModal: boolean; }; export const CreateBox: React.FC = ({ @@ -265,7 +264,7 @@ export const CreateBox: React.FC = ({ {/* isModal is undefined on /s/ page */} - {isModal ? ( + {isModal && closeModal ? ( // TODO: IconButton doesn't have aria label or visuallyhidden text (reads floating label too late) = ({ }[nestedPageType]; } else if (albumId) link = dashboard.discover(activeTeam); - let prefix = 'All sandboxes'; + let prefix = 'All devboxes and sandboxes'; if (nestedPageType) { prefix = { - 'synced-sandboxes': 'Synced sandboxes', + 'synced-sandboxes': 'Imported templates', 'repository-branches': 'All repositories', }[nestedPageType]; } else if (albumId) prefix = 'Discover'; diff --git a/packages/app/src/app/pages/Dashboard/Components/Folder/FolderCard.tsx b/packages/app/src/app/pages/Dashboard/Components/Folder/FolderCard.tsx index daf45fd068b..651ac660c4d 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Folder/FolderCard.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Folder/FolderCard.tsx @@ -93,7 +93,7 @@ export const FolderCard: React.FC = ({ {!isNewFolder ? ( {numberOfSandboxes || 0}{' '} - {numberOfSandboxes === 1 ? 'sandbox' : 'sandboxes'} + {numberOfSandboxes === 1 ? 'item' : 'items'} ) : null} diff --git a/packages/app/src/app/pages/Dashboard/Components/Folder/FolderListItem.tsx b/packages/app/src/app/pages/Dashboard/Components/Folder/FolderListItem.tsx index 634afab96b4..4f8d79bd503 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Folder/FolderListItem.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Folder/FolderListItem.tsx @@ -109,7 +109,7 @@ export const FolderListItem = ({ {!isNewFolder ? ( {numberOfSandboxes || 0}{' '} - {numberOfSandboxes === 1 ? 'sandbox' : 'sandboxes'} + {numberOfSandboxes === 1 ? 'item' : 'items'} ) : null} diff --git a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenu.tsx b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenu.tsx index 7ac8c03526f..753b2619480 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenu.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenu.tsx @@ -43,7 +43,8 @@ interface IContextMenuProps extends IMenuProps { repositories: Array; setRenaming: null | ((value: boolean) => void); createNewFolder: () => void; - createNewSandbox: (() => void) | null; + createNewSandbox: () => void; + createNewDevbox: () => void; page: PageTypes; } @@ -60,6 +61,7 @@ export const ContextMenu: React.FC = ({ setRenaming, createNewFolder, createNewSandbox, + createNewDevbox, page, }) => { if (!visible) return null; @@ -122,6 +124,7 @@ export const ContextMenu: React.FC = ({ menu = ( ); diff --git a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/ContainerMenu.tsx b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/ContainerMenu.tsx index fbbe45c7a1f..7fe44ca8bf4 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/ContainerMenu.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/ContainerMenu.tsx @@ -4,12 +4,14 @@ import { Context, MenuItem } from '../ContextMenu'; interface ContainerMenuProps { createNewFolder: () => void; - createNewSandbox: (() => void) | null; + createNewSandbox: () => void; + createNewDevbox: () => void; } export const ContainerMenu: React.FC = ({ createNewFolder, createNewSandbox, + createNewDevbox, }) => { const { visible, setVisibility, position } = React.useContext(Context); @@ -20,15 +22,21 @@ export const ContainerMenu: React.FC = ({ position={position} style={{ width: 160 }} > - {typeof createNewSandbox === 'function' && ( - { - createNewSandbox(); - }} - > - Create new sandbox - - )} + { + createNewDevbox(); + }} + > + Create new devbox + + { + createNewSandbox(); + }} + > + Create new sandbox + + createNewFolder()}>Create new folder ); 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 b78d3af0834..89c13c208cd 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 @@ -150,7 +150,7 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { const FROZEN_ITEMS = [ sandboxes.some(s => !s.sandbox.isFrozen) && { - label: 'Freeze sandboxes', + label: 'Freeze items', fn: () => { actions.dashboard.changeSandboxesFrozen({ sandboxIds: sandboxes.map(sandbox => sandbox.sandbox.id), @@ -159,7 +159,7 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { }, }, sandboxes.some(s => s.sandbox.isFrozen) && { - label: 'Unfreeze sandboxes', + label: 'Unfreeze items', fn: () => { actions.dashboard.changeSandboxesFrozen({ sandboxIds: sandboxes.map(sandbox => sandbox.sandbox.id), @@ -224,7 +224,7 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { const DELETE = { label: 'Delete items', fn: deleteItems }; const RECOVER = { - label: 'Recover Sandboxes', + label: 'Recover items', fn: () => { actions.dashboard.recoverSandboxes( [...sandboxes, ...templates].map(s => s.sandbox.id) @@ -232,7 +232,7 @@ export const MultiMenu = ({ selectedItems, page }: IMultiMenuProps) => { }, }; const PERMANENTLY_DELETE = { - label: 'Permanently delete sandboxes', + label: 'Permanently delete items', fn: () => { actions.dashboard.permanentlyDeleteSandboxes( [...sandboxes, ...templates].map(s => s.sandbox.id) 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 8756d7471a7..37fdb34281d 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 @@ -44,8 +44,9 @@ export const SandboxMenu: React.FC = ({ const url = sandboxUrl(sandbox, hasBetaEditorExperiment); const linksToV2 = sandbox.isV2 || (!sandbox.isSse && hasBetaEditorExperiment); const folderUrl = getFolderUrl(item, activeTeam); + const boxType = sandbox.isV2 ? 'devbox' : 'sandbox'; - const label = isTemplate ? 'template' : 'sandbox'; + const label = isTemplate ? 'template' : boxType; const restricted = isFree && sandbox.privacy !== 0; // TODO(@CompuIves): remove the `item.sandbox.teamId === null` check, once the server is not @@ -178,7 +179,7 @@ export const SandboxMenu: React.FC = ({ }} disabled={restricted} > - Fork sandbox + Fork {boxType} ) : null} {isOwner && userRole !== 'READ' ? ( @@ -313,7 +314,7 @@ export const SandboxMenu: React.FC = ({ }} disabled={restricted} > - Convert to sandbox + Convert to {boxType} ) : ( = ({ }} disabled={restricted} > - Make sandbox a template + Make {boxType} a template ))} {hasAccess && @@ -405,7 +406,7 @@ export const SandboxMenu: React.FC = ({ setVisibility(false); }} > - Delete sandbox + Delete {boxType} )} @@ -416,7 +417,7 @@ export const SandboxMenu: React.FC = ({ actions.dashboard.unlikeSandbox(sandbox.id); }} > - Unlike sandbox + Unlike {boxType} )} diff --git a/packages/app/src/app/pages/Dashboard/Components/Selection/index.tsx b/packages/app/src/app/pages/Dashboard/Components/Selection/index.tsx index 902787a3336..8c71103a01e 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Selection/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Selection/index.tsx @@ -88,8 +88,9 @@ const Context = React.createContext({ interface SelectionProviderProps { items: Array; - createNewFolder?: (() => void) | null; - createNewSandbox?: (() => void) | null; + createNewFolder?: () => void; + createNewSandbox?: () => void; + createNewDevbox?: () => void; activeTeamId: string | null; page: PageTypes; interactive?: boolean; @@ -97,8 +98,9 @@ interface SelectionProviderProps { export const SelectionProvider: React.FC = ({ items = [], - createNewFolder = null, - createNewSandbox = null, + createNewFolder, + createNewSandbox, + createNewDevbox, activeTeamId, page, children, @@ -705,6 +707,7 @@ export const SelectionProvider: React.FC = ({ page={page} createNewFolder={createNewFolder} createNewSandbox={createNewSandbox} + createNewDevbox={createNewDevbox} /> ); diff --git a/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxCard.tsx b/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxCard.tsx index 72a334fbde9..4be447519a8 100644 --- a/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxCard.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxCard.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - Stack, - Text, - Icon, - InteractiveOverlay, - Badge, -} from '@codesandbox/components'; +import { Stack, Text, Icon, InteractiveOverlay } from '@codesandbox/components'; import { Link } from 'react-router-dom'; import { StyledCard } from '../shared/StyledCard'; @@ -40,9 +34,6 @@ export const SyncedSandboxCard = ({ name, path, url, ...props }) => { - - Synced - diff --git a/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx b/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx index 0d4793aa186..2b6f4e53d78 100644 --- a/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx @@ -68,11 +68,7 @@ export const SyncedSandboxListItem = ({ name, path, url, ...props }) => { - - - Synced - - + {/* empty column to align with sandbox list items */} diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Drafts/EmptyDrafts.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Drafts/EmptyDrafts.tsx index f22a3696f47..300be68aaa6 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Drafts/EmptyDrafts.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Drafts/EmptyDrafts.tsx @@ -7,10 +7,11 @@ export const EmptyDrafts: React.FC = () => { return ( - By default, every sandbox you create will show up on this folder. + By default, every devbox or sandbox you create will show up on this + folder.
- Sandboxes in My Drafts are not visible to your collaborators unless - moved to the {quotes('All sandboxes')} section. + Items in My Drafts are not visible to your collaborators unless moved to + the {quotes('All devboxes and sandboxes')} section.
diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx index 82b5c02cdb5..1640f481449 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx @@ -64,10 +64,20 @@ export const SandboxesPage = () => { ? () => { actions.modalOpened({ modal: 'createSandbox', - collectionId: currentCollection.id, + itemId: currentCollection.id, }); } - : null + : undefined + } + createNewDevbox={ + currentCollection + ? () => { + actions.modalOpened({ + modal: 'createDevbox', + itemId: currentCollection.id, + }); + } + : undefined } > diff --git a/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx b/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx index badba1ecebe..1b5bc4c3b54 100644 --- a/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx @@ -267,8 +267,17 @@ export const Sidebar: React.FC = ({ /> ) : null} + {state.sidebar.hasSyncedSandboxes ? ( + + ) : null} + = ({ ]} /> - {state.sidebar.hasSyncedSandboxes ? ( - - ) : null} - { Are you sure you want to delete this workspace? This action is{' '} - irreversible and it will delete all sandboxes in this + irreversible and it will delete all the items in this workspace {otherUsers ? ( <> diff --git a/packages/app/src/app/pages/common/Modals/EmptyTrash/index.tsx b/packages/app/src/app/pages/common/Modals/EmptyTrash/index.tsx index 219fadd89ff..e8c7088c45b 100644 --- a/packages/app/src/app/pages/common/Modals/EmptyTrash/index.tsx +++ b/packages/app/src/app/pages/common/Modals/EmptyTrash/index.tsx @@ -11,7 +11,7 @@ export const EmptyTrash: FunctionComponent = () => { return ( { await permanentlyDeleteSandboxes(trashSandboxIds); diff --git a/packages/app/src/app/pages/common/Modals/MoveSandboxFolderModal/DirectoryPicker/SandboxesItem/index.tsx b/packages/app/src/app/pages/common/Modals/MoveSandboxFolderModal/DirectoryPicker/SandboxesItem/index.tsx index 2649bc86865..dac351fb291 100644 --- a/packages/app/src/app/pages/common/Modals/MoveSandboxFolderModal/DirectoryPicker/SandboxesItem/index.tsx +++ b/packages/app/src/app/pages/common/Modals/MoveSandboxFolderModal/DirectoryPicker/SandboxesItem/index.tsx @@ -112,7 +112,7 @@ class SandboxesItemComponent extends React.Component< url="/" folders={folders} foldersByPath={foldersByPath} - name="All Sandboxes" + name="All devboxes and sandboxes" disabled={disabledMessage} open onSelect={onSelect} diff --git a/packages/app/src/app/pages/common/Modals/index.tsx b/packages/app/src/app/pages/common/Modals/index.tsx index 5fc3c44c454..9b007f13e8c 100644 --- a/packages/app/src/app/pages/common/Modals/index.tsx +++ b/packages/app/src/app/pages/common/Modals/index.tsx @@ -247,6 +247,7 @@ const Modals: FunctionComponent = () => { settings: { customVSCodeTheme }, }, currentModal, + currentModalItemId, } = useAppState(); const [localState, setLocalState] = useState({ @@ -282,6 +283,13 @@ const Modals: FunctionComponent = () => { }, [pathname, localState]); const modal = currentModal && modals[currentModal]; + if (currentModal === 'createDevbox' || currentModal === 'createSandbox') { + modal.props = { + ...modal.props, + ...(currentModalItemId ? { collectionId: currentModalItemId } : {}), + }; + } + return ( { export { Modals }; export interface ModalContentProps { - closeModal: () => void; - isModal: true; + closeModal?: () => void; + isModal: boolean; } From f5382281cc8f699bb161ed150c043eb16426ee60 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Fri, 17 Nov 2023 09:01:15 +0000 Subject: [PATCH 15/45] Update SyncedSandboxListItem.tsx --- .../Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx b/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx index 2b6f4e53d78..3175632705e 100644 --- a/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/SyncedSandbox/SyncedSandboxListItem.tsx @@ -3,7 +3,6 @@ import { Element, Icon, Stack, - Badge, ListAction, Text, Grid, From a9c7cb1e11459afeb214597355f47765839d5024 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Fri, 17 Nov 2023 10:00:53 +0000 Subject: [PATCH 16/45] Update constants.ts and PermissionSettings.tsx --- packages/app/src/app/constants.ts | 8 +++---- .../components/PermissionSettings.tsx | 21 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/app/src/app/constants.ts b/packages/app/src/app/constants.ts index 42fd9b75d10..0222173ecdc 100644 --- a/packages/app/src/app/constants.ts +++ b/packages/app/src/app/constants.ts @@ -24,7 +24,7 @@ export const FREE_FEATURES: Feature[] = [ }, { key: 'public_limit', - label: 'Public repositories & sandboxes', + label: 'Public repositories & devboxes', }, { key: 'ai', @@ -44,7 +44,7 @@ export const PERSONAL_PRO_FEATURES: Feature[] = [ }, { key: 'limit_sandboxes', - label: 'Unlimited private repositories & sandboxes', + label: 'Unlimited private repositories & devboxes', }, { key: 'ai', @@ -64,7 +64,7 @@ export const TEAM_PRO_FEATURES: Feature[] = [ }, { key: 'private', - label: 'Unlimited private repositories & sandboxes', + label: 'Unlimited private repositories & devboxes', }, { key: 'ai', @@ -85,7 +85,7 @@ export const TEAM_PRO_FEATURES_WITH_PILLS: Feature[] = [ }, { key: 'private', - label: 'Unlimited private repositories & sandboxes', + label: 'Unlimited private repositories & devboxes', }, { key: 'ai', diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Settings/components/PermissionSettings.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Settings/components/PermissionSettings.tsx index 8b6e41088b9..ac9388977cb 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Settings/components/PermissionSettings.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Settings/components/PermissionSettings.tsx @@ -35,7 +35,7 @@ export const PermissionSettings = () => { You need a Pro subscription to change - sandbox permissions. + permissions. { ) : null} {isPro && !isBillingManager ? ( - + ) : null} @@ -75,15 +75,16 @@ export const PermissionSettings = () => { const privacyOptions = { 0: { - description: 'All your sandboxes are public by default.', + description: 'All your devboxes and sandboxes are public by default.', icon: () => , }, 1: { - description: 'Only people with a private link are able to see a Sandbox.', + description: + 'Only people that get the link are able to see your devboxes and sandboxes.', icon: () => , }, 2: { - description: 'Only people you share a Sandbox with, can see it.', + description: 'Only people with access can see your devboxes and sandboxes.', icon: () => , }, }; @@ -232,13 +233,13 @@ const SandboxSecurity = ({ disabled }: { disabled: boolean }) => { - Sandbox Security + Devbox and sandbox security - Disable forking and moving sandboxes outside of the workspace + Disable forking and moving items outside of the workspace { /> - Disable exporting sandboxes as .zip + Disable exporting items as .zip setPreventSandboxExport(!preventSandboxExport)} @@ -290,7 +291,7 @@ const AIPermission = ({ disabled }: { disabled: boolean }) => { key: 'privateRepositories', }, { - text: 'Enable AI feature for private sandboxes', + text: 'Enable AI feature for private devboxes', key: 'privateSandboxes', }, { @@ -298,7 +299,7 @@ const AIPermission = ({ disabled }: { disabled: boolean }) => { key: 'publicRepositories', }, { - text: 'Enable AI feature for public sandboxes', + text: 'Enable AI feature for public devboxes', key: 'publicSandboxes', }, ]; From d739902e0300d188e89c7cb673922cd40a0d9de2 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Fri, 17 Nov 2023 12:18:32 +0000 Subject: [PATCH 17/45] Update 8 files --- .../src/app/components/Create/CreateBox.tsx | 31 +++++- .../components/Create/ImportRepository.tsx | 104 ++++-------------- .../Create/ImportRepository/Import.tsx | 59 ++++------ .../components/Create/ImportSandbox/index.ts | 1 - .../ImportTemplate.tsx} | 4 +- .../components/Create/ImportTemplate/index.ts | 1 + .../src/app/components/Create/elements.tsx | 2 +- 7 files changed, 78 insertions(+), 124 deletions(-) delete mode 100644 packages/app/src/app/components/Create/ImportSandbox/index.ts rename packages/app/src/app/components/Create/{ImportSandbox/ImportSandbox.tsx => ImportTemplate/ImportTemplate.tsx} (97%) create mode 100644 packages/app/src/app/components/Create/ImportTemplate/index.ts diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index eae0ae976e9..a4f290e6ee6 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -35,6 +35,7 @@ import { useOfficialTemplates } from './hooks/useOfficialTemplates'; import { useTeamTemplates } from './hooks/useTeamTemplates'; import { CreateSandboxParams } from './utils/types'; import { SearchBox } from './SearchBox'; +import { ImportTemplate } from './ImportTemplate'; export const COLUMN_MEDIA_THRESHOLD = 1600; @@ -105,6 +106,7 @@ export const CreateBox: React.FC = ({ activeTeam && teamTemplatesData.state === 'ready' && teamTemplatesData.teamTemplates.length > 0; + const showImportTemplates = hasLogIn && activeTeam && type === 'devbox'; const recentlyUsedTemplates = teamTemplatesData.state === 'ready' @@ -132,8 +134,13 @@ export const CreateBox: React.FC = ({ teamTemplatesData.state === 'ready' ? teamTemplatesData.teamTemplates.filter(noDevboxesWhenListingSandboxes) : []; + const officialTemplateIds = officialTemplates.map(t => t.id); - const allTemplates = [...officialTemplates, ...teamTemplates] + // For all templates, ensure official and team templates do not create duplications in the list + const allTemplates = [ + ...officialTemplates, + ...teamTemplates.filter(t => !officialTemplateIds.includes(t.id)), + ] .filter(noDevboxesWhenListingSandboxes) .filter(t => searchQuery @@ -327,6 +334,22 @@ export const CreateBox: React.FC = ({ ) : null} + {showImportTemplates ? ( + { + track('Create New - Click Tab', { + codesandbox: 'V1', + event_source: 'UI', + tab_name: 'Import template', + }); + }} + stopId="import-template" + > + Import template + + ) : null} + trackTabClick('official')} @@ -450,6 +473,12 @@ export const CreateBox: React.FC = ({ ) : null} + {showImportTemplates ? ( + + + + ) : null} + = ({ isModal, closeModal, }) => { - const { environment } = useAppState(); - const mediaQuery = window.matchMedia('screen and (max-width: 950px)'); const mobileScreenSize = mediaQuery.matches; - const tabState = useTabState({ - orientation: mobileScreenSize ? 'horizontal' : 'vertical', - selectedId: 'import', - }); - const [viewState, setViewState] = useState<'initial' | 'fork'>('initial'); const [selectedRepo, setSelectedRepo] = useState(); @@ -52,8 +37,6 @@ export const ImportRepository: React.FC = ({ setViewState('fork'); }; - const showImportRepository = !environment.isOnPrem; - return ( @@ -68,7 +51,7 @@ export const ImportRepository: React.FC = ({ {viewState === 'initial' ? ( - Import + Import repository ) : ( // TODO: add aria-label based on title to IconButton? @@ -104,70 +87,27 @@ export const ImportRepository: React.FC = ({ - - {viewState === 'initial' ? ( - - - {showImportRepository && ( - { - track('Create New - Click Tab', { - codesandbox: 'V1', - event_source: 'UI', - tab_name: 'Import from Github', - }); - }} - stopId="import" - > - Import repository - - )} - - { - track('Create New - Click Tab', { - codesandbox: 'V1', - event_source: 'UI', - tab_name: 'Import template', - }); - }} - stopId="import-template" - > - Import template - - - - ) : null} - - {viewState === 'fork' ? ( - - ) : null} - - - - {viewState === 'initial' && ( - - - - - - - - - - )} - - {viewState === 'fork' ? ( - { - setViewState('initial'); - }} - /> - ) : null} - + {viewState === 'initial' ? ( + + + + ) : null} + {viewState === 'fork' ? ( + <> + + + + + + { + setViewState('initial'); + }} + /> + + + ) : null} diff --git a/packages/app/src/app/components/Create/ImportRepository/Import.tsx b/packages/app/src/app/components/Create/ImportRepository/Import.tsx index e458d11ca57..0eb61f56119 100644 --- a/packages/app/src/app/components/Create/ImportRepository/Import.tsx +++ b/packages/app/src/app/components/Create/ImportRepository/Import.tsx @@ -220,44 +220,29 @@ export const Import: React.FC = ({ onRepoSelect }) => { return ( - - - Import repository - - - + Directly work on your GitHub repository in CodeSandbox. +
+ Learn more about Repositories{' '} + - Directly work on your GitHub repository in CodeSandbox. -
- Learn more about Repositories{' '} -
- here - - . -
-
+ here + + . +
{restrictsPublicRepos ? : null} diff --git a/packages/app/src/app/components/Create/ImportSandbox/index.ts b/packages/app/src/app/components/Create/ImportSandbox/index.ts deleted file mode 100644 index 4623b993c30..00000000000 --- a/packages/app/src/app/components/Create/ImportSandbox/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ImportSandbox } from './ImportSandbox'; diff --git a/packages/app/src/app/components/Create/ImportSandbox/ImportSandbox.tsx b/packages/app/src/app/components/Create/ImportTemplate/ImportTemplate.tsx similarity index 97% rename from packages/app/src/app/components/Create/ImportSandbox/ImportSandbox.tsx rename to packages/app/src/app/components/Create/ImportTemplate/ImportTemplate.tsx index 9eb3679ce4c..c65fff4c938 100644 --- a/packages/app/src/app/components/Create/ImportSandbox/ImportSandbox.tsx +++ b/packages/app/src/app/components/Create/ImportTemplate/ImportTemplate.tsx @@ -43,7 +43,7 @@ function getRepoInfoFromURL( return { owner, repo, branch, folder }; } -export const ImportSandbox = () => { +export const ImportTemplate = () => { const [repoUrl, setRepoUrl] = React.useState(''); const info = getRepoInfoFromURL(repoUrl); @@ -97,7 +97,7 @@ export const ImportSandbox = () => { lineHeight: 1.5, }} > - Create a read-only sandbox template that stays in sync with a GitHub + Create a read-only devbox template that stays in sync with a GitHub repository or folder.
Learn more about synced templates{' '} diff --git a/packages/app/src/app/components/Create/ImportTemplate/index.ts b/packages/app/src/app/components/Create/ImportTemplate/index.ts new file mode 100644 index 00000000000..42938e2b38b --- /dev/null +++ b/packages/app/src/app/components/Create/ImportTemplate/index.ts @@ -0,0 +1 @@ +export { ImportTemplate } from './ImportTemplate'; diff --git a/packages/app/src/app/components/Create/elements.tsx b/packages/app/src/app/components/Create/elements.tsx index d21d9aec34c..80eae9d6da5 100644 --- a/packages/app/src/app/components/Create/elements.tsx +++ b/packages/app/src/app/components/Create/elements.tsx @@ -4,7 +4,7 @@ import { Tab as BaseTab, TabList, TabPanel, TabStateReturn } from 'reakit/Tab'; import { Select } from '@codesandbox/components'; export const Container = styled.div` - height: 530px; + height: 564px; overflow: hidden; border-radius: 4px; background-color: #151515; From 109dc05c77811b42c7e74dca5e91b878d2a3f7a1 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Fri, 17 Nov 2023 12:37:10 +0000 Subject: [PATCH 18/45] Update FromTemplate.tsx and useEssentialTemplates.ts --- packages/app/src/app/components/Create/FromTemplate.tsx | 2 +- .../src/app/components/Create/hooks/useEssentialTemplates.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/app/components/Create/FromTemplate.tsx b/packages/app/src/app/components/Create/FromTemplate.tsx index 5bd895d74f0..c2326bee27a 100644 --- a/packages/app/src/app/components/Create/FromTemplate.tsx +++ b/packages/app/src/app/components/Create/FromTemplate.tsx @@ -10,7 +10,7 @@ import { } from '@codesandbox/components'; import { StyledSelect } from './elements'; -import { CreateSandboxParams } from './types'; +import { CreateSandboxParams } from './utils/types'; import { InputText } from '../dashboard/InputText'; interface FromTemplateProps { diff --git a/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts b/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts index 0ab89b36c01..6d1d5822d9e 100644 --- a/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts +++ b/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts @@ -46,7 +46,7 @@ export const useEssentialTemplates = (showEssentialTemplates: boolean) => { if (essentialState.state === 'loading') { getEssentials(); } - }, [essentialState.state]); + }, [essentialState.state, showEssentialTemplates]); return essentialState; }; From f24dc9f0d47d8bd887db6dfd51906e90a2edf6f3 Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Fri, 17 Nov 2023 12:48:42 +0000 Subject: [PATCH 19/45] Update CreateBox.tsx and state.ts --- .../src/app/components/Create/CreateBox.tsx | 24 +++++++++++-------- packages/app/src/app/overmind/state.ts | 3 +-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index a4f290e6ee6..309d5df9b48 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -36,6 +36,7 @@ import { useTeamTemplates } from './hooks/useTeamTemplates'; import { CreateSandboxParams } from './utils/types'; import { SearchBox } from './SearchBox'; import { ImportTemplate } from './ImportTemplate'; +import { FromTemplate } from './FromTemplate'; export const COLUMN_MEDIA_THRESHOLD = 1600; @@ -55,11 +56,13 @@ const FEATURED_IDS = [ type CreateBoxProps = ModalContentProps & { collectionId?: string; type?: 'devbox' | 'sandbox'; + hasSecondStep?: boolean; }; export const CreateBox: React.FC = ({ collectionId, type = 'devbox', + hasSecondStep = false, closeModal, isModal, }) => { @@ -80,7 +83,7 @@ export const CreateBox: React.FC = ({ const [viewState, setViewState] = useState<'initial' | 'fromTemplate'>( 'initial' ); - const [selectedTemplate] = useState(); + const [selectedTemplate, setSelectedTemplate] = useState(); const [searchQuery, setSearchQuery] = useState(''); const noDevboxesWhenListingSandboxes = (t: TemplateFragment) => @@ -204,11 +207,12 @@ export const CreateBox: React.FC = ({ template.sandbox.title || template.sandbox.alias || template.sandbox.id, }); - createFromTemplate(template, {}); - - // Temporarily disable the second screen until we have more functionality on it - // setSelectedTemplate(template); - // setViewState('fromTemplate'); + if (hasSecondStep) { + setSelectedTemplate(template); + setViewState('fromTemplate'); + } else { + createFromTemplate(template, {}); + } }; const openTemplate = (template: TemplateFragment, trackingSource: string) => { @@ -398,9 +402,9 @@ export const CreateBox: React.FC = ({ ) : null} - {/* {viewState === 'fromTemplate' ? ( + {viewState === 'fromTemplate' ? ( - ) : null} */} + ) : null} @@ -517,7 +521,7 @@ export const CreateBox: React.FC = ({ )} - {/* {viewState === 'fromTemplate' ? ( + {viewState === 'fromTemplate' ? ( { @@ -527,7 +531,7 @@ export const CreateBox: React.FC = ({ createFromTemplate(selectedTemplate, params); }} /> - ) : null} */} + ) : null} diff --git a/packages/app/src/app/overmind/state.ts b/packages/app/src/app/overmind/state.ts index 3cf7919720c..1a215a231cd 100755 --- a/packages/app/src/app/overmind/state.ts +++ b/packages/app/src/app/overmind/state.ts @@ -52,7 +52,7 @@ type State = { }; currentModal: string | null; currentModalMessage: string | null; - currentModalItemId: string | null; // Used for passing collection id for create modals + currentModalItemId?: string; // Used for passing collection id for create modals uploadedFiles: UploadFile[] | null; maxStorage: number; usedStorage: number; @@ -152,7 +152,6 @@ export const state: State = { }, currentModal: null, currentModalMessage: null, - currentModalItemId: null, uploadedFiles: null, maxStorage: 0, usedStorage: 0, From e7ef74869bc85dadab6cfece6ce7f0eaec835dbb Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Fri, 17 Nov 2023 16:22:10 +0000 Subject: [PATCH 20/45] feat: Upgrade Sandbox to Devbox Upgrade sandbox to devbox, improving coding experience and adding new features. --- .../app/src/app/components/Create/CreateBox.tsx | 2 +- .../Preview/DevTools/TerminalUpgrade/index.tsx | 13 ++++++------- .../components/StripeMessages/UpgradeSSEToV2.tsx | 4 ++-- packages/app/src/app/hooks/useUpgradeFromV1ToV2.ts | 4 ++-- packages/app/src/app/overmind/effects/api/index.ts | 2 +- .../Sandbox/Editor/Workspace/screens/AI/index.tsx | 7 +++---- .../Editor/Workspace/screens/Docker/index.tsx | 7 +++---- .../Editor/Workspace/screens/VSCode/index.tsx | 6 +++--- 8 files changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 309d5df9b48..6f58e6ee15b 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -62,7 +62,7 @@ type CreateBoxProps = ModalContentProps & { export const CreateBox: React.FC = ({ collectionId, type = 'devbox', - hasSecondStep = false, + hasSecondStep = true, closeModal, isModal, }) => { diff --git a/packages/app/src/app/components/Preview/DevTools/TerminalUpgrade/index.tsx b/packages/app/src/app/components/Preview/DevTools/TerminalUpgrade/index.tsx index 9989de534ca..08d68602f3c 100644 --- a/packages/app/src/app/components/Preview/DevTools/TerminalUpgrade/index.tsx +++ b/packages/app/src/app/components/Preview/DevTools/TerminalUpgrade/index.tsx @@ -38,19 +38,18 @@ export const TerminalUpgradeComponent: React.FC = ({ gap={4} > - To use the terminal, you need to upgrade your Browser Sandbox into a - Cloud Sandbox. + To use the terminal, you need to upgrade your Sandbox into a Devbox. - Cloud Sandboxes are an improved coding experience that runs your code - in the cloud. They bring new languages, servers, databases, - a built-in AI assistant, and much more. See a preview below. + Devboxes are an improved coding experience that runs your code in + the cloud. They bring new languages, servers, databases, a built-in + AI assistant, and much more. See a preview below. {canConvert - ? `Do you want to convert it into a Cloud Sandbox?` - : `Do you want to fork into a Cloud Sandbox?`} + ? `Do you want to convert it into a Devbox?` + : `Do you want to fork into a Devbox?`} { return ( - This sandbox runs much faster in our new editor. Do you want to{' '} - {canConvert ? 'convert' : 'fork'} it to a Cloud Sandbox? + This sandbox runs much faster as a devbox. Do you want to{' '} + {canConvert ? 'convert' : 'fork'} it? {canConvert ? 'Yes, convert' : 'Yes, fork'} diff --git a/packages/app/src/app/hooks/useUpgradeFromV1ToV2.ts b/packages/app/src/app/hooks/useUpgradeFromV1ToV2.ts index 047407f577d..8e17b39aa39 100644 --- a/packages/app/src/app/hooks/useUpgradeFromV1ToV2.ts +++ b/packages/app/src/app/hooks/useUpgradeFromV1ToV2.ts @@ -26,7 +26,7 @@ export const useUpgradeFromV1ToV2 = ( setIsLoading(true); - track(`Editor - ${trackingLocation} Convert to Cloud Sandbox`, { + track(`Editor - ${trackingLocation} Convert to Devbox`, { owned: canConvert, }); @@ -70,7 +70,7 @@ export const useUpgradeFromV1ToV2 = ( } catch (err) { setIsLoading(false); effects.notificationToast.error( - 'Failed to convert to Cloud Sandbox. Please try again.' + 'Failed to convert. Please try again.' ); } }, diff --git a/packages/app/src/app/overmind/effects/api/index.ts b/packages/app/src/app/overmind/effects/api/index.ts index 94b72806c24..1b53a498e5c 100755 --- a/packages/app/src/app/overmind/effects/api/index.ts +++ b/packages/app/src/app/overmind/effects/api/index.ts @@ -506,7 +506,7 @@ export default { }, /** * Updates a sandbox. Used to update sandbox metadata but also to convert - * a sandbox to a cloud sandbox. + * a sandbox to a devbox. */ updateSandbox(sandboxId: string, data: Partial): Promise { return api.put(`/sandboxes/${sandboxId}`, { diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/AI/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/AI/index.tsx index 1b7b1b13203..ad32db81ad3 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/AI/index.tsx +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/AI/index.tsx @@ -15,12 +15,11 @@ export const AITools = () => { - To access AI tools, you need to upgrade your Browser Sandbox into a - Cloud Sandbox. + To access AI tools, you need to upgrade your Sandbox into a Devbox. - Cloud Sandboxes are an improved coding experience that runs your code - in the cloud. They allow you to run a Chat Devtool you can use to ask + Devboxes are an improved coding experience that runs your code in the + cloud. They allow you to run a Chat Devtool you can use to ask questions and generate code with AI diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Docker/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Docker/index.tsx index 686c0552812..19c57b092cc 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Docker/index.tsx +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Docker/index.tsx @@ -15,12 +15,11 @@ export const Docker = () => { - To run Docker, you need to upgrade your Browser Sandbox into a Cloud - Sandbox. + To run Docker, you need to upgrade your Sandbox into a Devbox. - Cloud Sandboxes are an improved coding experience that runs your code - in the cloud. They allow you to run Docker, code in new languages, add + Devboxes are an improved coding experience that runs your code in the + cloud. They allow you to run Docker, code in new languages, add servers, databases, and much more. diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/VSCode/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/VSCode/index.tsx index 578b4f0cbe0..d5ac88bd00d 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/VSCode/index.tsx +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/VSCode/index.tsx @@ -16,11 +16,11 @@ export const VSCode = () => { To use VS Code together with CodeSandbox, you need to convert your - Browser Sandbox into a Cloud Sandbox. + Sandbox into a Devbox. - Cloud Sandboxes are an improved coding experience that runs your code - in the cloud. They allow you to run Docker, code in new languages, add + Devboxes are an improved coding experience that runs your code in the + cloud. They allow you to run Docker, code in new languages, add servers, databases, and much more. From f57530190e6d8fa12b360501a36b2b5498a0262b Mon Sep 17 00:00:00 2001 From: Alex Moldovan Date: Sat, 18 Nov 2023 11:03:00 +0000 Subject: [PATCH 21/45] Update 13 files --- .../src/app/components/Create/CreateBox.tsx | 147 +++------- .../Create/hooks/useAllTemplates.ts | 41 +++ .../Create/hooks/useEssentialTemplates.ts | 52 ---- .../Create/hooks/useFeaturedTemplates.ts | 36 +++ .../Create/hooks/useOfficialTemplates.ts | 17 +- .../Create/hooks/useTeamTemplates.ts | 82 ++++-- .../Create/hooks/useTemplateCollections.ts | 62 ++++ .../src/app/components/Create/utils/api.ts | 16 +- .../app/components/Create/utils/queries.ts | 30 +- .../src/app/components/Create/utils/types.ts | 2 +- packages/app/src/app/graphql/types.ts | 277 +++++++++++++----- .../app/src/app/overmind/effects/api/index.ts | 6 +- .../app/src/app/overmind/internalActions.ts | 3 +- 13 files changed, 469 insertions(+), 302 deletions(-) create mode 100644 packages/app/src/app/components/Create/hooks/useAllTemplates.ts delete mode 100644 packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts create mode 100644 packages/app/src/app/components/Create/hooks/useFeaturedTemplates.ts create mode 100644 packages/app/src/app/components/Create/hooks/useTemplateCollections.ts diff --git a/packages/app/src/app/components/Create/CreateBox.tsx b/packages/app/src/app/components/Create/CreateBox.tsx index 6f58e6ee15b..8816b252e2b 100644 --- a/packages/app/src/app/components/Create/CreateBox.tsx +++ b/packages/app/src/app/components/Create/CreateBox.tsx @@ -30,29 +30,18 @@ import { SandboxAlternative, } from './elements'; import { TemplateList } from './TemplateList'; -import { useEssentialTemplates } from './hooks/useEssentialTemplates'; +import { useTemplateCollections } from './hooks/useTemplateCollections'; import { useOfficialTemplates } from './hooks/useOfficialTemplates'; import { useTeamTemplates } from './hooks/useTeamTemplates'; import { CreateSandboxParams } from './utils/types'; import { SearchBox } from './SearchBox'; import { ImportTemplate } from './ImportTemplate'; import { FromTemplate } from './FromTemplate'; +import { useFeaturedTemplates } from './hooks/useFeaturedTemplates'; +import { useAllTemplates } from './hooks/useAllTemplates'; export const COLUMN_MEDIA_THRESHOLD = 1600; -const FEATURED_IDS = [ - 'new', - 'vanilla', - 'vue', - 'hsd8ke', // docker starter v2 - 'fxis37', // next v2 - '9qputt', // vite + react v2 - 'prp60l', // remix v2 - 'angular', - 'react-ts', - 'rjk9n4zj7m', // static v1 -]; - type CreateBoxProps = ModalContentProps & { collectionId?: string; type?: 'devbox' | 'sandbox'; @@ -73,7 +62,7 @@ export const CreateBox: React.FC = ({ const mobileScreenSize = mediaQuery.matches; const showFeaturedTemplates = type === 'devbox'; - const showEssentialTemplates = type === 'devbox'; + const showCollections = type === 'devbox'; const tabState = useTabState({ orientation: mobileScreenSize ? 'horizontal' : 'vertical', @@ -86,72 +75,33 @@ export const CreateBox: React.FC = ({ const [selectedTemplate, setSelectedTemplate] = useState(); const [searchQuery, setSearchQuery] = useState(''); - const noDevboxesWhenListingSandboxes = (t: TemplateFragment) => - type === 'sandbox' ? !t.sandbox.isV2 : true; - - const essentialState = useEssentialTemplates(showEssentialTemplates); - const officialTemplatesData = useOfficialTemplates(); - const teamTemplatesData = useTeamTemplates({ + const { collections } = useTemplateCollections({ type }); + const { templates: officialTemplates } = useOfficialTemplates({ type }); + const { teamTemplates, recentTemplates } = useTeamTemplates({ teamId: activeTeam, hasLogIn, + type, + }); + + const recentlyUsedTemplates = recentTemplates.slice(0, 3); + const featuredTemplates = useFeaturedTemplates({ + officialTemplates, + recentTemplates, }); - const officialTemplates = - officialTemplatesData.state === 'ready' - ? officialTemplatesData.templates.filter(noDevboxesWhenListingSandboxes) - : []; + const allTemplates = useAllTemplates({ + officialTemplates, + teamTemplates, + collections, + searchQuery, + }); /** * Only show the team templates if the list is populated. */ - const showTeamTemplates = - hasLogIn && - activeTeam && - teamTemplatesData.state === 'ready' && - teamTemplatesData.teamTemplates.length > 0; - const showImportTemplates = hasLogIn && activeTeam && type === 'devbox'; - - const recentlyUsedTemplates = - teamTemplatesData.state === 'ready' - ? teamTemplatesData.recentTemplates.slice(0, 3) - : []; - const hasRecentlyUsedTemplates = recentlyUsedTemplates.length > 0; - - const featuredOfficialTemplates = - officialTemplatesData.state === 'ready' - ? FEATURED_IDS.map( - id => - // If the template is already in recently used, don't add it twice - !recentlyUsedTemplates.find(t => t.sandbox.id === id) && - officialTemplates.find(t => t.sandbox.id === id) - ).filter(Boolean) - : []; - - const featuredTemplates = featuredOfficialTemplates.slice( - 0, - hasRecentlyUsedTemplates ? 6 : 9 - ); - - const teamTemplates = - teamTemplatesData.state === 'ready' - ? teamTemplatesData.teamTemplates.filter(noDevboxesWhenListingSandboxes) - : []; - const officialTemplateIds = officialTemplates.map(t => t.id); - - // For all templates, ensure official and team templates do not create duplications in the list - const allTemplates = [ - ...officialTemplates, - ...teamTemplates.filter(t => !officialTemplateIds.includes(t.id)), - ] - .filter(noDevboxesWhenListingSandboxes) - .filter(t => - searchQuery - ? (t.sandbox.alias || t.sandbox.alias || '') - .toLowerCase() - .includes(searchQuery.toLowerCase()) - : true - ); + const showTeamTemplates = teamTemplates.length > 0; + const showImportTemplates = hasLogIn && activeTeam && type === 'devbox'; useEffect(() => { if (searchQuery) { @@ -364,16 +314,15 @@ export const CreateBox: React.FC = ({ - {showEssentialTemplates && - essentialState.state === 'success' - ? essentialState.essentials.map(essential => ( + {showCollections + ? collections.map(collection => ( trackTabClick(essential.title)} + stopId={slugify(collection.title)} + onClick={() => trackTabClick(collection.title)} > - {essential.title} + {collection.title} )) : null} @@ -497,27 +446,25 @@ export const CreateBox: React.FC = ({ /> - {essentialState.state === 'success' - ? essentialState.essentials.map(essential => ( - - { - selectTemplate(template, essential.title); - }} - onOpenTemplate={template => { - openTemplate(template, essential.title); - }} - /> - - )) - : null} + {collections.map(collection => ( + + { + selectTemplate(template, collection.title); + }} + onOpenTemplate={template => { + openTemplate(template, collection.title); + }} + /> + + ))} )} diff --git a/packages/app/src/app/components/Create/hooks/useAllTemplates.ts b/packages/app/src/app/components/Create/hooks/useAllTemplates.ts new file mode 100644 index 00000000000..79535f5d1aa --- /dev/null +++ b/packages/app/src/app/components/Create/hooks/useAllTemplates.ts @@ -0,0 +1,41 @@ +import { TemplateFragment } from 'app/graphql/types'; +import { TemplateCollection } from '../utils/types'; + +interface UseAllTemplatesParams { + searchQuery?: string; + officialTemplates: TemplateFragment[]; + teamTemplates: TemplateFragment[]; + collections: TemplateCollection[]; +} + +export const useAllTemplates = ({ + officialTemplates, + teamTemplates, + collections, + searchQuery, +}: UseAllTemplatesParams) => { + // Using a map to ensure unique entries for templates + const allTemplatesMap: Map = new Map(); + + officialTemplates.forEach(t => { + allTemplatesMap.set(t.id, t); + }); + + teamTemplates.forEach(t => { + allTemplatesMap.set(t.id, t); + }); + + collections.forEach(c => { + c.templates.forEach(t => { + allTemplatesMap.set(t.id, t); + }); + }); + + return Array.from(allTemplatesMap.values()).filter(t => + searchQuery + ? (t.sandbox.alias || t.sandbox.alias || '') + .toLowerCase() + .includes(searchQuery.toLowerCase()) + : true + ); +}; diff --git a/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts b/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts deleted file mode 100644 index 6d1d5822d9e..00000000000 --- a/packages/app/src/app/components/Create/hooks/useEssentialTemplates.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useEffect, useState } from 'react'; -import { TemplateInfo } from '../utils/types'; -import { getTemplateInfosFromAPI } from '../utils/api'; - -type EssentialsState = - | { - state: 'loading'; - } - | { - state: 'success'; - essentials: TemplateInfo[]; - } - | { - state: 'error'; - error: string; - }; - -export const useEssentialTemplates = (showEssentialTemplates: boolean) => { - const [essentialState, setEssentialState] = useState({ - state: 'loading', - }); - - useEffect(() => { - if (!showEssentialTemplates) { - return; - } - - async function getEssentials() { - try { - const result = await getTemplateInfosFromAPI( - '/api/v1/sandboxes/templates/explore' - ); - - setEssentialState({ - state: 'success', - essentials: result, - }); - } catch { - setEssentialState({ - state: 'error', - error: 'Something went wrong when fetching more templates, sorry!', - }); - } - } - - if (essentialState.state === 'loading') { - getEssentials(); - } - }, [essentialState.state, showEssentialTemplates]); - - return essentialState; -}; diff --git a/packages/app/src/app/components/Create/hooks/useFeaturedTemplates.ts b/packages/app/src/app/components/Create/hooks/useFeaturedTemplates.ts new file mode 100644 index 00000000000..36ec4f7d41a --- /dev/null +++ b/packages/app/src/app/components/Create/hooks/useFeaturedTemplates.ts @@ -0,0 +1,36 @@ +import { TemplateFragment } from 'app/graphql/types'; + +interface UseFeaturedTemplatesParams { + recentTemplates: TemplateFragment[]; + officialTemplates: TemplateFragment[]; +} + +const FEATURED_IDS = [ + 'new', // react + 'vanilla', // js + 'pb6sit', // vue + 'hsd8ke', // docker + 'fxis37', // next + '9qputt', // react (vite + ts) + 'prp60l', // remix + 'angular', // angular + 'rjk9n4zj7m', // html/css +]; + +export const useFeaturedTemplates = ({ + recentTemplates, + officialTemplates, +}: UseFeaturedTemplatesParams) => { + const recentlyUsedTemplates = recentTemplates.slice(0, 3); + + const hasRecentlyUsedTemplates = recentlyUsedTemplates.length > 0; + + const featuredOfficialTemplates = FEATURED_IDS.map( + id => + // If the template is already in recently used, don't add it twice + !recentlyUsedTemplates.find(t => t.sandbox.id === id) && + officialTemplates.find(t => t.sandbox.id === id) + ).filter(Boolean); + + return featuredOfficialTemplates.slice(0, hasRecentlyUsedTemplates ? 6 : 9); +}; diff --git a/packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts b/packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts index 83d3e7760d8..3e9d092a1b9 100644 --- a/packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts +++ b/packages/app/src/app/components/Create/hooks/useOfficialTemplates.ts @@ -5,6 +5,7 @@ import { getTemplateInfosFromAPI } from '../utils/api'; type State = | { state: 'loading'; + templates: TemplateFragment[]; } | { state: 'ready'; @@ -12,14 +13,23 @@ type State = } | { state: 'error'; + templates: TemplateFragment[]; error: string; }; -export const useOfficialTemplates = (): State => { +export const useOfficialTemplates = ({ + type, +}: { + type: 'devbox' | 'sandbox'; +}): State => { const [officialTemplates, setOfficialTemplates] = useState({ state: 'loading', + templates: [], }); + const noDevboxesWhenListingSandboxes = (t: TemplateFragment) => + type === 'sandbox' ? !t.sandbox.isV2 : true; + useEffect(() => { async function fetchTemplates() { try { @@ -29,11 +39,14 @@ export const useOfficialTemplates = (): State => { setOfficialTemplates({ state: 'ready', - templates: response[0].templates, + templates: response[0].templates.filter( + noDevboxesWhenListingSandboxes + ), }); } catch { setOfficialTemplates({ state: 'error', + templates: [], error: 'Something went wrong when fetching more templates, sorry!', }); } diff --git a/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts b/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts index 067c4ec69f4..a56a32537a4 100644 --- a/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts +++ b/packages/app/src/app/components/Create/hooks/useTeamTemplates.ts @@ -1,53 +1,69 @@ import { useQuery } from '@apollo/react-hooks'; import { - ListPersonalTemplatesQuery, - ListPersonalTemplatesQueryVariables, + RecentAndWorkspaceTemplatesQuery, + RecentAndWorkspaceTemplatesQueryVariables, TemplateFragment, } from 'app/graphql/types'; -import { LIST_PERSONAL_TEMPLATES } from '../utils/queries'; +import { FETCH_TEAM_TEMPLATES } from '../utils/queries'; -type State = - | { state: 'loading' } - | { - state: 'ready'; - recentTemplates: TemplateFragment[]; - teamTemplates: TemplateFragment[]; - } - | { - state: 'error'; - error: string; - }; +type BaseState = { + recentTemplates: TemplateFragment[]; + teamTemplates: TemplateFragment[]; +}; -function getTeamTemplates(data: ListPersonalTemplatesQuery, teamId: string) { - return data.me.teams.find(team => team.id === teamId)?.templates || []; -} +type State = BaseState & + ( + | { state: 'idle' } + | { + state: 'loading'; + } + | { + state: 'ready'; + } + | { + state: 'error'; + error: string; + } + ); type UseTeamTemplatesParams = { teamId: string; hasLogIn: boolean; + type: 'devbox' | 'sandbox'; }; export const useTeamTemplates = ({ teamId, hasLogIn, + type, }: UseTeamTemplatesParams): State => { + const skip = !hasLogIn; + + const noDevboxesWhenListingSandboxes = (t: TemplateFragment) => + type === 'sandbox' ? !t.sandbox.isV2 : true; + const { data, error } = useQuery< - ListPersonalTemplatesQuery, - ListPersonalTemplatesQueryVariables - >(LIST_PERSONAL_TEMPLATES, { - /** - * With LIST_PERSONAL_TEMPLATES we're also fetching team templates. We're reusing - * this query here because it has already been preloaded and cached by overmind. We're - * filtering what we need later. - */ - variables: {}, + RecentAndWorkspaceTemplatesQuery, + RecentAndWorkspaceTemplatesQueryVariables + >(FETCH_TEAM_TEMPLATES, { + variables: { teamId }, fetchPolicy: 'cache-and-network', - skip: !hasLogIn, + skip, }); + if (skip) { + return { + state: 'idle', + recentTemplates: [], + teamTemplates: [], + }; + } + if (error) { return { state: 'error', + recentTemplates: [], + teamTemplates: [], error: error.message, }; } @@ -58,14 +74,18 @@ export const useTeamTemplates = ({ if (typeof data?.me === 'undefined') { return { state: 'loading', + recentTemplates: [], + teamTemplates: [], }; } - const teamTemplates = getTeamTemplates(data, teamId); - return { state: 'ready', - recentTemplates: data.me.recentlyUsedTemplates, - teamTemplates, + recentTemplates: [], // data.me.recentlyUsedTemplates.filter( + // noDevboxesWhenListingSandboxes + // ), + teamTemplates: data.me.team.templates.filter( + noDevboxesWhenListingSandboxes + ), }; }; diff --git a/packages/app/src/app/components/Create/hooks/useTemplateCollections.ts b/packages/app/src/app/components/Create/hooks/useTemplateCollections.ts new file mode 100644 index 00000000000..23c85115406 --- /dev/null +++ b/packages/app/src/app/components/Create/hooks/useTemplateCollections.ts @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import { TemplateCollection } from '../utils/types'; +import { getTemplateInfosFromAPI } from '../utils/api'; + +type TemplateCollectionsState = + | { + state: 'idle'; + collections: TemplateCollection[]; + } + | { + state: 'loading'; + collections: TemplateCollection[]; + } + | { + state: 'success'; + collections: TemplateCollection[]; + } + | { + state: 'error'; + error: string; + collections: TemplateCollection[]; + }; + +export const useTemplateCollections = ({ + type, +}: { + type: 'devbox' | 'sandbox'; +}) => { + const [collectionsState, setCollectionsState] = useState< + TemplateCollectionsState + >({ + state: type === 'devbox' ? 'loading' : 'idle', + collections: [], + }); + + useEffect(() => { + async function fetchData() { + try { + const result = await getTemplateInfosFromAPI( + '/api/v1/sandboxes/templates/explore' + ); + + setCollectionsState({ + state: 'success', + collections: result, + }); + } catch { + setCollectionsState({ + state: 'error', + error: 'Something went wrong when fetching more templates, sorry!', + collections: [], + }); + } + } + + if (collectionsState.state === 'loading') { + fetchData(); + } + }, [collectionsState.state]); + + return collectionsState; +}; diff --git a/packages/app/src/app/components/Create/utils/api.ts b/packages/app/src/app/components/Create/utils/api.ts index 266f325e786..bafccc28e45 100644 --- a/packages/app/src/app/components/Create/utils/api.ts +++ b/packages/app/src/app/components/Create/utils/api.ts @@ -1,8 +1,8 @@ import { TemplateType } from '@codesandbox/common/lib/templates'; import { isServer } from '@codesandbox/common/lib/templates/helpers/is-server'; -import { TemplateInfo } from './types'; +import { TemplateCollection } from './types'; -interface IExploreTemplate { +interface ExploreTemplateAPIResponse { title: string; sandboxes: { id: string; @@ -36,8 +36,8 @@ interface IExploreTemplate { } const mapAPIResponseToTemplateInfo = ( - exploreTemplate: IExploreTemplate -): TemplateInfo => ({ + exploreTemplate: ExploreTemplateAPIResponse +): TemplateCollection => ({ key: exploreTemplate.title, title: exploreTemplate.title, templates: exploreTemplate.sandboxes.map(sandbox => ({ @@ -75,10 +75,14 @@ const mapAPIResponseToTemplateInfo = ( })), }); -export const getTemplateInfosFromAPI = (url: string): Promise => +export const getTemplateInfosFromAPI = ( + url: string +): Promise => fetch(url) .then(res => res.json()) - .then((body: IExploreTemplate[]) => body.map(mapAPIResponseToTemplateInfo)); + .then((body: ExploreTemplateAPIResponse[]) => + body.map(mapAPIResponseToTemplateInfo) + ); type ValidateRepositoryDestinationFn = ( destination: string diff --git a/packages/app/src/app/components/Create/utils/queries.ts b/packages/app/src/app/components/Create/utils/queries.ts index a77e23581f9..7d27a3bc92e 100644 --- a/packages/app/src/app/components/Create/utils/queries.ts +++ b/packages/app/src/app/components/Create/utils/queries.ts @@ -31,38 +31,14 @@ const TEMPLATE_FRAGMENT = gql` } `; -export const LIST_PERSONAL_TEMPLATES = gql` - query ListPersonalTemplates { +export const FETCH_TEAM_TEMPLATES = gql` + query RecentAndWorkspaceTemplates($teamId: UUID4) { me { - templates { - ...Template - } - recentlyUsedTemplates { ...Template - - sandbox { - git { - id - username - commitSha - path - repo - branch - } - } - } - - bookmarkedTemplates { - ...Template } - teams { - id - name - bookmarkedTemplates { - ...Template - } + team(id: $teamId) { templates { ...Template } diff --git a/packages/app/src/app/components/Create/utils/types.ts b/packages/app/src/app/components/Create/utils/types.ts index 81923ff19aa..313fa7663a9 100644 --- a/packages/app/src/app/components/Create/utils/types.ts +++ b/packages/app/src/app/components/Create/utils/types.ts @@ -6,7 +6,7 @@ export type CreateSandboxParams = { createRepo?: boolean; }; -export interface TemplateInfo { +export interface TemplateCollection { title?: string; key: string; templates: TemplateFragment[]; diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts index 974da1a106b..513f529c9da 100644 --- a/packages/app/src/app/graphql/types.ts +++ b/packages/app/src/app/graphql/types.ts @@ -72,6 +72,14 @@ export type RootQueryType = { */ branchByName: Branch; curatedAlbums: Array; + /** + * Check for an active live session + * + * Accessible to members of the content's workspace and non-editor guests that have already + * joined the session. Prospective guests should use the mutation `joinLiveSession` with the + * session's ID instead. + */ + getLiveSession: Maybe; /** Get git repo and related V1 sandboxes */ git: Maybe; /** @@ -173,6 +181,10 @@ export type RootQueryTypeBranchByNameArgs = { team: InputMaybe; }; +export type RootQueryTypeGetLiveSessionArgs = { + vmId: Scalars['ID']; +}; + export type RootQueryTypeGitArgs = { branch: Scalars['String']; path: Scalars['String']; @@ -1171,6 +1183,86 @@ export enum GitProvider { Github = 'GITHUB', } +/** + * Live session started by an editor of a cloud sandbox for the benefit of guest users + * + * In a live session, there are editors (people who naturally have write access to the content) + * and guests (a superset that includes editors and other individuals who join the live session). + * Editors always have write permission, while non-editor guests have permissions determined by + * the default level-of-access or an individual override for that particular user. See mutations + * `setLiveSessionDefaultPermission` and `setLiveSessionGuestPermission` for more information. + */ +export type LiveSession = { + __typename?: 'LiveSession'; + /** + * Default level of access for non-editor guests + * + * Editors of the cloud sandbox can change this using the `setLiveSessionDefaultPermission` + * mutation. + */ + defaultPermission: LiveSessionPermission; + /** + * Guests of a live session + * + * This list includes all non-editor guests, regardless of their level of access. Users who have + * natural write access to the content will not appear here. + */ + guests: Array; + /** User that started the live session */ + host: Maybe; + /** Session identifier, used for client-side caching */ + id: Scalars['ID']; + /** Timestamp of the time when the live session opened (ISO 8601) */ + startedAt: Scalars['String']; + /** + * Timestamp of the time when the live session ended (ISO 8601) + * + * In practice, this field will only be visible in response to a `stopLiveSession` mutation or + * as the final message of a `liveSessionEvents` subscription. + */ + stoppedAt: Maybe; + /** ID of the related sandbox VM */ + vmId: Scalars['ID']; +}; + +/** Level of access for a live session */ +export enum LiveSessionPermission { + Read = 'READ', + Write = 'WRITE', +} + +/** + * Guest of a live session + * + * This record represents a non-editor guest of a live session. + */ +export type LiveSessionGuest = { + __typename?: 'LiveSessionGuest'; + /** URL of the user's avatar image */ + avatarUrl: Scalars['String']; + /** Level of access for the guest */ + permission: LiveSessionPermission; + /** CodeSandbox user ID */ + userId: Scalars['ID']; + /** CodeSandbox username */ + username: Scalars['String']; +}; + +/** + * Hose of a live session + * + * This user initially started the live session. + */ +export type LiveSessionHost = { + __typename?: 'LiveSessionHost'; + /** URL of the user's avatar image */ + avatarUrl: Scalars['String']; + /** CodeSandbox user ID */ + userId: Scalars['ID']; + /** CodeSandbox username */ + username: Scalars['String']; +}; + /** Details about a repository as it appears on GitHub (Open API `repository`) */ export type GithubRepo = { __typename?: 'GithubRepo'; @@ -1705,6 +1797,13 @@ export type RootMutationType = { inviteToTeam: Team; /** Invite someone to a team via email */ inviteToTeamViaEmail: Scalars['String']; + /** + * Join an existing live session + * + * Accessible to non-editor guests for content that has an existing live session. For editors, + * this mutation is a no-op. Returns an error if no live session exists. + */ + joinLiveSession: LiveSession; /** Leave a team */ leaveTeam: Scalars['String']; /** Make templates from sandboxes */ @@ -1781,6 +1880,31 @@ export type RootMutationType = { setBranchProtection: Branch; /** Set the default authorization for any new members joining this workspace */ setDefaultTeamMemberAuthorization: WorkspaceSandboxSettings; + /** + * Set the default level of access for guests in a live session + * + * Accessible to editors of the underlying content. + * + * With a default permission of `READ`, guests join with the ability to read the code and watch + * changes taking place without making changes of their own, like a classroom mode. With `WRITE`, + * all new guests will be able to make changes immediately. + * + * Individual guest permissions can be overridden using the `setLiveSessionGuestPermission` + * mutation. Changing the default permission does not reset any individual guest permissions + * set using the `setLiveSessionGuestPermission` mutation. It also does not affect editors (those + * who naturally have access to the content). + */ + setLiveSessionDefaultPermission: LiveSession; + /** + * Set the level of access for a specific guest in a live session + * + * Accessible to editors of the underlying content. + * + * If an individual guest should have a level of access different than the default permission + * set using the `setLiveSessionDefaultPermission` mutation, this mutation allows targeted + * access. It has no effect on editors (those who naturally have access to the content). + */ + setLiveSessionGuestPermission: LiveSession; setPreventSandboxesExport: Array; setPreventSandboxesLeavingWorkspace: Array; /** set sandbox always on status */ @@ -1797,6 +1921,22 @@ export type RootMutationType = { setTeamName: Team; setWorkspaceSandboxSettings: WorkspaceSandboxSettings; softCancelSubscription: ProSubscription; + /** + * Begin a new live session for a running VM + * + * Accessible to editors of the underlying content as long as live sessions are allowed by the + * content and its workspace. + * + * The live session will be automatically stopped a few minutes after the VM session ends, or + * immediately after calling the `stopLiveSession` mutation. + */ + startLiveSession: LiveSession; + /** + * Immediately close a live session + * + * Accessible to editors of the underlying content. + */ + stopLiveSession: LiveSession; /** Unbookmark a template */ unbookmarkTemplate: Maybe