Skip to content

Commit

Permalink
Remix: fork projects (#4910)
Browse files Browse the repository at this point in the history
* fork project link

* add forking to state

* fork project with url param

* check projectId not projectName
  • Loading branch information
ruggi authored Feb 15, 2024
1 parent 7893ec4 commit d1c0dcb
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 13 deletions.
6 changes: 6 additions & 0 deletions editor/src/components/editor/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,11 @@ export interface AddToast {
toast: Notice
}

export interface SetForking {
action: 'SET_FORKING'
forking: boolean
}

export interface RemoveToast {
action: 'REMOVE_TOAST'
id: string
Expand Down Expand Up @@ -1251,6 +1256,7 @@ export type EditorAction =
| UpdateImportsFromCollaborationUpdate
| UpdateCodeFromCollaborationUpdate
| SetCommentFilterMode
| SetForking

export type DispatchPriority =
| 'everyone'
Expand Down
8 changes: 8 additions & 0 deletions editor/src/components/editor/actions/action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ import type {
UpdateImportsFromCollaborationUpdate,
UpdateCodeFromCollaborationUpdate,
SetCommentFilterMode,
SetForking,
} from '../action-types'
import type { InsertionSubjectWrapper, Mode } from '../editor-modes'
import { EditorModes, insertionSubject } from '../editor-modes'
Expand Down Expand Up @@ -518,6 +519,13 @@ export function showToast(toastContent: Notice): AddToast {
return addToast(toastContent)
}

export function setForking(forking: boolean): SetForking {
return {
action: 'SET_FORKING',
forking: forking,
}
}

let selectionControlTimer: any // TODO maybe this should live inside the editormodel
export function hideAndShowSelectionControls(dispatch: EditorDispatch): void {
dispatch([CanvasActions.setSelectionControlsVisibility(false)], 'canvas')
Expand Down
1 change: 1 addition & 0 deletions editor/src/components/editor/actions/action-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export function isTransientAction(action: EditorAction): boolean {
case 'TRUNCATE_HISTORY':
case 'UPDATE_PROJECT_SERVER_STATE':
case 'SET_COMMENT_FILTER_MODE':
case 'SET_FORKING':
return true

case 'TRUE_UP_ELEMENTS':
Expand Down
8 changes: 8 additions & 0 deletions editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ import type {
UpdateImportsFromCollaborationUpdate,
UpdateCodeFromCollaborationUpdate,
SetCommentFilterMode,
SetForking,
} from '../action-types'
import { isLoggedIn } from '../action-types'
import type { Mode } from '../editor-modes'
Expand Down Expand Up @@ -912,6 +913,7 @@ export function restoreEditorState(
filesModifiedByAnotherUser: currentEditor.filesModifiedByAnotherUser,
activeFrames: currentEditor.activeFrames,
commentFilterMode: currentEditor.commentFilterMode,
forking: currentEditor.forking,
}
}

Expand Down Expand Up @@ -2074,6 +2076,12 @@ export const UPDATE_FNS = {
ADD_TOAST: (action: AddToast, editor: EditorModel): EditorModel => {
return addToastToState(editor, action.toast)
},
SET_FORKING: (action: SetForking, editor: EditorModel): EditorModel => {
return {
...editor,
forking: action.forking,
}
},
UPDATE_GITHUB_OPERATIONS: (action: UpdateGithubOperations, editor: EditorModel): EditorModel => {
const operations = [...editor.githubOperations]
switch (action.type) {
Expand Down
45 changes: 35 additions & 10 deletions editor/src/components/editor/editor-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,28 @@ import { GithubRepositoryCloneFlow } from '../github/github-repository-clone-flo
import { getPermissions } from './store/permissions'
import { CommentMaintainer } from '../../core/commenting/comment-maintainer'
import { useIsLoggedIn, useLiveblocksConnectionListener } from '../../core/shared/multiplayer-hooks'
import { ForkSearchParamKey, ProjectForkFlow } from './project-fork-flow'

const liveModeToastId = 'play-mode-toast'

function pushProjectURLToBrowserHistory(projectId: string, projectName: string): void {
function pushProjectURLToBrowserHistory(
projectId: string,
projectName: string,
forking: boolean,
): void {
// Make sure we don't replace the query params
const queryParams = window.top?.location.search
const queryParams = new URLSearchParams(window.top?.location.search)
if (forking) {
// …but if it's forking, remove the fork param
queryParams.delete(ForkSearchParamKey)
}

const queryParamsStr = queryParams.size > 0 ? `?${queryParams.toString()}` : ''

const projectURL = projectURLForProject(projectId, projectName)
const title = `Utopia ${projectName}`
window.top?.history.pushState({}, title, `${projectURL}${queryParams}`)

window.top?.history.pushState({}, title, `${projectURL}${queryParamsStr}`)
}

export interface EditorProps {}
Expand Down Expand Up @@ -333,15 +346,17 @@ export const EditorComponentInner = React.memo((props: EditorProps) => {
document.title = projectName + ' - Utopia'
}, [projectName])

const forking = useEditorState(Substores.restOfEditor, (store) => store.editor.forking, '')

React.useEffect(() => {
if (IS_BROWSER_TEST_DEBUG) {
return
}
if (projectId != null) {
pushProjectURLToBrowserHistory(projectId, projectName)
pushProjectURLToBrowserHistory(projectId, projectName, forking)
;(window as any).utopiaProjectID = projectId
}
}, [projectName, projectId])
}, [projectName, projectId, forking])

const onClosePreview = React.useCallback(
() => dispatch([EditorActions.setPanelVisibility('preview', false)]),
Expand Down Expand Up @@ -478,6 +493,7 @@ export const EditorComponentInner = React.memo((props: EditorProps) => {
</SimpleFlexColumn>
<ModalComponent />
<GithubRepositoryCloneFlow />
<ProjectForkFlow />
<LockedOverlay />
</SimpleFlexRow>
<EditorCommon
Expand Down Expand Up @@ -626,15 +642,21 @@ const LockedOverlay = React.memo(() => {
const githubOperations = useEditorState(
Substores.github,
(store) => store.editor.githubOperations.filter((op) => githubOperationLocksEditor(op)),
'EditorComponentInner githubOperations',
'LockedOverlay githubOperations',
)

const editorLocked = React.useMemo(() => githubOperations.length > 0, [githubOperations])

const refreshingDependencies = useEditorState(
Substores.restOfEditor,
(store) => store.editor.refreshingDependencies,
'EditorComponentInner refreshingDependencies',
'LockedOverlay refreshingDependencies',
)

const forking = useEditorState(
Substores.restOfEditor,
(store) => store.editor.forking,
'LockedOverlay forking',
)

const anim = keyframes`
Expand All @@ -647,8 +669,8 @@ const LockedOverlay = React.memo(() => {
`

const locked = React.useMemo(() => {
return editorLocked || refreshingDependencies
}, [editorLocked, refreshingDependencies])
return editorLocked || refreshingDependencies || forking
}, [editorLocked, refreshingDependencies, forking])

const dialogContent = React.useMemo((): string | null => {
if (refreshingDependencies) {
Expand All @@ -657,8 +679,11 @@ const LockedOverlay = React.memo(() => {
if (githubOperations.length > 0) {
return `${githubOperationPrettyName(githubOperations[0])}…`
}
if (forking) {
return 'Forking project…'
}
return null
}, [refreshingDependencies, githubOperations])
}, [refreshingDependencies, githubOperations, forking])

if (!locked) {
return null
Expand Down
7 changes: 6 additions & 1 deletion editor/src/components/editor/persistence-hooks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React from 'react'
import { useRefEditorState } from './store/store-hook'
import { persistentModelFromEditorModel } from './store/editor-state'
import { useDispatch } from './store/dispatch-context'
import { setForking } from './actions/action-creators'

export function useTriggerForkProject(): () => void {
const dispatch = useDispatch()

const storeRef = useRefEditorState((store) => store)
return React.useCallback(async () => {
dispatch([setForking(true)])
const store = storeRef.current
store.persistence.fork(store.editor.projectName, persistentModelFromEditorModel(store.editor))
}, [storeRef])
}, [storeRef, dispatch])
}
2 changes: 2 additions & 0 deletions editor/src/components/editor/persistence/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { notice } from '../../common/notice'
import type { EditorAction, EditorDispatch } from '../action-types'
import {
setForkedFromProjectID,
setForking,
setProjectID,
setProjectName,
showToast,
Expand Down Expand Up @@ -89,6 +90,7 @@ export class PersistenceMachine {
this.queuedActions.push(setForkedFromProjectID(state.context.projectId!))
this.queuedActions.push(setProjectName(state.context.project!.name))
this.queuedActions.push(showToast(notice('Project successfully forked!')))
this.queuedActions.push(setForking(false))
}

const updateFileActions = event.downloadAssetsResult.filesWithFileNames.map(
Expand Down
28 changes: 28 additions & 0 deletions editor/src/components/editor/project-fork-flow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { useTriggerForkProject } from './persistence-hooks'
import { Substores, useEditorState } from './store/store-hook'

export const ForkSearchParamKey = 'fork'

export const ProjectForkFlow = React.memo(() => {
const searchParams = new URLSearchParams(window.location.search)
const shouldFork = searchParams.get(ForkSearchParamKey) === 'true'

const projectId = useEditorState(
Substores.restOfEditor,
(store) => store.editor.id,
'ProjectForkFlow projectId',
)

const triggerForkProject = useTriggerForkProject()

React.useEffect(() => {
if (shouldFork && projectId != null) {
triggerForkProject()
}
}, [shouldFork, triggerForkProject, projectId])

return null
})

ProjectForkFlow.displayName = 'ProjectForkFlow'
7 changes: 6 additions & 1 deletion editor/src/components/editor/store/editor-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,7 @@ export interface EditorState {
filesModifiedByAnotherUser: Array<string>
activeFrames: ActiveFrame[]
commentFilterMode: CommentFilterMode
forking: boolean
}

export function editorState(
Expand Down Expand Up @@ -1540,6 +1541,7 @@ export function editorState(
filesModifiedByAnotherUser: Array<string>,
activeFrames: ActiveFrame[],
commentFilterMode: CommentFilterMode,
forking: boolean,
): EditorState {
return {
id: id,
Expand Down Expand Up @@ -1609,7 +1611,7 @@ export function editorState(
forceParseFiles: forceParseFiles,
allElementProps: allElementProps,
currentAllElementProps: currentAllElementProps,
variablesInScope,
variablesInScope: variablesInScope,
currentVariablesInScope: currentVariablesInScope,
githubSettings: githubSettings,
imageDragSessionState: imageDragSessionState,
Expand All @@ -1621,6 +1623,7 @@ export function editorState(
filesModifiedByAnotherUser: filesModifiedByAnotherUser,
activeFrames: activeFrames,
commentFilterMode: commentFilterMode,
forking: forking,
}
}

Expand Down Expand Up @@ -2518,6 +2521,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState {
filesModifiedByAnotherUser: [],
activeFrames: [],
commentFilterMode: 'all',
forking: false,
}
}

Expand Down Expand Up @@ -2894,6 +2898,7 @@ export function editorModelFromPersistentModel(
filesModifiedByAnotherUser: [],
activeFrames: [],
commentFilterMode: 'all',
forking: false,
}
return editor
}
Expand Down
2 changes: 2 additions & 0 deletions editor/src/components/editor/store/editor-update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ export function runSimpleLocalEditorAction(
)
case 'SET_COMMENT_FILTER_MODE':
return UPDATE_FNS.SET_SHOW_RESOLVED_THREADS(action, state)
case 'SET_FORKING':
return UPDATE_FNS.SET_FORKING(action, state)
default:
return state
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4561,6 +4561,8 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall<EditorState> = (
newValue.commentFilterMode,
)

const forkingResults = BooleanKeepDeepEquality(oldValue.forking, newValue.forking)

const areEqual =
idResult.areEqual &&
forkedFromProjectIdResult.areEqual &&
Expand Down Expand Up @@ -4639,7 +4641,8 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall<EditorState> = (
internalClipboardResults.areEqual &&
filesModifiedByAnotherUserResults.areEqual &&
activeFramesResults.areEqual &&
commentFilterModeResults.areEqual
commentFilterModeResults.areEqual &&
forkingResults.areEqual

if (areEqual) {
return keepDeepEqualityResult(oldValue, true)
Expand Down Expand Up @@ -4724,6 +4727,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall<EditorState> = (
filesModifiedByAnotherUserResults.value,
activeFramesResults.value,
commentFilterModeResults.value,
forkingResults.value,
)

return keepDeepEqualityResult(newEditorState, false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,5 @@ export const EmptyEditorStateForKeysOnly: EditorState = {
filesModifiedByAnotherUser: [],
activeFrames: [],
commentFilterMode: 'all',
forking: false,
}
6 changes: 6 additions & 0 deletions utopia-remix/app/components/projectActionContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ export const ProjectContextMenu = React.memo(({ project }: { project: ProjectWit
// TODO notification toast
},
},
{
text: 'Fork',
onClick: (project) => {
window.open(projectEditorLink(project.proj_id) + '/?fork=true', '_blank')
},
},
'separator',
{
text: 'Rename',
Expand Down

0 comments on commit d1c0dcb

Please sign in to comment.