From 03865fc18a97a04789eaf77179f28908060d6738 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 5 Nov 2024 17:52:05 +0200 Subject: [PATCH] feat(import): allow continuing the import after an error (#6614) This PR adds the ability to control the import process after an error: 1. Import a different project 2. Decide to continue anyway (for example to correct it using our editor). This PR distinguishes between an `Error` (that can be continued) and a `Critical Error` (for example a non-existent repo) - that we cannot recover from. **PR details:** 1. In [editor/src/core/shared/github/operations/load-branch.ts](https://github.com/concrete-utopia/utopia/pull/6614/files#diff-3c1474cac72b0e757d951539630bd72175135c05eddbff91d9f10639d2796440) this PR adds the ability to stop before dependencies fetching and resume afterwards 2. In [editor/src/components/editor/import-wizard/import-wizard.tsx](https://github.com/concrete-utopia/utopia/pull/6614/files#diff-a1ec93eec4b0720f01ff6ec77bb4efb6cc3ac0ba4297d3fe7bb77049c601ca94) this PR adds the UI for displaying the correct status/buttons according to the current import state. 3. The rest of the changes are mostly actions/state changes. For example this flow: 1. Importing a non-existing repo (critical error) 2. Then importing a project with errors (TS/server packages, etc), choosing to continue importing 3. Deciding to import a different project - which is a success Current design: image **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 --- editor/src/components/editor/action-types.ts | 7 + .../editor/actions/action-creators.ts | 9 ++ .../components/editor/actions/action-utils.ts | 1 + .../src/components/editor/actions/actions.tsx | 16 +- .../editor/import-wizard/components.tsx | 39 +++-- .../editor/import-wizard/import-wizard.tsx | 144 ++++++++++++------ .../components/editor/store/editor-state.ts | 16 +- .../components/editor/store/editor-update.tsx | 2 + .../store/store-deep-equality-instances.ts | 23 ++- .../store/store-hook-substore-helpers.ts | 3 +- .../editor/store/store-hook-substore-types.ts | 2 +- .../github/github-repository-clone-flow.tsx | 17 ++- .../shared/github/operations/load-branch.ts | 66 +++++--- .../shared/import/import-operation-service.ts | 49 +++++- .../shared/import/import-operation-types.ts | 38 ++++- editor/src/utils/feature-switches.ts | 2 +- 16 files changed, 333 insertions(+), 101 deletions(-) diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index b00f6ce2d31b..552ecdf9b594 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -90,6 +90,7 @@ import { assertNever } from '../../core/shared/utils' import type { ImportOperation, ImportOperationAction, + ImportStatus, } from '../../core/shared/import/import-operation-types' import type { ProjectRequirements } from '../../core/shared/import/project-health-check/utopia-requirements-types' export { isLoggedIn, loggedInUser, notLoggedIn } from '../../common/user' @@ -1008,6 +1009,11 @@ export interface UpdateImportOperations { type: ImportOperationAction } +export interface UpdateImportStatus { + action: 'UPDATE_IMPORT_STATUS' + importStatus: ImportStatus +} + export interface UpdateProjectRequirements { action: 'UPDATE_PROJECT_REQUIREMENTS' requirements: Partial @@ -1376,6 +1382,7 @@ export type EditorAction = | SetImageDragSessionState | UpdateGithubOperations | UpdateImportOperations + | UpdateImportStatus | UpdateProjectRequirements | SetImportWizardOpen | UpdateBranchContents diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 31db2ce322e9..a8c9383b9f1a 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -240,6 +240,7 @@ import type { SetImportWizardOpen, UpdateImportOperations, UpdateProjectRequirements, + UpdateImportStatus, } from '../action-types' import type { InsertionSubjectWrapper, Mode } from '../editor-modes' import { EditorModes, insertionSubject } from '../editor-modes' @@ -274,6 +275,7 @@ import type { ElementPathTrees } from '../../../core/shared/element-path-tree' import type { ImportOperation, ImportOperationAction, + ImportStatus, } from '../../../core/shared/import/import-operation-types' import type { ProjectRequirements } from '../../../core/shared/import/project-health-check/utopia-requirements-types' @@ -1610,6 +1612,13 @@ export function updateImportOperations( } } +export function updateImportStatus(importStatus: ImportStatus): UpdateImportStatus { + return { + action: 'UPDATE_IMPORT_STATUS', + importStatus: importStatus, + } +} + export function updateProjectRequirements( requirements: Partial, ): UpdateProjectRequirements { diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index 21bf6e0852ba..aeffe3713f27 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -141,6 +141,7 @@ export function isTransientAction(action: EditorAction): boolean { case 'SET_ERROR_BOUNDARY_HANDLING': case 'SET_IMPORT_WIZARD_OPEN': case 'UPDATE_IMPORT_OPERATIONS': + case 'UPDATE_IMPORT_STATUS': case 'UPDATE_PROJECT_REQUIREMENTS': return true diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 5b2d20b462bf..5bbbe2ef3620 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -355,6 +355,7 @@ import type { SetImportWizardOpen, UpdateImportOperations, UpdateProjectRequirements, + UpdateImportStatus, } from '../action-types' import { isAlignment, isLoggedIn } from '../action-types' import type { Mode } from '../editor-modes' @@ -1034,7 +1035,7 @@ export function restoreEditorState( githubSettings: currentEditor.githubSettings, imageDragSessionState: currentEditor.imageDragSessionState, githubOperations: currentEditor.githubOperations, - importOperations: currentEditor.importOperations, + importState: currentEditor.importState, projectRequirements: currentEditor.projectRequirements, importWizardOpen: currentEditor.importWizardOpen, branchOriginContents: currentEditor.branchOriginContents, @@ -2178,13 +2179,22 @@ export const UPDATE_FNS = { }, UPDATE_IMPORT_OPERATIONS: (action: UpdateImportOperations, editor: EditorModel): EditorModel => { const resultImportOperations = getUpdateOperationResult( - editor.importOperations, + editor.importState.importOperations, action.operations, action.type, ) return { ...editor, - importOperations: resultImportOperations, + importState: { ...editor.importState, importOperations: resultImportOperations }, + } + }, + UPDATE_IMPORT_STATUS: (action: UpdateImportStatus, editor: EditorModel): EditorModel => { + return { + ...editor, + importState: { + ...editor.importState, + importStatus: action.importStatus, + }, } }, UPDATE_PROJECT_REQUIREMENTS: ( diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx index ec15c248288c..e086e93771fa 100644 --- a/editor/src/components/editor/import-wizard/components.tsx +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -15,11 +15,10 @@ import { RequirementResolutionResult } from '../../../core/shared/import/project export function OperationLine({ operation }: { operation: ImportOperation }) { const operationRunningStatus = React.useMemo(() => { - return operation.timeStarted == null - ? 'waiting' - : operation.timeDone == null - ? 'running' - : 'done' + if (operation.timeDone != null) { + return 'done' + } + return operation.timeStarted == null ? 'waiting' : 'running' }, [operation.timeStarted, operation.timeDone]) const colorTheme = useColorTheme() const textColor = operationRunningStatus === 'waiting' ? 'gray' : colorTheme.fg0.value @@ -29,7 +28,7 @@ export function OperationLine({ operation }: { operation: ImportOperation }) { () => childrenShown || operation.timeDone == null || - operation.result == ImportOperationResult.Error, + operation.result != ImportOperationResult.Success, [childrenShown, operation.timeDone, operation.result], ) const hasChildren = React.useMemo( @@ -253,14 +252,26 @@ function getImportOperationText(operation: ImportOperation): React.ReactNode { } switch (operation.type) { case 'loadBranch': - return ( - - Loading branch{' '} - - {operation.githubRepo?.owner}/{operation.githubRepo?.repository}@{operation.branchName} - - - ) + if (operation.branchName != null) { + return ( + + Loading branch{' '} + + {operation.githubRepo?.owner}/{operation.githubRepo?.repository}@ + {operation.branchName} + + + ) + } else { + return ( + + Loading repository{' '} + + {operation.githubRepo?.owner}/{operation.githubRepo?.repository} + + + ) + } case 'fetchDependency': return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}` case 'parseFiles': diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 1e212a88cdca..e1a38c8e92d4 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -3,14 +3,25 @@ import React from 'react' import { jsx } from '@emotion/react' import { getProjectID } from '../../../common/env-vars' -import { Button, FlexRow, Icons, useColorTheme, UtopiaStyles } from '../../../uuiui' +import { Button, FlexRow, useColorTheme, UtopiaStyles } from '../../../uuiui' import { useEditorState, Substores } from '../store/store-hook' -import { when } from '../../../utils/react-conditionals' -import { hideImportWizard } from '../../../core/shared/import/import-operation-service' +import { unless, when } from '../../../utils/react-conditionals' +import { + getTotalImportStatusAndResult, + hideImportWizard, + updateProjectImportStatus, +} from '../../../core/shared/import/import-operation-service' import { OperationLine } from './components' +import type { TotalImportResult } from '../../../core/shared/import/import-operation-types' import { ImportOperationResult } from '../../../core/shared/import/import-operation-types' import { assertNever } from '../../../core/shared/utils' import { useDispatch } from '../store/dispatch-context' +import { + setImportWizardOpen, + setLeftMenuTab, + updateGithubSettings, +} from '../actions/action-creators' +import { emptyGithubSettings, LeftMenuTab } from '../store/editor-state' export const ImportWizard = React.memo(() => { const colorTheme = useColorTheme() @@ -22,12 +33,14 @@ export const ImportWizard = React.memo(() => { 'ImportWizard importWizardOpen', ) - const operations = useEditorState( + const importState = useEditorState( Substores.github, - (store) => store.editor.importOperations, - 'ImportWizard operations', + (store) => store.editor.importState, + 'ImportWizard importState', ) + const operations = importState.importOperations + const dispatch = useDispatch() const handleDismiss = React.useCallback(() => { @@ -38,25 +51,10 @@ export const ImportWizard = React.memo(() => { e.stopPropagation() }, []) - const totalImportResult: ImportOperationResult | null = React.useMemo(() => { - let result: ImportOperationResult = ImportOperationResult.Success - for (const operation of operations) { - // if one of the operations is still running, we don't know the total result yet - if (operation.timeDone == null || operation.result == null) { - return null - } - // if any operation is an error, the total result is an error - if (operation.result == ImportOperationResult.Error) { - return ImportOperationResult.Error - } - // if any operation is at least a warn, the total result is a warn, - // but we also need to check if there are any errors - if (operation.result == ImportOperationResult.Warn) { - result = ImportOperationResult.Warn - } - } - return result - }, [operations]) + const totalImportResult: TotalImportResult = React.useMemo( + () => getTotalImportStatusAndResult(importState), + [importState], + ) if (projectId == null) { return null @@ -86,12 +84,12 @@ export const ImportWizard = React.memo(() => { boxShadow: UtopiaStyles.popup.boxShadow, borderRadius: 10, width: 600, - height: 420, + height: 450, position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', - fontSize: '14px', + fontSize: '13px', lineHeight: 'normal', letterSpacing: 'normal', padding: 20, @@ -104,11 +102,13 @@ export const ImportWizard = React.memo(() => { css={{ justifyContent: 'space-between', width: '100%', + height: '30px', + flex: 'none', }} >
Loading Project
{when( - totalImportResult == null, + totalImportResult.importStatus.status === 'in-progress', ) } - if (importResult == ImportOperationResult.Error) { + if ( + importResult.result == ImportOperationResult.Error || + importResult.result == ImportOperationResult.CriticalError + ) { return ( -
Error Importing Project
- +
+ {importResult.importStatus.status !== 'done' || + importResult.result === ImportOperationResult.CriticalError + ? 'Error Importing Project' + : 'Project Imported With Errors'} +
+ + {unless( + importResult.result == ImportOperationResult.CriticalError, + , + )}
) } diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 233bf98cf6d4..9bd727ec1d77 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -189,7 +189,11 @@ import type { OnlineState } from '../online-status' import type { NavigatorRow } from '../../navigator/navigator-row' import type { FancyError } from '../../../core/shared/code-exec-utils' import type { GridCellCoordinates } from '../../canvas/canvas-strategies/strategies/grid-cell-bounds' -import type { ImportOperation } from '../../../core/shared/import/import-operation-types' +import { + emptyImportState, + type ImportOperation, + type ImportState, +} from '../../../core/shared/import/import-operation-types' import { emptyProjectRequirements, type ProjectRequirements, @@ -1457,7 +1461,7 @@ export interface EditorState { githubSettings: ProjectGithubSettings imageDragSessionState: ImageDragSessionState githubOperations: Array - importOperations: Array + importState: ImportState projectRequirements: ProjectRequirements importWizardOpen: boolean githubData: GithubData @@ -1543,7 +1547,7 @@ export function editorState( githubSettings: ProjectGithubSettings, imageDragSessionState: ImageDragSessionState, githubOperations: Array, - importOperations: Array, + importState: ImportState, importWizardOpen: boolean, projectRequirements: ProjectRequirements, branchOriginContents: ProjectContentTreeRoot | null, @@ -1630,7 +1634,7 @@ export function editorState( githubSettings: githubSettings, imageDragSessionState: imageDragSessionState, githubOperations: githubOperations, - importOperations: importOperations, + importState: importState, importWizardOpen: importWizardOpen, projectRequirements: projectRequirements, githubData: githubData, @@ -2710,7 +2714,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { githubSettings: emptyGithubSettings(), imageDragSessionState: notDragging(), githubOperations: [], - importOperations: [], + importState: emptyImportState(), importWizardOpen: false, projectRequirements: emptyProjectRequirements(), branchOriginContents: null, @@ -3080,7 +3084,7 @@ export function editorModelFromPersistentModel( githubSettings: persistentModel.githubSettings, imageDragSessionState: notDragging(), githubOperations: [], - importOperations: [], + importState: emptyImportState(), importWizardOpen: false, projectRequirements: emptyProjectRequirements(), refreshingDependencies: false, diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index 2c7ded3d89bc..a7782b72af78 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -222,6 +222,8 @@ export function runSimpleLocalEditorAction( return UPDATE_FNS.UPDATE_GITHUB_OPERATIONS(action, state) case 'UPDATE_IMPORT_OPERATIONS': return UPDATE_FNS.UPDATE_IMPORT_OPERATIONS(action, state) + case 'UPDATE_IMPORT_STATUS': + return UPDATE_FNS.UPDATE_IMPORT_STATUS(action, state) case 'SET_IMPORT_WIZARD_OPEN': return UPDATE_FNS.SET_IMPORT_WIZARD_OPEN(action, state) case 'UPDATE_PROJECT_REQUIREMENTS': diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index b1b7f7816a3c..6c039e513818 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -643,7 +643,11 @@ import type { } from '../../../core/property-controls/component-descriptor-parser' import type { Axis } from '../../../components/canvas/gap-utils' import type { GridCellCoordinates } from '../../canvas/canvas-strategies/strategies/grid-cell-bounds' -import type { ImportOperation } from '../../../core/shared/import/import-operation-types' +import { + newImportState, + type ImportOperation, + type ImportState, +} from '../../../core/shared/import/import-operation-types' import type { ProjectRequirements, RequirementResolution, @@ -4904,6 +4908,14 @@ export const GithubOperationKeepDeepEquality: KeepDeepEqualityCall> = arrayDeepEquality(GithubOperationKeepDeepEquality) +export const ImportStateKeepDeepEquality: KeepDeepEqualityCall = combine2EqualityCalls( + (importState) => importState.importStatus, + createCallWithTripleEquals(), + (importState) => importState.importOperations, + arrayDeepEquality(ImportOperationKeepDeepEquality), + newImportState, +) + export const ColorSwatchDeepEquality: KeepDeepEqualityCall = combine2EqualityCalls( (c) => c.id, StringKeepDeepEquality, @@ -5434,10 +5446,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( newValue.githubOperations, ) - const importOperationsResults = arrayDeepEquality(ImportOperationKeepDeepEquality)( - oldValue.importOperations, - newValue.importOperations, - ) + const importStateResults = ImportStateKeepDeepEquality(oldValue.importState, newValue.importState) const importWizardOpenResults = BooleanKeepDeepEquality( oldValue.importWizardOpen, @@ -5570,7 +5579,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( githubSettingsResults.areEqual && imageDragSessionStateEqual.areEqual && githubOperationsResults.areEqual && - importOperationsResults.areEqual && + importStateResults.areEqual && importWizardOpenResults.areEqual && projectRequirementsResults.areEqual && branchContentsResults.areEqual && @@ -5659,7 +5668,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( githubSettingsResults.value, imageDragSessionStateEqual.value, githubOperationsResults.value, - importOperationsResults.value, + importStateResults.value, importWizardOpenResults.value, projectRequirementsResults.value, branchContentsResults.value, diff --git a/editor/src/components/editor/store/store-hook-substore-helpers.ts b/editor/src/components/editor/store/store-hook-substore-helpers.ts index 8d985eccca1d..94249fa36087 100644 --- a/editor/src/components/editor/store/store-hook-substore-helpers.ts +++ b/editor/src/components/editor/store/store-hook-substore-helpers.ts @@ -1,5 +1,6 @@ import { type EditorState } from './editor-state' import { emptyProjectRequirements } from '../../../core/shared/import/project-health-check/utopia-requirements-types' +import { emptyImportState } from '../../../core/shared/import/import-operation-types' export const EmptyEditorStateForKeysOnly: EditorState = { id: null, @@ -158,7 +159,7 @@ export const EmptyEditorStateForKeysOnly: EditorState = { githubSettings: null as any, imageDragSessionState: null as any, githubOperations: [], - importOperations: [], + importState: emptyImportState(), importWizardOpen: false, projectRequirements: emptyProjectRequirements(), branchOriginContents: null, diff --git a/editor/src/components/editor/store/store-hook-substore-types.ts b/editor/src/components/editor/store/store-hook-substore-types.ts index b08ce4a6da76..178ab22a95d2 100644 --- a/editor/src/components/editor/store/store-hook-substore-types.ts +++ b/editor/src/components/editor/store/store-hook-substore-types.ts @@ -178,7 +178,7 @@ export const githubSubstateKeys = [ 'githubSettings', 'githubOperations', 'githubData', - 'importOperations', + 'importState', 'projectRequirements', ] as const export const emptyGithubSubstate = { diff --git a/editor/src/components/github/github-repository-clone-flow.tsx b/editor/src/components/github/github-repository-clone-flow.tsx index fbd56a47ef4d..6973c8b94524 100644 --- a/editor/src/components/github/github-repository-clone-flow.tsx +++ b/editor/src/components/github/github-repository-clone-flow.tsx @@ -26,6 +26,12 @@ import { CloneParamKey } from '../editor/persistence/persistence-backend' import { getPublicRepositoryEntryOrNull } from '../../core/shared/github/operations/load-repositories' import { OperationContext } from '../../core/shared/github/operations/github-operation-context' import { notice } from '../common/notice' +import { isFeatureEnabled } from '../../utils/feature-switches' +import { + notifyOperationFinished, + startImportProcess, +} from '../../core/shared/import/import-operation-service' +import { ImportOperationResult } from '../../core/shared/import/import-operation-types' export const LoadActionsDispatched = 'loadActionDispatched' @@ -123,7 +129,16 @@ async function cloneGithubRepo( repo: githubRepo.repository, }) if (repositoryEntry == null) { - dispatch([showToast(notice('Cannot find repository', 'ERROR'))]) + if (isFeatureEnabled('Import Wizard')) { + startImportProcess(dispatch) + notifyOperationFinished( + dispatch, + { type: 'loadBranch', branchName: githubRepo.branch ?? undefined, githubRepo: githubRepo }, + ImportOperationResult.CriticalError, + ) + } else { + dispatch([showToast(notice('Cannot find repository', 'ERROR'))]) + } return } const githubBranch = githubRepo.branch ?? repositoryEntry.defaultBranch diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index 8182198dfb15..9fe8389eaabd 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -1,6 +1,6 @@ import type { UtopiaTsWorkers } from '../../../../core/workers/common/worker-types' import type { ProjectContentTreeRoot } from '../../../../components/assets' -import { getProjectFileByFilePath, walkContentsTree } from '../../../../components/assets' +import { getProjectFileByFilePath } from '../../../../components/assets' import { packageJsonFileFromProjectContents, walkContentsTreeAsync, @@ -12,6 +12,7 @@ import { showToast, truncateHistory, updateBranchContents, + updateImportStatus, updateProjectContents, } from '../../../../components/editor/actions/action-creators' import type { @@ -24,7 +25,12 @@ import { refreshDependencies } from '../../dependencies' import type { RequestedNpmDependency } from '../../npm-dependency-types' import { forceNotNull } from '../../optional-utils' import { isTextFile } from '../../project-file-types' -import type { BranchContent, GithubOperationSource } from '../helpers' +import type { + BranchContent, + GetBranchContentResponse, + GetBranchContentSuccess, + GithubOperationSource, +} from '../helpers' import { connectRepo, getExistingAssets, @@ -40,6 +46,7 @@ import { updateProjectContentsWithParseResults } from '../../parser-projectconte import { notifyOperationFinished, notifyOperationStarted, + pauseImport, startImportProcess, } from '../../import/import-operation-service' import { @@ -48,6 +55,7 @@ import { } from '../../import/project-health-check/utopia-requirements-service' import { checkAndFixUtopiaRequirements } from '../../import/project-health-check/check-utopia-requirements' import { ImportOperationResult } from '../../import/import-operation-types' +import { isFeatureEnabled } from '../../../../utils/feature-switches' export const saveAssetsToProject = (operationContext: GithubOperationContext) => @@ -151,9 +159,23 @@ export const updateProjectWithBranchContent = switch (responseBody.type) { case 'FAILURE': + if (isFeatureEnabled('Import Wizard')) { + notifyOperationFinished( + dispatch, + { type: 'loadBranch' }, + ImportOperationResult.CriticalError, + ) + } throw githubAPIError(operation, responseBody.failureReason) case 'SUCCESS': if (responseBody.branch == null) { + if (isFeatureEnabled('Import Wizard')) { + notifyOperationFinished( + dispatch, + { type: 'loadBranch' }, + ImportOperationResult.CriticalError, + ) + } throw githubAPIError(operation, `Could not find branch ${branchName}`) } const newGithubData: Partial = { @@ -178,7 +200,8 @@ export const updateProjectWithBranchContent = checkAndFixUtopiaRequirements(dispatch, parseResults) if (requirementResolutionResult === RequirementResolutionResult.Critical) { - return [] + // wait for the user to resume the import if they choose to + await pauseImport(dispatch) } // Update the editor with everything so that if anything else fails past this point @@ -219,28 +242,33 @@ export const updateProjectWithBranchContent = // When the dependencies update has gone through, then indicate that the project was imported. await dependenciesPromise .catch(() => { - dispatch( - [ - showToast( - notice( - `Github: There was an error when attempting to update the dependencies.`, - 'ERROR', - ), + if (isFeatureEnabled('Import Wizard')) { + notifyOperationFinished( + dispatch, + { type: 'refreshDependencies' }, + ImportOperationResult.Error, + ) + } else { + showToast( + notice( + `Github: There was an error when attempting to update the dependencies.`, + 'ERROR', ), - ], - 'everyone', - ) + ) + } }) .finally(() => { dispatch( [ extractPropertyControlsFromDescriptorFiles(componentDescriptorFiles), - showToast( - notice( - `Github: Updated the project with the content from ${branchName}`, - 'SUCCESS', - ), - ), + isFeatureEnabled('Import Wizard') + ? updateImportStatus({ status: 'done' }) + : showToast( + notice( + `Github: Updated the project with the content from ${branchName}`, + 'SUCCESS', + ), + ), ], 'everyone', ) diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index e04b0cb10103..107f19dd8917 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -3,17 +3,22 @@ import type { EditorAction, EditorDispatch } from '../../../components/editor/ac import { setImportWizardOpen, updateImportOperations, + updateImportStatus, } from '../../../components/editor/actions/action-creators' import { ImportOperationAction } from './import-operation-types' import type { ImportOperation, - ImportOperationResult, ImportOperationType, + ImportState, + ImportStatus, + TotalImportResult, } from './import-operation-types' +import { ImportOperationResult } from './import-operation-types' import { isFeatureEnabled } from '../../../utils/feature-switches' export function startImportProcess(dispatch: EditorDispatch) { const actions: EditorAction[] = [ + updateImportStatus({ status: 'in-progress' }), updateImportOperations( [ { type: 'loadBranch' }, @@ -147,3 +152,45 @@ export function getUpdateOperationResult( } return operations } + +export function updateProjectImportStatus(dispatch: EditorDispatch, importStatus: ImportStatus) { + dispatch([updateImportStatus(importStatus)]) +} + +export function getTotalImportStatusAndResult(importState: ImportState): TotalImportResult { + const operations = importState.importOperations + // if any operation is an error, the total result is immediately an error + for (const operation of operations) { + // with critical errors we are done immediately + if (operation.result == ImportOperationResult.CriticalError) { + return { + result: ImportOperationResult.CriticalError, + importStatus: { status: 'done' }, + } + } + // we continue on errors, to let the user decide + if (operation.result == ImportOperationResult.Error) { + return { + result: ImportOperationResult.Error, + importStatus: importState.importStatus, + } + } + } + // if any operation is a warning, the total result is a warning + if (operations.some((op) => op.result == ImportOperationResult.Warn)) { + return { + result: ImportOperationResult.Warn, + importStatus: importState.importStatus, + } + } + return { + result: ImportOperationResult.Success, + importStatus: importState.importStatus, + } +} + +export function pauseImport(dispatch: EditorDispatch): Promise { + return new Promise((resolve) => + updateProjectImportStatus(dispatch, { status: 'paused', onResume: resolve }), + ) +} diff --git a/editor/src/core/shared/import/import-operation-types.ts b/editor/src/core/shared/import/import-operation-types.ts index 08fa2ea2ed05..7789e76639a4 100644 --- a/editor/src/core/shared/import/import-operation-types.ts +++ b/editor/src/core/shared/import/import-operation-types.ts @@ -7,14 +7,16 @@ type ImportOperationData = { timeStarted?: number | null timeDone?: number | null result?: ImportOperationResult - error?: string children?: ImportOperation[] } export const ImportOperationResult = { Success: 'success', - Error: 'error', Warn: 'warn', + // an error that shows "continue anyway" button + Error: 'error', + // an error that we can't recover from + CriticalError: 'criticalError', } as const export type ImportOperationResult = @@ -73,6 +75,33 @@ export type ImportOperation = export type ImportOperationType = ImportOperation['type'] +export type ImportState = { + importStatus: ImportStatus + importOperations: ImportOperation[] +} + +export function newImportState(importStatus: ImportStatus, importOperations: ImportOperation[]) { + return { + importStatus: importStatus, + importOperations: importOperations, + } +} + +export function emptyImportState(): ImportState { + return newImportState({ status: 'not-started' }, []) +} + +export type ImportStatus = + | ImportStatusNotStarted + | ImportStatusInProgress + | ImportStatusDone + | ImportStatusPaused + +export type ImportStatusNotStarted = { status: 'not-started' } +export type ImportStatusInProgress = { status: 'in-progress' } +export type ImportStatusDone = { status: 'done' } +export type ImportStatusPaused = { status: 'paused'; onResume: () => void } + export const ImportOperationAction = { Add: 'add', Remove: 'remove', @@ -82,3 +111,8 @@ export const ImportOperationAction = { export type ImportOperationAction = (typeof ImportOperationAction)[keyof typeof ImportOperationAction] + +export type TotalImportResult = { + importStatus: ImportStatus + result: ImportOperationResult +} diff --git a/editor/src/utils/feature-switches.ts b/editor/src/utils/feature-switches.ts index 0044642a5ccf..fab803319515 100644 --- a/editor/src/utils/feature-switches.ts +++ b/editor/src/utils/feature-switches.ts @@ -70,7 +70,7 @@ let FeatureSwitches: { [feature in FeatureName]: boolean } = { 'Condensed Navigator Entries': !IS_TEST_ENVIRONMENT, 'Use Parsing Cache': false, 'Canvas Fast Selection Hack': true, - 'Import Wizard': false, + 'Import Wizard': !IS_TEST_ENVIRONMENT, 'Show Debug Features': false, }