Skip to content

Commit

Permalink
feat(import): github import wizard (#6507)
Browse files Browse the repository at this point in the history
This PR adds a Github import wizard, tracking the progress of importing
a project from Github, showing errors or warnings. The design is still
preliminary, but it is hidden behind a feature switch so that the logic
can be reviewed and merges, in order not to create a long living PR.
<video
src="https://github.com/user-attachments/assets/65c484b2-815a-4c07-8fb2-2346f06a466e"></video>

It supports error/warning states:
<img width="1362" alt="image"
src="https://github.com/user-attachments/assets/cc999476-f2d8-43bb-b7aa-4999fe15f589">

## PR Includes:
1. Adding three actions
    - `SetImportWizardOpen` - controlling the wizard UI
- `UpdateImportOperations` - updating the results of individual import
operations (desc. to follow).
- `UpdateProjectRequirements` - updating the results of checking and
fixing Utopia specific requirements
2. Utilities to start/end tracking of import operations, their results
and timings.
3. Tracked import operations - tracking hooks are being called at
appropriate places in the code.
    - Loading the code from Github
    - Parsing the files
    - Checking specific Utopia requirements
    - Loading dependencies
4. Utopia checks:
    - storyboard file (created if not present)
    - `utopia` entry in package.json
    - project composition (JS/TS)
5. Not tracked in this PR but will in subsequent ones (can be added
modularily):
    - React version - currently a placeholder
    - Server side packages - mainly Node builtins
    - Style frameworks
6. The wizard UI itself - showing progress and results (still
preliminary)

**Note for reviewers:** there are a lot of changed files due to adding
new actions and passing down the `dispatch` parameter, but the main
logic is contained in the new services and the wizard React components.

**Manual Tests:**
I hereby swear that:

- [X] I opened a hydrogen project and it loaded
- [X] I could navigate to various routes in Play mode
  • Loading branch information
liady authored Oct 29, 2024
1 parent 764f258 commit 914b7bc
Show file tree
Hide file tree
Showing 25 changed files with 1,569 additions and 78 deletions.
24 changes: 24 additions & 0 deletions editor/src/components/editor/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ import type { Optic } from '../../core/shared/optics/optics'
import { makeOptic } from '../../core/shared/optics/optics'
import type { ElementPathTrees } from '../../core/shared/element-path-tree'
import { assertNever } from '../../core/shared/utils'
import type {
ImportOperation,
ImportOperationAction,
} from '../../core/shared/import/import-operation-types'
import type { ProjectRequirements } from '../../core/shared/import/proejct-health-check/utopia-requirements-types'
export { isLoggedIn, loggedInUser, notLoggedIn } from '../../common/user'
export type { LoginState, UserDetails } from '../../common/user'

Expand Down Expand Up @@ -997,6 +1002,22 @@ export interface UpdateGithubOperations {
type: GithubOperationType
}

export interface UpdateImportOperations {
action: 'UPDATE_IMPORT_OPERATIONS'
operations: ImportOperation[]
type: ImportOperationAction
}

export interface UpdateProjectRequirements {
action: 'UPDATE_PROJECT_REQUIREMENTS'
requirements: Partial<ProjectRequirements>
}

export interface SetImportWizardOpen {
action: 'SET_IMPORT_WIZARD_OPEN'
open: boolean
}

export interface SetRefreshingDependencies {
action: 'SET_REFRESHING_DEPENDENCIES'
value: boolean
Expand Down Expand Up @@ -1354,6 +1375,9 @@ export type EditorAction =
| UpdateAgainstGithub
| SetImageDragSessionState
| UpdateGithubOperations
| UpdateImportOperations
| UpdateProjectRequirements
| SetImportWizardOpen
| UpdateBranchContents
| SetRefreshingDependencies
| ApplyCommandsAction
Expand Down
35 changes: 35 additions & 0 deletions editor/src/components/editor/actions/action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ import type {
ToggleDataCanCondense,
UpdateMetadataInEditorState,
SetErrorBoundaryHandling,
SetImportWizardOpen,
UpdateImportOperations,
UpdateProjectRequirements,
} from '../action-types'
import type { InsertionSubjectWrapper, Mode } from '../editor-modes'
import { EditorModes, insertionSubject } from '../editor-modes'
Expand Down Expand Up @@ -268,6 +271,11 @@ import type { Collaborator } from '../../../core/shared/multiplayer'
import type { PageTemplate } from '../../canvas/remix/remix-utils'
import type { Bounds } from 'utopia-vscode-common'
import type { ElementPathTrees } from '../../../core/shared/element-path-tree'
import type {
ImportOperation,
ImportOperationAction,
} from '../../../core/shared/import/import-operation-types'
import type { ProjectRequirements } from '../../../core/shared/import/proejct-health-check/utopia-requirements-types'

export function clearSelection(): EditorAction {
return {
Expand Down Expand Up @@ -1591,6 +1599,33 @@ export function resetCanvas(): ResetCanvas {
}
}

export function updateImportOperations(
operations: ImportOperation[],
type: ImportOperationAction,
): UpdateImportOperations {
return {
action: 'UPDATE_IMPORT_OPERATIONS',
operations: operations,
type: type,
}
}

export function updateProjectRequirements(
requirements: Partial<ProjectRequirements>,
): UpdateProjectRequirements {
return {
action: 'UPDATE_PROJECT_REQUIREMENTS',
requirements: requirements,
}
}

export function setImportWizardOpen(open: boolean): SetImportWizardOpen {
return {
action: 'SET_IMPORT_WIZARD_OPEN',
open: open,
}
}

export function setFilebrowserDropTarget(target: string | null): SetFilebrowserDropTarget {
return {
action: 'SET_FILEBROWSER_DROPTARGET',
Expand Down
3 changes: 3 additions & 0 deletions editor/src/components/editor/actions/action-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export function isTransientAction(action: EditorAction): boolean {
case 'RESET_ONLINE_STATE':
case 'INCREASE_ONLINE_STATE_FAILURE_COUNT':
case 'SET_ERROR_BOUNDARY_HANDLING':
case 'SET_IMPORT_WIZARD_OPEN':
case 'UPDATE_IMPORT_OPERATIONS':
case 'UPDATE_PROJECT_REQUIREMENTS':
return true

case 'TRUE_UP_ELEMENTS':
Expand Down
2 changes: 2 additions & 0 deletions editor/src/components/editor/actions/actions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,7 @@ describe('UPDATE_FROM_WORKER', () => {
updateToCheck,
startingEditorState,
defaultUserState,
NO_OP,
)

// Check that the model hasn't changed, because of the stale revised time.
Expand Down Expand Up @@ -1180,6 +1181,7 @@ describe('UPDATE_FROM_WORKER', () => {
updateToCheck,
startingEditorState,
defaultUserState,
NO_OP,
)

// Get the same values that we started with but from the updated editor state.
Expand Down
82 changes: 78 additions & 4 deletions editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ import type {
ToggleDataCanCondense,
UpdateMetadataInEditorState,
SetErrorBoundaryHandling,
SetImportWizardOpen,
UpdateImportOperations,
UpdateProjectRequirements,
} from '../action-types'
import { isAlignment, isLoggedIn } from '../action-types'
import type { Mode } from '../editor-modes'
Expand Down Expand Up @@ -626,6 +629,13 @@ import { canCondenseJSXElementChild } from '../../../utils/can-condense'
import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-utils'
import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils'
import { styleP } from '../../inspector/inspector-common'
import { getUpdateOperationResult } from '../../../core/shared/import/import-operation-service'
import {
notifyCheckingRequirement,
notifyResolveRequirement,
updateRequirements,
} from '../../../core/shared/import/proejct-health-check/utopia-requirements-service'
import { RequirementResolutionResult } from '../../../core/shared/import/proejct-health-check/utopia-requirements-types'
import { applyValuesAtPath, deleteValuesAtPath } from '../../canvas/commands/utils/property-utils'

export const MIN_CODE_PANE_REOPEN_WIDTH = 100
Expand Down Expand Up @@ -1030,6 +1040,9 @@ export function restoreEditorState(
githubSettings: currentEditor.githubSettings,
imageDragSessionState: currentEditor.imageDragSessionState,
githubOperations: currentEditor.githubOperations,
importOperations: currentEditor.importOperations,
projectRequirements: currentEditor.projectRequirements,
importWizardOpen: currentEditor.importWizardOpen,
branchOriginContents: currentEditor.branchOriginContents,
githubData: currentEditor.githubData,
refreshingDependencies: currentEditor.refreshingDependencies,
Expand Down Expand Up @@ -1627,19 +1640,44 @@ function createStoryboardFileWithPlaceholderContents(
}

export function createStoryboardFileIfNecessary(
dispatch: EditorDispatch,
projectContents: ProjectContentTreeRoot,
createPlaceholder: 'create-placeholder' | 'skip-creating-placeholder',
): ProjectContentTreeRoot {
notifyCheckingRequirement(dispatch, 'storyboard', 'Checking for storyboard.js')
const storyboardFile = getProjectFileByFilePath(projectContents, StoryboardFilePath)
if (storyboardFile != null) {
notifyResolveRequirement(
dispatch,
'storyboard',
RequirementResolutionResult.Found,
'Storyboard.js found',
)
return projectContents
}

return (
const result =
createStoryboardFileIfRemixProject(projectContents) ??
createStoryboardFileIfMainComponentPresent(projectContents) ??
createStoryboardFileWithPlaceholderContents(projectContents, createPlaceholder)
)

if (result == projectContents) {
notifyResolveRequirement(
dispatch,
'storyboard',
RequirementResolutionResult.Partial,
'Storyboard.js skipped',
)
} else {
notifyResolveRequirement(
dispatch,
'storyboard',
RequirementResolutionResult.Fixed,
'Storyboard.js created',
)
}

return result
}

// JS Editor Actions:
Expand Down Expand Up @@ -2229,6 +2267,34 @@ export const UPDATE_FNS = {
githubOperations: operations,
}
},
UPDATE_IMPORT_OPERATIONS: (action: UpdateImportOperations, editor: EditorModel): EditorModel => {
const resultImportOperations = getUpdateOperationResult(
editor.importOperations,
action.operations,
action.type,
)
return {
...editor,
importOperations: resultImportOperations,
}
},
UPDATE_PROJECT_REQUIREMENTS: (
action: UpdateProjectRequirements,
editor: EditorModel,
dispatch: EditorDispatch,
): EditorModel => {
const result = updateRequirements(dispatch, editor.projectRequirements, action.requirements)
return {
...editor,
projectRequirements: result,
}
},
SET_IMPORT_WIZARD_OPEN: (action: SetImportWizardOpen, editor: EditorModel): EditorModel => {
return {
...editor,
importWizardOpen: action.open,
}
},
SET_REFRESHING_DEPENDENCIES: (
action: SetRefreshingDependencies,
editor: EditorModel,
Expand Down Expand Up @@ -3947,6 +4013,7 @@ export const UPDATE_FNS = {
action: UpdateFromWorker,
editor: EditorModel,
userState: UserState,
dispatch: EditorDispatch,
): EditorModel => {
let workingProjectContents: ProjectContentTreeRoot = editor.projectContents
let anyParsedUpdates: boolean = false
Expand Down Expand Up @@ -3983,6 +4050,7 @@ export const UPDATE_FNS = {
return {
...editor,
projectContents: createStoryboardFileIfNecessary(
dispatch,
workingProjectContents,
// If we are in the process of cloning a Github repository, do not create placeholder Storyboard
userState.githubState.gitRepoToLoad != null
Expand Down Expand Up @@ -4808,7 +4876,11 @@ export const UPDATE_FNS = {
propertyControlsInfo: action.propertyControlsInfo,
}
},
UPDATE_TEXT: (action: UpdateText, editorStore: EditorStoreUnpatched): EditorStoreUnpatched => {
UPDATE_TEXT: (
action: UpdateText,
editorStore: EditorStoreUnpatched,
dispatch: EditorDispatch,
): EditorStoreUnpatched => {
const { textProp } = action
// This flag is useful when editing conditional expressions:
// if the edited element is a js expression AND the content is still between curly brackets after editing,
Expand Down Expand Up @@ -5006,6 +5078,7 @@ export const UPDATE_FNS = {
updateFromWorker(workerUpdates),
withFileChanges.unpatchedEditor,
withFileChanges.userState,
dispatch,
)
return {
...withFileChanges,
Expand Down Expand Up @@ -5621,7 +5694,7 @@ export const UPDATE_FNS = {
requestedNpmDependency('tailwindcss', tailwindVersion.version),
requestedNpmDependency('postcss', postcssVersion.version),
]
void fetchNodeModules(updatedNpmDeps, builtInDependencies).then(
void fetchNodeModules(dispatch, updatedNpmDeps, builtInDependencies).then(
(fetchNodeModulesResult) => {
const loadedPackagesStatus = createLoadedPackageStatusMapFromDependencies(
updatedNpmDeps,
Expand Down Expand Up @@ -6331,6 +6404,7 @@ export async function load(
const migratedModel = applyMigrations(model)
const npmDependencies = dependenciesWithEditorRequirements(migratedModel.projectContents)
const fetchNodeModulesResult = await fetchNodeModules(
dispatch,
npmDependencies,
builtInDependencies,
retryFetchNodeModules,
Expand Down
12 changes: 10 additions & 2 deletions editor/src/components/editor/editor-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import {
navigatorTargetsSelector,
navigatorTargetsSelectorNavigatorTargets,
} from '../navigator/navigator-utils'
import { ImportWizard } from './import-wizard/import-wizard'

const liveModeToastId = 'play-mode-toast'

Expand Down Expand Up @@ -520,6 +521,7 @@ export const EditorComponentInner = React.memo((props: EditorProps) => {
<ProjectForkFlow />
<LockedOverlay />
<SharingDialog />
<ImportWizard />
</SimpleFlexRow>
{portalTarget != null
? ReactDOM.createPortal(<ComponentPickerContextMenu />, portalTarget)
Expand Down Expand Up @@ -683,6 +685,12 @@ const LockedOverlay = React.memo(() => {
'LockedOverlay refreshingDependencies',
)

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

const forking = useEditorState(
Substores.restOfEditor,
(store) => store.editor.forking,
Expand All @@ -699,8 +707,8 @@ const LockedOverlay = React.memo(() => {
`

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

const dialogContent = React.useMemo((): string | null => {
if (refreshingDependencies) {
Expand Down
Loading

0 comments on commit 914b7bc

Please sign in to comment.