Skip to content

Commit

Permalink
Feature: GitHub autofork url (#4704)
Browse files Browse the repository at this point in the history
* a much much simpler github autoforking

* Server: endpoint to default branch of a repo

* moving PubSub out of dispatch

* Update editor-update.spec.tsx

* add option for shorter clone URL

* a GithubRepositoryCloneFlow component to orchestrate the github load flow

* moving code to file

* Update editor.tsx

* new updatePubSubAtomFromOutsideReact

* extracted SignInButton

* extracted AuthenticateWithGithubButton

* progress on the clone overlay

* v1 of the actual working clone flow

* copy fixes and not showing double overlay during github load

* dispatching setGithubState in editor.tsx

* making secondaryButton optional

* using Dialog for the github clone flow

* Do not create placeholder Storyboard file while loading from Github

* moving getGithubRepoToLoad to file

* undoing unrelated pubsub atom changes

* undoing unrelated editor.tsx changes

* undoing more changes that ended up being irrelevant

* moving FullScreenOverlay to dedicated file

* actually undoing all these FullScreenOverlay changes

* Update github-clone-overlay.tsx

* renaming GithubRepositoryCloneFlow

* using isLoggedIn

* Update github-repository-clone-flow.tsx

* moving useOnClickAuthenticateWithGithub to its own file

* Update actions.tsx

* Update github-repository-clone-flow.tsx
  • Loading branch information
balazsbajorics authored Jan 15, 2024
1 parent 54da772 commit 15cd213
Show file tree
Hide file tree
Showing 29 changed files with 376 additions and 71 deletions.
1 change: 1 addition & 0 deletions editor/src/components/canvas/ui-jsx.test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ export async function renderTestEditorWithModel(
themeConfig: 'system',
githubState: {
authenticated: false,
gitRepoToLoad: null,
},
},
workers: workers,
Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/editor/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ export interface SetLoginState {

export interface SetGithubState {
action: 'SET_GITHUB_STATE'
githubState: GithubState
githubState: Partial<GithubState>
}

export interface SetUserConfiguration {
Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/editor/actions/action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,7 @@ export function setLoginState(loginState: LoginState): SetLoginState {
}
}

export function setGithubState(githubState: GithubState): SetGithubState {
export function setGithubState(githubState: Partial<GithubState>): SetGithubState {
return {
action: 'SET_GITHUB_STATE',
githubState: githubState,
Expand Down
12 changes: 10 additions & 2 deletions editor/src/components/editor/actions/actions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,11 @@ describe('UPDATE_FROM_WORKER', () => {
versionNumberOfAppJS - 1,
),
])
const updatedEditorState = UPDATE_FNS.UPDATE_FROM_WORKER(updateToCheck, startingEditorState)
const updatedEditorState = UPDATE_FNS.UPDATE_FROM_WORKER(
updateToCheck,
startingEditorState,
defaultUserState,
)

// Check that the model hasn't changed, because of the stale revised time.
expect(updatedEditorState).toBe(startingEditorState)
Expand Down Expand Up @@ -1041,7 +1045,11 @@ describe('UPDATE_FROM_WORKER', () => {
versionNumberOfAppJS + 1,
),
])
const updatedEditorState = UPDATE_FNS.UPDATE_FROM_WORKER(updateToCheck, startingEditorState)
const updatedEditorState = UPDATE_FNS.UPDATE_FROM_WORKER(
updateToCheck,
startingEditorState,
defaultUserState,
)

// Get the same values that we started with but from the updated editor state.
const updatedStoryboardVersionNumberFromState = unsafeGet(
Expand Down
26 changes: 22 additions & 4 deletions editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1516,7 +1516,11 @@ function createStoryboardFileIfMainComponentPresent(

function createStoryboardFileWithPlaceholderContents(
projectContents: ProjectContentTreeRoot,
createPlaceholder: 'create-placeholder' | 'skip-creating-placeholder',
): ProjectContentTreeRoot {
if (createPlaceholder === 'skip-creating-placeholder') {
return projectContents
}
const updatedProjectContents = addFileToProjectContents(
projectContents,
StoryboardFilePath,
Expand All @@ -1527,6 +1531,7 @@ function createStoryboardFileWithPlaceholderContents(

export function createStoryboardFileIfNecessary(
projectContents: ProjectContentTreeRoot,
createPlaceholder: 'create-placeholder' | 'skip-creating-placeholder',
): ProjectContentTreeRoot {
const storyboardFile = getProjectFileByFilePath(projectContents, StoryboardFilePath)
if (storyboardFile != null) {
Expand All @@ -1536,7 +1541,7 @@ export function createStoryboardFileIfNecessary(
return (
createStoryboardFileIfRemixProject(projectContents) ??
createStoryboardFileIfMainComponentPresent(projectContents) ??
createStoryboardFileWithPlaceholderContents(projectContents)
createStoryboardFileWithPlaceholderContents(projectContents, createPlaceholder)
)
}

Expand Down Expand Up @@ -3651,7 +3656,11 @@ export const UPDATE_FNS = {
},
}
},
UPDATE_FROM_WORKER: (action: UpdateFromWorker, editor: EditorModel): EditorModel => {
UPDATE_FROM_WORKER: (
action: UpdateFromWorker,
editor: EditorModel,
userState: UserState,
): EditorModel => {
let workingProjectContents: ProjectContentTreeRoot = editor.projectContents
let anyParsedUpdates: boolean = false

Expand Down Expand Up @@ -3682,7 +3691,13 @@ export const UPDATE_FNS = {
}
return {
...editor,
projectContents: createStoryboardFileIfNecessary(workingProjectContents),
projectContents: createStoryboardFileIfNecessary(
workingProjectContents,
// If we are in the process of cloning a Github repository, do not create placeholder Storyboard
userState.githubState.gitRepoToLoad != null
? 'skip-creating-placeholder'
: 'create-placeholder',
),
canvas: {
...editor.canvas,
canvasContentInvalidateCount: anyParsedUpdates
Expand Down Expand Up @@ -4747,7 +4762,10 @@ export const UPDATE_FNS = {
SET_GITHUB_STATE: (action: SetGithubState, userState: UserState): UserState => {
return {
...userState,
githubState: action.githubState,
githubState: {
...userState.githubState,
...action.githubState,
},
}
},
SET_USER_CONFIGURATION: (action: SetUserConfiguration, userState: UserState): UserState => {
Expand Down
2 changes: 2 additions & 0 deletions editor/src/components/editor/editor-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { EditorModes } from './editor-modes'
import { checkIsMyProject } from './store/collaborative-editing'
import { useDataThemeAttributeOnBody } from '../../core/commenting/comment-hooks'
import { CollaborationStateUpdater } from './store/collaboration-state'
import { GithubRepositoryCloneFlow } from '../github/github-repository-clone-flow'

const liveModeToastId = 'play-mode-toast'

Expand Down Expand Up @@ -464,6 +465,7 @@ export const EditorComponentInner = React.memo((props: EditorProps) => {
</SimpleFlexRow>
</SimpleFlexColumn>
<ModalComponent />
<GithubRepositoryCloneFlow />
<LockedOverlay />
</SimpleFlexRow>
<EditorCommon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ function createEditorStore(
themeConfig: 'light',
githubState: {
authenticated: false,
gitRepoToLoad: null,
},
},
workers: new UtopiaTsWorkersImplementation(
Expand Down
13 changes: 11 additions & 2 deletions editor/src/components/editor/store/editor-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export function emptyUserConfiguration(): UserConfiguration {

export interface GithubState {
authenticated: boolean
gitRepoToLoad: GithubRepoWithBranch | null
}

export interface UserState extends UserConfiguration {
Expand All @@ -282,7 +283,7 @@ export interface GithubListBranches {
export interface GithubLoadBranch {
name: 'loadBranch'
githubRepo: GithubRepo
branchName: string
branchName: string | null
}

export interface GithubLoadRepositories {
Expand Down Expand Up @@ -399,6 +400,7 @@ export const defaultUserState: UserState = {
themeConfig: 'system',
githubState: {
authenticated: false,
gitRepoToLoad: null,
},
}

Expand Down Expand Up @@ -1164,6 +1166,8 @@ export interface GithubRepo {
repository: string
}

export type GithubRepoWithBranch = GithubRepo & { branch: string | null }

export function githubRepoFullName(repo: GithubRepo | null): string | null {
if (repo == null) {
return null
Expand Down Expand Up @@ -2967,8 +2971,13 @@ const defaultDependencies = Utils.mapArrayToDictionary(

export const defaultIndexHtmlFilePath = 'public/index.html'

export const EmptyPackageJson = {
name: 'utopia-project',
version: '0.1.0',
}

export const DefaultPackageJson = {
name: 'Utopia Project',
name: 'utopia-project',
version: '0.1.0',
utopia: {
'main-ui': StoryboardFilePath.slice(1),
Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/editor/store/editor-update.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ describe('updating package.json', () => {
expect(packageJsonFile.fileContents).toMatchInlineSnapshot(`
Object {
"code": "{
\\"name\\": \\"Utopia Project\\",
\\"name\\": \\"utopia-project\\",
\\"version\\": \\"0.1.0\\",
\\"utopia\\": {
\\"main-ui\\": \\"utopia/storyboard.js\\",
Expand Down
2 changes: 1 addition & 1 deletion editor/src/components/editor/store/editor-update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export function runSimpleLocalEditorAction(
case 'REMOVE_FILE_CONFLICT':
return UPDATE_FNS.REMOVE_FILE_CONFLICT(action, state)
case 'UPDATE_FROM_WORKER':
return UPDATE_FNS.UPDATE_FROM_WORKER(action, state)
return UPDATE_FNS.UPDATE_FROM_WORKER(action, state, userState)
case 'UPDATE_FROM_CODE_EDITOR':
return UPDATE_FNS.UPDATE_FROM_CODE_EDITOR(
action,
Expand Down
1 change: 1 addition & 0 deletions editor/src/components/editor/store/store-hook.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const initialEditorStore: EditorStorePatched = {
loginState: notLoggedIn,
githubState: {
authenticated: false,
gitRepoToLoad: null,
},
},
workers: null as any,
Expand Down
179 changes: 179 additions & 0 deletions editor/src/components/github/github-repository-clone-flow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import React from 'react'
import { GithubOperations } from '../../core/shared/github/operations'
import { forceNotNull } from '../../core/shared/optional-utils'
import { NO_OP } from '../../core/shared/utils'
import { totallyEmptyDefaultProject } from '../../sample-projects/sample-project-utils'
import invariant from '../../third-party/remix/invariant'
import { useOnClickAuthenticateWithGithub } from '../../utils/github-auth-hooks'
import { Dialog, FormButton } from '../../uuiui'
import { isLoggedIn, type EditorDispatch } from '../editor/action-types'
import { setGithubState } from '../editor/actions/action-creators'
import { useDispatch } from '../editor/store/dispatch-context'
import { type EditorStorePatched, type GithubRepoWithBranch } from '../editor/store/editor-state'
import { Substores, useEditorState, useRefEditorState } from '../editor/store/store-hook'
import { onClickSignIn } from '../titlebar/title-bar'

export const LoadActionsDispatched = 'loadActionDispatched'

export const GithubRepositoryCloneFlow = React.memo(() => {
const githubRepo = useEditorState(
Substores.userState,
(store) => store.userState.githubState.gitRepoToLoad,
'GithubRepositoryCloneFlow gitRepoToLoad',
)
const userLoggedIn = useEditorState(
Substores.userState,
(store) => isLoggedIn(store.userState.loginState),
'GithubRepositoryCloneFlow userLoggedIn',
)

const githubAuthenticated = useEditorState(
Substores.userState,
(store) => store.userState.githubState.authenticated,
'GithubRepositoryCloneFlow githubAuthenticated',
)

const onClickAuthenticateWithGithub = useOnClickAuthenticateWithGithub()

if (githubRepo == null) {
// we don't want to load anything, so just return null to hide this overlay
return null
}
if (!userLoggedIn) {
// we want to prompt the user to log in
return (
<Dialog
title='Not Signed In'
content={<>You need to be signed in to be able to clone a repository from GitHub</>}
closeCallback={NO_OP}
defaultButton={
<FormButton primary onClick={onClickSignIn}>
Sign In To Utopia
</FormButton>
}
/>
)
}
if (!githubAuthenticated) {
// we want to prompt the user to log authenticate their github
return (
<Dialog
title='Connect to Github'
content={
<>
You need to connect Utopia with your Github account to be able to access Github
repositories
</>
}
closeCallback={NO_OP}
defaultButton={
<FormButton primary onClick={onClickAuthenticateWithGithub}>
Authenticate with Github
</FormButton>
}
/>
)
}

// The GitClonePseudoElement triggers the actual repo cloning
return <GitClonePseudoElement githubRepo={githubRepo} />
})

// The git repo clone flow is initiated from the URL, which means we only ever want to do it once per editor load
let didWeInitiateGitRepoDownloadSinceTheEditorLoaded = false

async function cloneGithubRepo(
dispatch: EditorDispatch,
storeRef: { current: EditorStorePatched },
githubRepo: GithubRepoWithBranch,
) {
if (didWeInitiateGitRepoDownloadSinceTheEditorLoaded) {
return
}
didWeInitiateGitRepoDownloadSinceTheEditorLoaded = true
const projectName = `${githubRepo.owner}-${githubRepo.repository}`

const githubBranch = githubRepo.branch
// Obtain a projectID from the server, and save an empty initial project
storeRef.current.persistence.createNew(projectName, totallyEmptyDefaultProject())
const loadActionDispatchedByPersistenceMachine =
await awaitLoadActionDispatchedByPersistenceMachine()
const createdProjectID = loadActionDispatchedByPersistenceMachine.projectId
await GithubOperations.updateProjectWithBranchContent(
storeRef.current.workers,
dispatch,
forceNotNull('Should have a project ID by now.', createdProjectID),
githubRepo,
githubBranch,
false,
[],
storeRef.current.builtInDependencies,
{}, // Assuming a totally empty project (that is being saved probably parallel to this operation, hopefully not causing any race conditions)
)

// at this point we can assume the repo is loaded and we can finally hide the overlay
dispatch([
setGithubState({
gitRepoToLoad: null,
}),
])

// TODO make sure the EditorState knows we have a github repo connected!!!
}

const GitClonePseudoElement = React.memo((props: { githubRepo: GithubRepoWithBranch }) => {
const { githubRepo } = props
const dispatch = useDispatch()

const editorStoreRef = useRefEditorState((store) => store)

React.useEffect(() => {
void cloneGithubRepo(dispatch, editorStoreRef, githubRepo)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

// The GitClonePseudoElement's sole job is to call cloneGithubRepo in a useEffect.
// I pulled it to a dedicated component so it's purpose remains clear and this useEffect doesn't get lost in the noise
return null
})

function awaitLoadActionDispatchedByPersistenceMachine(): Promise<{ projectId: string }> {
invariant(
PubSub.countSubscriptions(LoadActionsDispatched) === 0,
'At this point, awaitLoadActionDispatchedByPersistenceMachine should have zero listeners',
)
return new Promise((resolve, reject) => {
const listener = (message: string, data: { projectId: string }) => {
PubSub.unsubscribe(listener)
resolve(data)
}
PubSub.subscribe(LoadActionsDispatched, listener)
})
}

export function getGithubRepoToLoad(urlSearchParams: string): GithubRepoWithBranch | null {
const urlParams = new URLSearchParams(urlSearchParams)
const githubBranch = urlParams.get('github_branch')

const githubCloneUrl = urlParams.get('clone')
if (githubCloneUrl != null) {
const splitGitRepoUrl = githubCloneUrl.split('/')
return {
owner: splitGitRepoUrl[0],
repository: splitGitRepoUrl[1],
branch: githubBranch,
}
}

const githubOwner = urlParams.get('github_owner')
const githubRepo = urlParams.get('github_repo')
if (githubOwner != null && githubRepo != null) {
return {
owner: githubOwner,
repository: githubRepo,
branch: githubBranch,
}
}

return null
}
Loading

0 comments on commit 15cd213

Please sign in to comment.