From 76d0a727c4ce2ca03e970bbaa45b98d27d8aa4b5 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 8 Oct 2024 18:30:59 +0300 Subject: [PATCH 01/25] import wizard --- editor/src/components/editor/action-types.ts | 14 +++ .../editor/actions/action-creators.ts | 21 +++++ .../components/editor/actions/action-utils.ts | 2 + .../src/components/editor/actions/actions.tsx | 52 +++++++++++ .../components/editor/editor-component.tsx | 2 + .../import-wizard/import-wizard-service.tsx | 91 +++++++++++++++++++ .../editor/import-wizard/import-wizard.tsx | 91 +++++++++++++++++++ .../components/editor/store/editor-state.ts | 11 +++ .../components/editor/store/editor-update.tsx | 2 + .../store/store-deep-equality-instances.ts | 28 ++++++ .../store/store-hook-substore-helpers.ts | 2 + .../editor/store/store-hook-substore-types.ts | 7 +- .../navigator/left-pane/github-pane/index.tsx | 5 + .../shared/github/operations/load-branch.ts | 19 ++++ 14 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 editor/src/components/editor/import-wizard/import-wizard-service.tsx create mode 100644 editor/src/components/editor/import-wizard/import-wizard.tsx diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index f841a7836ecd..cae8a6ea6f89 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -87,6 +87,7 @@ 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, ImportOperationType } from './import-wizard/import-wizard-service' export { isLoggedIn, loggedInUser, notLoggedIn } from '../../common/user' export type { LoginState, UserDetails } from '../../common/user' @@ -997,6 +998,12 @@ export interface UpdateGithubOperations { type: GithubOperationType } +export interface UpdateImportOperations { + action: 'UPDATE_IMPORT_OPERATIONS' + operations: ImportOperation[] + type: ImportOperationType +} + export interface SetRefreshingDependencies { action: 'SET_REFRESHING_DEPENDENCIES' value: boolean @@ -1178,6 +1185,11 @@ export interface SetSharingDialogOpen { open: boolean } +export interface SetImportWizardOpen { + action: 'SET_IMPORT_WIZARD_OPEN' + open: boolean +} + export interface ResetOnlineState { action: 'RESET_ONLINE_STATE' } @@ -1354,6 +1366,7 @@ export type EditorAction = | UpdateAgainstGithub | SetImageDragSessionState | UpdateGithubOperations + | UpdateImportOperations | UpdateBranchContents | SetRefreshingDependencies | ApplyCommandsAction @@ -1377,6 +1390,7 @@ export type EditorAction = | SetCollaborators | ExtractPropertyControlsFromDescriptorFiles | SetSharingDialogOpen + | SetImportWizardOpen | ResetOnlineState | IncreaseOnlineStateFailureCount | SetErrorBoundaryHandling diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 5061e880e8aa..f1618c41d272 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -237,6 +237,8 @@ import type { ToggleDataCanCondense, UpdateMetadataInEditorState, SetErrorBoundaryHandling, + SetImportWizardOpen, + UpdateImportOperations, } from '../action-types' import type { InsertionSubjectWrapper, Mode } from '../editor-modes' import { EditorModes, insertionSubject } from '../editor-modes' @@ -268,6 +270,7 @@ 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, ImportOperationType } from '../import-wizard/import-wizard-service' export function clearSelection(): EditorAction { return { @@ -1591,6 +1594,17 @@ export function resetCanvas(): ResetCanvas { } } +export function updateImportOperations( + operations: ImportOperation[], + type: ImportOperationType, +): UpdateImportOperations { + return { + action: 'UPDATE_IMPORT_OPERATIONS', + operations: operations, + type: type, + } +} + export function setFilebrowserDropTarget(target: string | null): SetFilebrowserDropTarget { return { action: 'SET_FILEBROWSER_DROPTARGET', @@ -1876,6 +1890,13 @@ export function setSharingDialogOpen(open: boolean): SetSharingDialogOpen { } } +export function setImportWizardOpen(open: boolean): SetImportWizardOpen { + return { + action: 'SET_IMPORT_WIZARD_OPEN', + open: open, + } +} + export function resetOnlineState(): ResetOnlineState { return { action: 'RESET_ONLINE_STATE', diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index a695b30ad16a..09b87f449f15 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -137,6 +137,8 @@ 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': return true case 'TRUE_UP_ELEMENTS': diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index b70275861002..77c1e84cdad9 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -352,6 +352,8 @@ import type { ToggleDataCanCondense, UpdateMetadataInEditorState, SetErrorBoundaryHandling, + SetImportWizardOpen, + UpdateImportOperations, } from '../action-types' import { isAlignment, isLoggedIn } from '../action-types' import type { Mode } from '../editor-modes' @@ -1033,6 +1035,7 @@ export function restoreEditorState( githubSettings: currentEditor.githubSettings, imageDragSessionState: currentEditor.imageDragSessionState, githubOperations: currentEditor.githubOperations, + importOperations: currentEditor.importOperations, branchOriginContents: currentEditor.branchOriginContents, githubData: currentEditor.githubData, refreshingDependencies: currentEditor.refreshingDependencies, @@ -1044,6 +1047,7 @@ export function restoreEditorState( forking: currentEditor.forking, collaborators: currentEditor.collaborators, sharingDialogOpen: currentEditor.sharingDialogOpen, + importWizardOpen: currentEditor.importWizardOpen, editorRemixConfig: currentEditor.editorRemixConfig, } } @@ -2232,6 +2236,48 @@ export const UPDATE_FNS = { githubOperations: operations, } }, + UPDATE_IMPORT_OPERATIONS: (action: UpdateImportOperations, editor: EditorModel): EditorModel => { + const operations = [...editor.importOperations] + switch (action.type) { + case 'add': + action.operations.forEach((operation) => { + operations.push(operation) + }) + break + case 'remove': + // remove according to name + action.operations.forEach((operation) => { + const idx = operations.findIndex((op) => op.name === operation.name) + if (idx >= 0) { + operations.splice(idx, 1) + } + }) + break + case 'update': + // update fields according to name + action.operations.forEach((operation) => { + const idx = operations.findIndex((op) => op.name === operation.name) + if (idx >= 0) { + operations[idx] = { + ...operations[idx], + ...operation, + } + } + // if not found, add it + if (idx === -1) { + operations.push(operation) + } + }) + break + default: + const _exhaustiveCheck: never = action.type + throw new Error('Unknown operation type.') + } + return { + ...editor, + importOperations: operations, + } + }, SET_REFRESHING_DEPENDENCIES: ( action: SetRefreshingDependencies, editor: EditorModel, @@ -6121,6 +6167,12 @@ export const UPDATE_FNS = { sharingDialogOpen: action.open, } }, + SET_IMPORT_WIZARD_OPEN: (action: SetImportWizardOpen, editor: EditorModel): EditorModel => { + return { + ...editor, + importWizardOpen: action.open, + } + }, SET_ERROR_BOUNDARY_HANDLING: ( action: SetErrorBoundaryHandling, editor: EditorModel, diff --git a/editor/src/components/editor/editor-component.tsx b/editor/src/components/editor/editor-component.tsx index e4d1cfacfe0b..e6638c88be3f 100644 --- a/editor/src/components/editor/editor-component.tsx +++ b/editor/src/components/editor/editor-component.tsx @@ -96,6 +96,7 @@ import { navigatorTargetsSelector, navigatorTargetsSelectorNavigatorTargets, } from '../navigator/navigator-utils' +import { ImportWizard } from './import-wizard/import-wizard' const liveModeToastId = 'play-mode-toast' @@ -520,6 +521,7 @@ export const EditorComponentInner = React.memo((props: EditorProps) => { + {portalTarget != null ? ReactDOM.createPortal(, portalTarget) diff --git a/editor/src/components/editor/import-wizard/import-wizard-service.tsx b/editor/src/components/editor/import-wizard/import-wizard-service.tsx new file mode 100644 index 000000000000..704eb6a2eb0b --- /dev/null +++ b/editor/src/components/editor/import-wizard/import-wizard-service.tsx @@ -0,0 +1,91 @@ +import type { EditorDispatch } from '../action-types' +import { setImportWizardOpen, updateImportOperations } from '../actions/action-creators' +import type { GithubRepo } from '../store/editor-state' + +export function startImportWizard(dispatch: EditorDispatch) { + dispatch([ + setImportWizardOpen(true), + updateImportOperations( + [ + { + name: 'loadBranch', + branchName: '', + githubRepo: { + owner: '', + repository: '', + }, + }, + { + name: 'loadRepositories', + githubRepos: [], + }, + ], + 'add', + ), + ]) +} + +export function showImportWizard(dispatch: EditorDispatch) { + dispatch([setImportWizardOpen(true)]) +} + +export function hideImportWizard(dispatch: EditorDispatch) { + dispatch([setImportWizardOpen(false)]) +} + +export function updateOperation(dispatch: EditorDispatch, operation: ImportOperation) { + dispatch([updateImportOperations([operation], 'update')]) +} + +export function addOperation(dispatch: EditorDispatch, operation: ImportOperation) { + dispatch([updateImportOperations([operation], 'add')]) +} + +export function removeOperation(dispatch: EditorDispatch, operation: ImportOperation) { + dispatch([updateImportOperations([operation], 'remove')]) +} + +export function notifyOperationStarted(dispatch: EditorDispatch, operation: ImportOperation) { + const operationWithTime = { + ...operation, + timeStarted: Date.now(), + } + dispatch([updateImportOperations([operationWithTime], 'update')]) +} + +export function notifyOperationFinished( + dispatch: EditorDispatch, + operation: ImportOperation, + result: ImportOperationResult, +) { + const operationWithTime = { + ...operation, + timeDone: Date.now(), + result: result, + } + dispatch([updateImportOperations([operationWithTime], 'update')]) +} + +type ImportOperationData = { + timeStarted?: number + timeDone?: number + result?: ImportOperationResult + error?: string +} + +type ImportOperationResult = 'success' | 'error' | 'partial' + +type ImportLoadBranch = { + name: 'loadBranch' + branchName: string + githubRepo: GithubRepo +} & ImportOperationData + +type ImportLoadRepositories = { + name: 'loadRepositories' + githubRepos: GithubRepo[] +} & ImportOperationData + +export type ImportOperation = ImportLoadBranch | ImportLoadRepositories + +export type ImportOperationType = 'add' | 'remove' | 'update' diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx new file mode 100644 index 000000000000..5f56696e80e8 --- /dev/null +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -0,0 +1,91 @@ +import React from 'react' +import { getProjectID } from '../../../common/env-vars' +import { Button, Icons, useColorTheme, UtopiaStyles } from '../../../uuiui' +import { useDispatch } from '../store/dispatch-context' +import { useEditorState, Substores } from '../store/store-hook' +import { when } from '../../../utils/react-conditionals' +import { hideImportWizard } from './import-wizard-service' + +export const ImportWizard = React.memo(() => { + const dispatch = useDispatch() + const colorTheme = useColorTheme() + + const projectId = getProjectID() + + const importWizardOpen: boolean = useEditorState( + Substores.restOfEditor, + (store) => store.editor.importWizardOpen, + 'ImportWizard importWizardOpen', + ) + + const operations = useEditorState( + Substores.github, + (store) => store.editor.importOperations, + 'ImportWizard operations', + ) + + const handleDismiss = React.useCallback(() => { + hideImportWizard(dispatch) + }, [dispatch]) + + const stopPropagation = React.useCallback((e: React.MouseEvent) => { + e.stopPropagation() + }, []) + + if (projectId == null) { + return null + } + + return ( +
+ {when( + importWizardOpen, +
+ +
{JSON.stringify(operations)}
+
, + )} +
+ ) +}) +ImportWizard.displayName = 'ImportWizard' diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 800ae1ae55a3..fff1fafc36bf 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -189,6 +189,7 @@ 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 '../import-wizard/import-wizard-service' const ObjectPathImmutable: any = OPI @@ -1452,6 +1453,7 @@ export interface EditorState { githubSettings: ProjectGithubSettings imageDragSessionState: ImageDragSessionState githubOperations: Array + importOperations: Array githubData: GithubData refreshingDependencies: boolean colorSwatches: Array @@ -1462,6 +1464,7 @@ export interface EditorState { forking: boolean collaborators: Collaborator[] sharingDialogOpen: boolean + importWizardOpen: boolean editorRemixConfig: EditorRemixConfig } @@ -1535,6 +1538,7 @@ export function editorState( githubSettings: ProjectGithubSettings, imageDragSessionState: ImageDragSessionState, githubOperations: Array, + importOperations: Array, branchOriginContents: ProjectContentTreeRoot | null, githubData: GithubData, refreshingDependencies: boolean, @@ -1546,6 +1550,7 @@ export function editorState( forking: boolean, collaborators: Collaborator[], sharingDialogOpen: boolean, + importWizardOpen: boolean, remixConfig: EditorRemixConfig, ): EditorState { return { @@ -1619,6 +1624,7 @@ export function editorState( githubSettings: githubSettings, imageDragSessionState: imageDragSessionState, githubOperations: githubOperations, + importOperations: importOperations, githubData: githubData, refreshingDependencies: refreshingDependencies, colorSwatches: colorSwatches, @@ -1629,6 +1635,7 @@ export function editorState( forking: forking, collaborators: collaborators, sharingDialogOpen: sharingDialogOpen, + importWizardOpen: importWizardOpen, editorRemixConfig: remixConfig, } } @@ -2696,6 +2703,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { githubSettings: emptyGithubSettings(), imageDragSessionState: notDragging(), githubOperations: [], + importOperations: [], branchOriginContents: null, githubData: emptyGithubData(), refreshingDependencies: false, @@ -2710,6 +2718,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { forking: false, collaborators: [], sharingDialogOpen: false, + importWizardOpen: false, editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, @@ -3063,6 +3072,7 @@ export function editorModelFromPersistentModel( githubSettings: persistentModel.githubSettings, imageDragSessionState: notDragging(), githubOperations: [], + importOperations: [], refreshingDependencies: false, branchOriginContents: null, githubData: emptyGithubData(), @@ -3077,6 +3087,7 @@ export function editorModelFromPersistentModel( forking: false, collaborators: [], sharingDialogOpen: false, + importWizardOpen: false, editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index e663daa121e8..bfc6d977b7bb 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -220,6 +220,8 @@ export function runSimpleLocalEditorAction( return UPDATE_FNS.SET_REFRESHING_DEPENDENCIES(action, state) case 'UPDATE_GITHUB_OPERATIONS': return UPDATE_FNS.UPDATE_GITHUB_OPERATIONS(action, state) + case 'SET_IMPORT_WIZARD_OPEN': + return UPDATE_FNS.SET_IMPORT_WIZARD_OPEN(action, state) case 'REMOVE_TOAST': return UPDATE_FNS.REMOVE_TOAST(action, state) case 'SET_HIGHLIGHTED_VIEWS': 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 f22431510ddb..216e75c26c1c 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -643,6 +643,7 @@ 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 '../import-wizard/import-wizard-service' export function ElementPropertyPathKeepDeepEquality(): KeepDeepEqualityCall { return combine2EqualityCalls( @@ -4825,9 +4826,22 @@ export const GithubOperationKeepDeepEquality: KeepDeepEqualityCall = ( + oldValue, + newValue, +) => { + if (oldValue.name !== newValue.name) { + return keepDeepEqualityResult(newValue, false) + } + return keepDeepEqualityResult(oldValue, true) +} + export const GithubOperationsKeepDeepEquality: KeepDeepEqualityCall> = arrayDeepEquality(GithubOperationKeepDeepEquality) +export const ImportOperationsKeepDeepEquality: KeepDeepEqualityCall> = + arrayDeepEquality(ImportOperationKeepDeepEquality) + export const ColorSwatchDeepEquality: KeepDeepEqualityCall = combine2EqualityCalls( (c) => c.id, StringKeepDeepEquality, @@ -5358,6 +5372,11 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( newValue.githubOperations, ) + const importOperationsResults = arrayDeepEquality(ImportOperationKeepDeepEquality)( + oldValue.importOperations, + newValue.importOperations, + ) + const branchContentsResults = nullableDeepEquality(ProjectContentTreeRootKeepDeepEquality())( oldValue.branchOriginContents, newValue.branchOriginContents, @@ -5405,6 +5424,11 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( newValue.sharingDialogOpen, ) + const importWizardOpenResults = BooleanKeepDeepEquality( + oldValue.importWizardOpen, + newValue.importWizardOpen, + ) + const remixConfigResults = RemixConfigKeepDeepEquality( oldValue.editorRemixConfig, newValue.editorRemixConfig, @@ -5479,6 +5503,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( githubSettingsResults.areEqual && imageDragSessionStateEqual.areEqual && githubOperationsResults.areEqual && + importOperationsResults.areEqual && branchContentsResults.areEqual && githubDataResults.areEqual && refreshingDependenciesResults.areEqual && @@ -5490,6 +5515,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( forkingResults.areEqual && collaboratorsResults.areEqual && sharingDialogOpenResults.areEqual && + importWizardOpenResults.areEqual && remixConfigResults.areEqual if (areEqual) { @@ -5565,6 +5591,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( githubSettingsResults.value, imageDragSessionStateEqual.value, githubOperationsResults.value, + importOperationsResults.value, branchContentsResults.value, githubDataResults.value, refreshingDependenciesResults.value, @@ -5576,6 +5603,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( forkingResults.value, collaboratorsResults.value, sharingDialogOpenResults.value, + importWizardOpenResults.value, remixConfigResults.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 61e8565823a4..686dbffc1c01 100644 --- a/editor/src/components/editor/store/store-hook-substore-helpers.ts +++ b/editor/src/components/editor/store/store-hook-substore-helpers.ts @@ -157,6 +157,7 @@ export const EmptyEditorStateForKeysOnly: EditorState = { githubSettings: null as any, imageDragSessionState: null as any, githubOperations: [], + importOperations: [], branchOriginContents: null, githubData: null as any, refreshingDependencies: false, @@ -171,6 +172,7 @@ export const EmptyEditorStateForKeysOnly: EditorState = { forking: false, collaborators: [], sharingDialogOpen: false, + importWizardOpen: false, editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, 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 c6a0fd468326..ce685e2d7dfe 100644 --- a/editor/src/components/editor/store/store-hook-substore-types.ts +++ b/editor/src/components/editor/store/store-hook-substore-types.ts @@ -174,7 +174,12 @@ export interface ThemeSubstate { } // GithubSubstate -export const githubSubstateKeys = ['githubSettings', 'githubOperations', 'githubData'] as const +export const githubSubstateKeys = [ + 'githubSettings', + 'githubOperations', + 'githubData', + 'importOperations', +] as const export const emptyGithubSubstate = { editor: pick(githubSubstateKeys, EmptyEditorStateForKeysOnly), } as const diff --git a/editor/src/components/navigator/left-pane/github-pane/index.tsx b/editor/src/components/navigator/left-pane/github-pane/index.tsx index 14e09ea9f02f..2e9541b1c39f 100644 --- a/editor/src/components/navigator/left-pane/github-pane/index.tsx +++ b/editor/src/components/navigator/left-pane/github-pane/index.tsx @@ -55,6 +55,10 @@ import { GithubOperations } from '../../../../core/shared/github/operations' import { useOnClickAuthenticateWithGithub } from '../../../../utils/github-auth-hooks' import { setFocus } from '../../../common/actions' import { OperationContext } from '../../../../core/shared/github/operations/github-operation-context' +import { + showImportWizard, + startImportWizard, +} from '../../../../components/editor/import-wizard/import-wizard-service' const compactTimeagoFormatter = (value: number, unit: string) => { return `${value}${unit.charAt(0)}` @@ -931,6 +935,7 @@ const BranchNotLoadedBlock = () => { const loadFromBranch = React.useCallback(() => { if (githubRepo != null && branchName != null && githubUserDetails != null) { + startImportWizard(dispatch) void GithubOperations.updateProjectWithBranchContent( workersRef.current, dispatch, diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index 88c4f2d2d8e4..3cfdce9972e3 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -39,6 +39,10 @@ import type { ExistingAsset } from '../../../../components/editor/server' import { GithubOperations } from '.' import { assertNever } from '../../utils' import { updateProjectContentsWithParseResults } from '../../parser-projectcontents-utils' +import { + notifyOperationFinished, + notifyOperationStarted, +} from '../../../../components/editor/import-wizard/import-wizard-service' export const saveAssetsToProject = (operationContext: GithubOperationContext) => @@ -115,6 +119,11 @@ export const updateProjectWithBranchContent = currentProjectContents: ProjectContentTreeRoot, initiator: GithubOperationSource, ): Promise => { + notifyOperationStarted(dispatch, { + name: 'loadBranch', + branchName: branchName, + githubRepo: githubRepo, + }) await runGithubOperation( { name: 'loadBranch', @@ -148,6 +157,16 @@ export const updateProjectWithBranchContent = newGithubData.branches = null } + notifyOperationFinished( + dispatch, + { + name: 'loadBranch', + branchName: branchName, + githubRepo: githubRepo, + }, + 'success', + ) + // Push any code through the parser so that the representations we end up with are in a state of `BOTH_MATCH`. // So that it will override any existing files that might already exist in the project when sending them to VS Code. const parsedProjectContents = createStoryboardFileIfNecessary( From 536c7474177d5eb30ef33de13f9f931b66e23774 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Wed, 9 Oct 2024 17:57:37 +0300 Subject: [PATCH 02/25] ui and functionality --- editor/src/components/editor/action-types.ts | 4 +- .../editor/actions/action-creators.ts | 4 +- .../src/components/editor/actions/actions.tsx | 44 +----- .../components/editor/editor-component.tsx | 11 +- .../import-wizard/import-wizard-service.tsx | 91 ----------- .../editor/import-wizard/import-wizard.tsx | 143 ++++++++++++++++-- .../components/editor/store/editor-state.ts | 17 ++- .../components/editor/store/editor-update.tsx | 2 + .../store/store-deep-equality-instances.ts | 4 +- .../package-manager/fetch-packages.ts | 27 ++++ .../shared/github/operations/load-branch.ts | 53 +++++-- 11 files changed, 231 insertions(+), 169 deletions(-) delete mode 100644 editor/src/components/editor/import-wizard/import-wizard-service.tsx diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index cae8a6ea6f89..1ff2abe37d2d 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -87,7 +87,7 @@ 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, ImportOperationType } from './import-wizard/import-wizard-service' +import type { ImportOperation, ImportOperationAction } from './import-wizard/import-wizard-service' export { isLoggedIn, loggedInUser, notLoggedIn } from '../../common/user' export type { LoginState, UserDetails } from '../../common/user' @@ -1001,7 +1001,7 @@ export interface UpdateGithubOperations { export interface UpdateImportOperations { action: 'UPDATE_IMPORT_OPERATIONS' operations: ImportOperation[] - type: ImportOperationType + type: ImportOperationAction } export interface SetRefreshingDependencies { diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index f1618c41d272..6c29c8caaeab 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -270,7 +270,7 @@ 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, ImportOperationType } from '../import-wizard/import-wizard-service' +import type { ImportOperation, ImportOperationAction } from '../import-wizard/import-wizard-service' export function clearSelection(): EditorAction { return { @@ -1596,7 +1596,7 @@ export function resetCanvas(): ResetCanvas { export function updateImportOperations( operations: ImportOperation[], - type: ImportOperationType, + type: ImportOperationAction, ): UpdateImportOperations { return { action: 'UPDATE_IMPORT_OPERATIONS', diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 77c1e84cdad9..6691e0ad89e7 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -632,6 +632,7 @@ import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-ut import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils' import { applyValuesAtPath } from '../../canvas/commands/adjust-number-command' import { styleP } from '../../inspector/inspector-common' +import { getUpdateOperationResult } from '../import-wizard/import-wizard-service' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -2237,45 +2238,14 @@ export const UPDATE_FNS = { } }, UPDATE_IMPORT_OPERATIONS: (action: UpdateImportOperations, editor: EditorModel): EditorModel => { - const operations = [...editor.importOperations] - switch (action.type) { - case 'add': - action.operations.forEach((operation) => { - operations.push(operation) - }) - break - case 'remove': - // remove according to name - action.operations.forEach((operation) => { - const idx = operations.findIndex((op) => op.name === operation.name) - if (idx >= 0) { - operations.splice(idx, 1) - } - }) - break - case 'update': - // update fields according to name - action.operations.forEach((operation) => { - const idx = operations.findIndex((op) => op.name === operation.name) - if (idx >= 0) { - operations[idx] = { - ...operations[idx], - ...operation, - } - } - // if not found, add it - if (idx === -1) { - operations.push(operation) - } - }) - break - default: - const _exhaustiveCheck: never = action.type - throw new Error('Unknown operation type.') - } + const resultImportOperations = getUpdateOperationResult( + editor.importOperations, + action.operations, + action.type, + ) return { ...editor, - importOperations: operations, + importOperations: resultImportOperations, } }, SET_REFRESHING_DEPENDENCIES: ( diff --git a/editor/src/components/editor/editor-component.tsx b/editor/src/components/editor/editor-component.tsx index e6638c88be3f..48edb31f5bae 100644 --- a/editor/src/components/editor/editor-component.tsx +++ b/editor/src/components/editor/editor-component.tsx @@ -679,11 +679,12 @@ const LockedOverlay = React.memo(() => { const editorLocked = React.useMemo(() => githubOperations.length > 0, [githubOperations]) - const refreshingDependencies = useEditorState( - Substores.restOfEditor, - (store) => store.editor.refreshingDependencies, - 'LockedOverlay refreshingDependencies', - ) + const refreshingDependencies = false + // useEditorState( + // Substores.restOfEditor, + // (store) => store.editor.refreshingDependencies, + // 'LockedOverlay refreshingDependencies', + // ) const forking = useEditorState( Substores.restOfEditor, diff --git a/editor/src/components/editor/import-wizard/import-wizard-service.tsx b/editor/src/components/editor/import-wizard/import-wizard-service.tsx deleted file mode 100644 index 704eb6a2eb0b..000000000000 --- a/editor/src/components/editor/import-wizard/import-wizard-service.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type { EditorDispatch } from '../action-types' -import { setImportWizardOpen, updateImportOperations } from '../actions/action-creators' -import type { GithubRepo } from '../store/editor-state' - -export function startImportWizard(dispatch: EditorDispatch) { - dispatch([ - setImportWizardOpen(true), - updateImportOperations( - [ - { - name: 'loadBranch', - branchName: '', - githubRepo: { - owner: '', - repository: '', - }, - }, - { - name: 'loadRepositories', - githubRepos: [], - }, - ], - 'add', - ), - ]) -} - -export function showImportWizard(dispatch: EditorDispatch) { - dispatch([setImportWizardOpen(true)]) -} - -export function hideImportWizard(dispatch: EditorDispatch) { - dispatch([setImportWizardOpen(false)]) -} - -export function updateOperation(dispatch: EditorDispatch, operation: ImportOperation) { - dispatch([updateImportOperations([operation], 'update')]) -} - -export function addOperation(dispatch: EditorDispatch, operation: ImportOperation) { - dispatch([updateImportOperations([operation], 'add')]) -} - -export function removeOperation(dispatch: EditorDispatch, operation: ImportOperation) { - dispatch([updateImportOperations([operation], 'remove')]) -} - -export function notifyOperationStarted(dispatch: EditorDispatch, operation: ImportOperation) { - const operationWithTime = { - ...operation, - timeStarted: Date.now(), - } - dispatch([updateImportOperations([operationWithTime], 'update')]) -} - -export function notifyOperationFinished( - dispatch: EditorDispatch, - operation: ImportOperation, - result: ImportOperationResult, -) { - const operationWithTime = { - ...operation, - timeDone: Date.now(), - result: result, - } - dispatch([updateImportOperations([operationWithTime], 'update')]) -} - -type ImportOperationData = { - timeStarted?: number - timeDone?: number - result?: ImportOperationResult - error?: string -} - -type ImportOperationResult = 'success' | 'error' | 'partial' - -type ImportLoadBranch = { - name: 'loadBranch' - branchName: string - githubRepo: GithubRepo -} & ImportOperationData - -type ImportLoadRepositories = { - name: 'loadRepositories' - githubRepos: GithubRepo[] -} & ImportOperationData - -export type ImportOperation = ImportLoadBranch | ImportLoadRepositories - -export type ImportOperationType = 'add' | 'remove' | 'update' diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 5f56696e80e8..08e68ffefaf0 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -1,13 +1,16 @@ -import React from 'react' +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@emotion/react' +import React, { useMemo } from 'react' import { getProjectID } from '../../../common/env-vars' import { Button, Icons, useColorTheme, UtopiaStyles } from '../../../uuiui' -import { useDispatch } from '../store/dispatch-context' +import { GithubSpinner } from '../../navigator/left-pane/github-pane/github-spinner' import { useEditorState, Substores } from '../store/store-hook' import { when } from '../../../utils/react-conditionals' -import { hideImportWizard } from './import-wizard-service' +import type { ImportOperation } from './import-wizard-service' +import { getImportOperationText, hideImportWizard } from './import-wizard-service' export const ImportWizard = React.memo(() => { - const dispatch = useDispatch() const colorTheme = useColorTheme() const projectId = getProjectID() @@ -25,8 +28,8 @@ export const ImportWizard = React.memo(() => { ) const handleDismiss = React.useCallback(() => { - hideImportWizard(dispatch) - }, [dispatch]) + hideImportWizard() + }, []) const stopPropagation = React.useCallback((e: React.MouseEvent) => { e.stopPropagation() @@ -59,13 +62,18 @@ export const ImportWizard = React.memo(() => { background: colorTheme.bg0.value, boxShadow: UtopiaStyles.popup.boxShadow, borderRadius: 10, - width: '500px', - height: '500px', + width: 610, + minHeight: 500, + maxHeight: 770, position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', - justifyContent: 'center', + fontSize: '14px', + lineHeight: 'normal', + letterSpacing: 'normal', + padding: 40, + overflow: 'hidden', }} onClick={stopPropagation} > @@ -82,10 +90,125 @@ export const ImportWizard = React.memo(() => { > -
{JSON.stringify(operations)}
+
+ {operations.map((operation, index) => ( + + ))} +
, )} ) }) ImportWizard.displayName = 'ImportWizard' + +function OperationLine({ operation }: { operation: ImportOperation }) { + const operationStatus = useMemo(() => { + if (operation.timeStarted == null) { + return 'not started' + } + if (operation.timeDone == null) { + return 'running' + } + return `done` + }, [operation.timeStarted, operation.timeDone]) + const textColor = React.useMemo(() => { + if (operationStatus === 'not started') { + return 'gray' + } else if (operationStatus === 'running') { + return 'black' + } else { + return operation.result === 'success' ? 'green' : 'red' + } + }, [operationStatus, operation.result]) + return ( +
&': { + paddingLeft: 10, + fontSize: 12, + }, + }} + > +
+ +
{getImportOperationText(operation)}
+
+ +
+
+ {operation.children != null && operation.children.length > 0 && ( +
+ {operation.children.map((child, index) => ( + + ))} +
+ )} +
+ ) +} + +function OperationIcon({ status, result }: { status: string; result?: string }) { + if (status === 'running') { + return + } else if (status === 'done' && result === 'success') { + return + } else if (status === 'not started') { + return + } else { + return + } +} + +function TimeFromInSeconds({ startTime, endTime }: { startTime?: number; endTime?: number }) { + const [currentTime, setCurrentTime] = React.useState(Date.now()) + React.useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(Date.now()) + }, 1000) + return () => clearInterval(interval) + }, []) + const time = useMemo(() => { + if (startTime == null) { + return 0 + } + if (endTime == null) { + return currentTime - startTime + } + return endTime - startTime + }, [startTime, endTime, currentTime]) + const timeInSeconds = + endTime != null ? (time / 1000).toFixed(2) : Math.max(Math.floor(time / 1000), 0) + return startTime == null ? null :
{timeInSeconds}s
+} diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index fff1fafc36bf..b29aeb398367 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -349,14 +349,15 @@ export type GithubOperation = | GithubSearchRepository export function githubOperationLocksEditor(op: GithubOperation): boolean { - switch (op.name) { - case 'listBranches': - case 'loadRepositories': - case 'listPullRequestsForBranch': - return false - default: - return true - } + return false + // switch (op.name) { + // case 'listBranches': + // case 'loadRepositories': + // case 'listPullRequestsForBranch': + // return false + // default: + // return true + // } } export function isGithubLoadingBranch( diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index bfc6d977b7bb..8c0634e4be53 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -220,6 +220,8 @@ export function runSimpleLocalEditorAction( return UPDATE_FNS.SET_REFRESHING_DEPENDENCIES(action, state) case 'UPDATE_GITHUB_OPERATIONS': return UPDATE_FNS.UPDATE_GITHUB_OPERATIONS(action, state) + case 'UPDATE_IMPORT_OPERATIONS': + return UPDATE_FNS.UPDATE_IMPORT_OPERATIONS(action, state) case 'SET_IMPORT_WIZARD_OPEN': return UPDATE_FNS.SET_IMPORT_WIZARD_OPEN(action, state) case 'REMOVE_TOAST': 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 216e75c26c1c..532ddf4aceb7 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -4830,7 +4830,9 @@ export const ImportOperationKeepDeepEquality: KeepDeepEqualityCall { - if (oldValue.name !== newValue.name) { + if (oldValue.type !== newValue.type) { + return keepDeepEqualityResult(newValue, false) + } else if (oldValue.id !== newValue.id) { return keepDeepEqualityResult(newValue, false) } return keepDeepEqualityResult(oldValue, true) diff --git a/editor/src/core/es-modules/package-manager/fetch-packages.ts b/editor/src/core/es-modules/package-manager/fetch-packages.ts index 59c33a1d2480..377a36e623b9 100644 --- a/editor/src/core/es-modules/package-manager/fetch-packages.ts +++ b/editor/src/core/es-modules/package-manager/fetch-packages.ts @@ -47,6 +47,10 @@ import { isBuiltInDependency } from './built-in-dependencies' import type { BuiltInDependencies } from './built-in-dependencies-list' import { mangleNodeModulePaths, mergeNodeModules } from './merge-modules' import { getJsDelivrFileUrl, getPackagerUrl } from './packager-url' +import { + notifyOperationFinished, + notifyOperationStarted, +} from '../../../components/editor/import-wizard/import-wizard-service' let depPackagerCache: { [key: string]: PackagerServerResponse } = {} @@ -287,13 +291,33 @@ export async function fetchNodeModules( const nodeModulesArr = await Promise.all( dependenciesToDownload.map( async (newDep): Promise> => { + function notifyEnd(result: 'success' | 'error') { + notifyOperationFinished( + { + type: 'fetchDependency', + id: `${newDep.name}@${newDep.version}`, + parentOperationType: 'refreshDependencies', + dependencyName: newDep.name, + dependencyVersion: newDep.version, + }, + result, + ) + } try { + notifyOperationStarted({ + type: 'fetchDependency', + id: `${newDep.name}@${newDep.version}`, + parentOperationType: 'refreshDependencies', + dependencyName: newDep.name, + dependencyVersion: newDep.version, + }) const matchingVersionResponse = await findMatchingVersion( newDep.name, newDep.version, 'skipFetch', ) if (isPackageNotFound(matchingVersionResponse)) { + notifyEnd('error') return left(failNotFound(newDep)) } @@ -323,12 +347,15 @@ export async function fetchNodeModules( * the real nice solution would be to apply npm's module resolution logic that * pulls up shared transitive dependencies to the main /node_modules/ folder. */ + notifyEnd('success') return right(mangleNodeModulePaths(newDep.name, packagerResponse)) } else { + notifyEnd('error') return left(failError(newDep)) } } catch (e) { // TODO: proper error handling, now we don't show error for a missing package. The error will be visible when you try to import + notifyEnd('error') return left(failError(newDep)) } }, diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index 3cfdce9972e3..d817719701ef 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -42,6 +42,7 @@ import { updateProjectContentsWithParseResults } from '../../parser-projectconte import { notifyOperationFinished, notifyOperationStarted, + startImportWizard, } from '../../../../components/editor/import-wizard/import-wizard-service' export const saveAssetsToProject = @@ -119,8 +120,9 @@ export const updateProjectWithBranchContent = currentProjectContents: ProjectContentTreeRoot, initiator: GithubOperationSource, ): Promise => { - notifyOperationStarted(dispatch, { - name: 'loadBranch', + startImportWizard(dispatch) + notifyOperationStarted({ + type: 'loadBranch', branchName: branchName, githubRepo: githubRepo, }) @@ -157,23 +159,45 @@ export const updateProjectWithBranchContent = newGithubData.branches = null } - notifyOperationFinished( - dispatch, - { - name: 'loadBranch', - branchName: branchName, - githubRepo: githubRepo, - }, - 'success', - ) + notifyOperationFinished({ type: 'loadBranch' }, 'success') + + notifyOperationStarted({ type: 'parseFiles' }) // Push any code through the parser so that the representations we end up with are in a state of `BOTH_MATCH`. // So that it will override any existing files that might already exist in the project when sending them to VS Code. + const parseResults = await updateProjectContentsWithParseResults( + workers, + responseBody.branch.content, + ) + + notifyOperationFinished({ type: 'parseFiles' }, 'success') + + notifyOperationStarted({ type: 'checkUtopiaRequirements' }) + notifyOperationStarted({ + type: 'createStoryboard', + parentOperationType: 'checkUtopiaRequirements', + }) + const parsedProjectContents = createStoryboardFileIfNecessary( - await updateProjectContentsWithParseResults(workers, responseBody.branch.content), + parseResults, 'create-placeholder', ) + notifyOperationFinished( + { type: 'createStoryboard', parentOperationType: 'checkUtopiaRequirements' }, + 'success', + ) + notifyOperationStarted({ + type: 'createPackageJsonEntry', + parentOperationType: 'checkUtopiaRequirements', + }) + // here will be the code to create package.json entry + notifyOperationFinished( + { type: 'createPackageJsonEntry', parentOperationType: 'checkUtopiaRequirements' }, + 'success', + ) + notifyOperationFinished({ type: 'checkUtopiaRequirements' }, 'success') + // Update the editor with everything so that if anything else fails past this point // there's no loss of data from the user's perspective. dispatch( @@ -199,13 +223,16 @@ export const updateProjectWithBranchContent = let dependenciesPromise: Promise = Promise.resolve() const packageJson = packageJsonFileFromProjectContents(parsedProjectContents) if (packageJson != null && isTextFile(packageJson)) { + notifyOperationStarted({ type: 'refreshDependencies' }) dependenciesPromise = refreshDependencies( dispatch, packageJson.fileContents.code, currentDeps, builtInDependencies, {}, - ).then(() => {}) + ).then(() => { + notifyOperationFinished({ type: 'refreshDependencies' }, 'success') + }) } // When the dependencies update has gone through, then indicate that the project was imported. From a8989565859c9d3389218b32412328f1aa8265cc Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Wed, 9 Oct 2024 18:03:13 +0300 Subject: [PATCH 03/25] file --- .../import-wizard/import-wizard-service.ts | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 editor/src/components/editor/import-wizard/import-wizard-service.ts diff --git a/editor/src/components/editor/import-wizard/import-wizard-service.ts b/editor/src/components/editor/import-wizard/import-wizard-service.ts new file mode 100644 index 000000000000..40762b71915d --- /dev/null +++ b/editor/src/components/editor/import-wizard/import-wizard-service.ts @@ -0,0 +1,220 @@ +import { assertNever } from '../../../core/shared/utils' +import type { EditorDispatch } from '../action-types' +import { setImportWizardOpen, updateImportOperations } from '../actions/action-creators' +import type { GithubRepo } from '../store/editor-state' + +let editorDispatch: EditorDispatch | null = null + +export function startImportWizard(dispatch: EditorDispatch) { + editorDispatch = dispatch + dispatch([ + setImportWizardOpen(true), + updateImportOperations( + [ + { + type: 'loadBranch', + }, + { + type: 'parseFiles', + }, + { + type: 'checkUtopiaRequirements', + }, + { + type: 'refreshDependencies', + }, + ], + 'replace', + ), + ]) +} + +export function showImportWizard() { + editorDispatch?.([setImportWizardOpen(true)]) +} + +export function hideImportWizard() { + editorDispatch?.([setImportWizardOpen(false)]) +} + +export function updateOperation(operation: ImportOperation) { + editorDispatch?.([updateImportOperations([operation], 'update')]) +} + +export function addOperation(operation: ImportOperation) { + editorDispatch?.([updateImportOperations([operation], 'add')]) +} + +export function removeOperation(operation: ImportOperation) { + editorDispatch?.([updateImportOperations([operation], 'remove')]) +} + +export function notifyOperationStarted(operation: ImportOperation) { + const operationWithTime = { + ...operation, + timeStarted: Date.now(), + } + editorDispatch?.([updateImportOperations([operationWithTime], 'update')]) +} + +export function notifyOperationFinished(operation: ImportOperation, result: ImportOperationResult) { + const operationWithTime = { + ...operation, + timeDone: Date.now(), + result: result, + } + editorDispatch?.([updateImportOperations([operationWithTime], 'update')]) +} + +export function areSameOperation(existing: ImportOperation, incoming: ImportOperation): boolean { + if (existing.id == null || incoming.id == null) { + return existing.type === incoming.type + } + return existing.id === incoming.id +} + +type ImportOperationData = { + id?: string | null + timeStarted?: number + timeDone?: number + result?: ImportOperationResult + error?: string + parentOperationType?: ImportOperationType + children?: ImportOperation[] +} + +type ImportOperationResult = 'success' | 'error' | 'partial' + +type ImportLoadBranch = { + type: 'loadBranch' + branchName?: string + githubRepo?: GithubRepo +} & ImportOperationData + +type ImportRefreshDependencies = { + type: 'refreshDependencies' +} & ImportOperationData + +type ImportFetchDependency = { + type: 'fetchDependency' + dependencyName: string + dependencyVersion: string +} & ImportOperationData + +type ImportParseFiles = { + type: 'parseFiles' +} & ImportOperationData + +type ImportCreateStoryboard = { + type: 'createStoryboard' +} & ImportOperationData + +type ImportCreatePackageJsonEntry = { + type: 'createPackageJsonEntry' +} & ImportOperationData + +type ImportCheckUtopiaRequirements = { + type: 'checkUtopiaRequirements' +} & ImportOperationData + +export type ImportOperation = + | ImportLoadBranch + | ImportRefreshDependencies + | ImportParseFiles + | ImportCreateStoryboard + | ImportFetchDependency + | ImportCreatePackageJsonEntry + | ImportCheckUtopiaRequirements + +type ImportOperationType = ImportOperation['type'] + +export type ImportOperationAction = 'add' | 'remove' | 'update' | 'replace' + +export function getImportOperationText(operation: ImportOperation) { + switch (operation.type) { + case 'loadBranch': + return `Loading branch ${operation.githubRepo?.owner}/${operation.githubRepo?.repository}@${operation.branchName}` + case 'fetchDependency': + return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}` + case 'parseFiles': + return 'Parsing files' + case 'createStoryboard': + return 'Creating storyboard file' + case 'refreshDependencies': + return 'Fetching dependencies' + case 'createPackageJsonEntry': + return 'Creating package.json entry' + case 'checkUtopiaRequirements': + return 'Checking Utopia requirements' + default: + assertNever(operation) + } +} + +function getParentArray(root: ImportOperation[], operation: ImportOperation): ImportOperation[] { + if (operation.parentOperationType == null) { + return root + } + const parentIndex = root.findIndex((op) => op.type === operation.parentOperationType) + if (parentIndex === -1) { + return root + } + const parent = root[parentIndex] + if (parent.children == null) { + root[parentIndex] = { + ...parent, + children: [], + } + } + return root[parentIndex].children ?? [] +} + +export function getUpdateOperationResult( + existingOperations: ImportOperation[], + incomingOperations: ImportOperation[], + type: ImportOperationAction, +): ImportOperation[] { + let operations: ImportOperation[] = existingOperations.map((operation) => ({ + ...operation, + children: [...(operation.children ?? [])], + })) + switch (type) { + case 'add': + incomingOperations.forEach((operation) => { + const parent = getParentArray(operations, operation) + parent.push(operation) + }) + break + case 'remove': + incomingOperations.forEach((operation) => { + const parent = getParentArray(operations, operation) + const idx = parent.findIndex((op) => areSameOperation(op, operation)) + if (idx >= 0) { + parent.splice(idx, 1) + } + }) + break + case 'update': + incomingOperations.forEach((operation) => { + const parent = getParentArray(operations, operation) + const idx = parent.findIndex((op) => areSameOperation(op, operation)) + if (idx >= 0) { + parent[idx] = { + ...parent[idx], + ...operation, + } + } + // if not found, add it + if (idx === -1) { + parent.push(operation) + } + }) + break + case 'replace': + operations = [...incomingOperations] + break + default: + assertNever(type) + } + return operations +} From a715add05f216eeb7cbb9d8aca8f0350b82dc5a1 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Wed, 9 Oct 2024 23:39:22 +0300 Subject: [PATCH 04/25] collapse dependencies --- .../import-wizard/import-wizard-service.ts | 29 +-- .../editor/import-wizard/import-wizard.tsx | 227 ++++++++++++++---- .../navigator/left-pane/github-pane/index.tsx | 1 - editor/src/core/shared/dependencies.ts | 20 +- .../shared/github/operations/load-branch.ts | 4 +- 5 files changed, 202 insertions(+), 79 deletions(-) diff --git a/editor/src/components/editor/import-wizard/import-wizard-service.ts b/editor/src/components/editor/import-wizard/import-wizard-service.ts index 40762b71915d..1741952ee156 100644 --- a/editor/src/components/editor/import-wizard/import-wizard-service.ts +++ b/editor/src/components/editor/import-wizard/import-wizard-service.ts @@ -53,14 +53,16 @@ export function notifyOperationStarted(operation: ImportOperation) { const operationWithTime = { ...operation, timeStarted: Date.now(), + timeDone: null, } editorDispatch?.([updateImportOperations([operationWithTime], 'update')]) } export function notifyOperationFinished(operation: ImportOperation, result: ImportOperationResult) { + const timeDone = Date.now() const operationWithTime = { ...operation, - timeDone: Date.now(), + timeDone: timeDone, result: result, } editorDispatch?.([updateImportOperations([operationWithTime], 'update')]) @@ -75,8 +77,8 @@ export function areSameOperation(existing: ImportOperation, incoming: ImportOper type ImportOperationData = { id?: string | null - timeStarted?: number - timeDone?: number + timeStarted?: number | null + timeDone?: number | null result?: ImportOperationResult error?: string parentOperationType?: ImportOperationType @@ -130,27 +132,6 @@ type ImportOperationType = ImportOperation['type'] export type ImportOperationAction = 'add' | 'remove' | 'update' | 'replace' -export function getImportOperationText(operation: ImportOperation) { - switch (operation.type) { - case 'loadBranch': - return `Loading branch ${operation.githubRepo?.owner}/${operation.githubRepo?.repository}@${operation.branchName}` - case 'fetchDependency': - return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}` - case 'parseFiles': - return 'Parsing files' - case 'createStoryboard': - return 'Creating storyboard file' - case 'refreshDependencies': - return 'Fetching dependencies' - case 'createPackageJsonEntry': - return 'Creating package.json entry' - case 'checkUtopiaRequirements': - return 'Checking Utopia requirements' - default: - assertNever(operation) - } -} - function getParentArray(root: ImportOperation[], operation: ImportOperation): ImportOperation[] { if (operation.parentOperationType == null) { return root diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 08e68ffefaf0..fb9467dc41a9 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -8,7 +8,8 @@ import { GithubSpinner } from '../../navigator/left-pane/github-pane/github-spin import { useEditorState, Substores } from '../store/store-hook' import { when } from '../../../utils/react-conditionals' import type { ImportOperation } from './import-wizard-service' -import { getImportOperationText, hideImportWizard } from './import-wizard-service' +import { hideImportWizard } from './import-wizard-service' +import { assertNever } from '../../../core/shared/utils' export const ImportWizard = React.memo(() => { const colorTheme = useColorTheme() @@ -100,8 +101,8 @@ export const ImportWizard = React.memo(() => { width: '100%', }} > - {operations.map((operation, index) => ( - + {operations.map((operation) => ( + ))} , @@ -127,63 +128,100 @@ function OperationLine({ operation }: { operation: ImportOperation }) { } else if (operationStatus === 'running') { return 'black' } else { - return operation.result === 'success' ? 'green' : 'red' + return operation.result === 'success' + ? 'green' + : operation.result === 'partial' + ? 'orange' + : 'red' } }, [operationStatus, operation.result]) return ( -
&': { - paddingLeft: 10, - fontSize: 12, - }, - }} + + + +
{getImportOperationText(operation)}
+
+ +
+
+ +
+ ) +} + +function OperationChildrenList({ operation }: { operation: ImportOperation }) { + if (operation.children == null || operation.children.length === 0) { + return null + } + if (operation.type === 'refreshDependencies') { + return (
- -
{getImportOperationText(operation)}
-
- -
+
- {operation.children != null && operation.children.length > 0 && ( -
- {operation.children.map((child, index) => ( - - ))} -
- )} + ) + } + return ( +
+ {operation.children.map((child) => ( + + ))}
) } +function DependenciesStatus({ + dependenciesOperations, +}: { + dependenciesOperations: ImportOperation[] + parentOperation: ImportOperation +}) { + const doneDependencies = dependenciesOperations.filter((op) => op.result === 'success') + const restOfDependencies = dependenciesOperations.filter((op) => op.result !== 'success') + const doneDependenciesText = + doneDependencies.length === 0 + ? '' + : `${doneDependencies.length} dependencies fetched successfully` + return ( + + + + +
{doneDependenciesText}
+
+
+ {restOfDependencies.map((operation) => ( + + ))} +
+ ) +} + function OperationIcon({ status, result }: { status: string; result?: string }) { if (status === 'running') { return } else if (status === 'done' && result === 'success') { return + } else if (status === 'done' && result === 'partial') { + return } else if (status === 'not started') { return } else { @@ -191,7 +229,7 @@ function OperationIcon({ status, result }: { status: string; result?: string }) } } -function TimeFromInSeconds({ startTime, endTime }: { startTime?: number; endTime?: number }) { +function TimeFromInSeconds({ operation }: { operation: ImportOperation }) { const [currentTime, setCurrentTime] = React.useState(Date.now()) React.useEffect(() => { const interval = setInterval(() => { @@ -199,16 +237,105 @@ function TimeFromInSeconds({ startTime, endTime }: { startTime?: number; endTime }, 1000) return () => clearInterval(interval) }, []) - const time = useMemo(() => { - if (startTime == null) { + const operationTime = useMemo(() => { + if (operation.timeStarted == null) { return 0 } - if (endTime == null) { - return currentTime - startTime + if (operation.timeDone == null) { + return currentTime - operation.timeStarted } - return endTime - startTime - }, [startTime, endTime, currentTime]) + return operation.timeDone - operation.timeStarted + }, [operation.timeStarted, operation.timeDone, currentTime]) const timeInSeconds = - endTime != null ? (time / 1000).toFixed(2) : Math.max(Math.floor(time / 1000), 0) - return startTime == null ? null :
{timeInSeconds}s
+ operation.timeDone != null + ? (operationTime / 1000).toFixed(2) + : Math.max(Math.floor(operationTime / 1000), 0) + return operation.timeStarted == null ? null : ( +
{timeInSeconds}s
+ ) +} + +function getImportOperationText(operation: ImportOperation): React.ReactNode { + switch (operation.type) { + case 'loadBranch': + return ( + + Loading branch{' '} + + {operation.githubRepo?.owner}/{operation.githubRepo?.repository}@{operation.branchName} + + + ) + case 'fetchDependency': + return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}` + case 'parseFiles': + return 'Parsing files' + case 'createStoryboard': + return 'Creating storyboard file' + case 'refreshDependencies': + return 'Fetching dependencies' + case 'createPackageJsonEntry': + return 'Creating package.json entry' + case 'checkUtopiaRequirements': + return 'Checking Utopia requirements' + default: + assertNever(operation) + } +} + +function OperationLineWrapper({ + children, + className, +}: { + children: React.ReactNode + className: string +}) { + return ( +
&': { + paddingLeft: 10, + fontSize: 12, + img: { + width: 12, + height: 12, + }, + }, + '.import-wizard-operation-children .operation-done [data-short-time=true]': { + visibility: 'hidden', + }, + }} + > + {children} +
+ ) +} + +function OperationLineContent({ + children, + textColor, +}: { + children: React.ReactNode + textColor: string +}) { + return ( +
+ {children} +
+ ) } diff --git a/editor/src/components/navigator/left-pane/github-pane/index.tsx b/editor/src/components/navigator/left-pane/github-pane/index.tsx index 2e9541b1c39f..f94e8dd31039 100644 --- a/editor/src/components/navigator/left-pane/github-pane/index.tsx +++ b/editor/src/components/navigator/left-pane/github-pane/index.tsx @@ -935,7 +935,6 @@ const BranchNotLoadedBlock = () => { const loadFromBranch = React.useCallback(() => { if (githubRepo != null && branchName != null && githubUserDetails != null) { - startImportWizard(dispatch) void GithubOperations.updateProjectWithBranchContent( workersRef.current, dispatch, diff --git a/editor/src/core/shared/dependencies.ts b/editor/src/core/shared/dependencies.ts index 345315ab1dba..34938fce58bc 100644 --- a/editor/src/core/shared/dependencies.ts +++ b/editor/src/core/shared/dependencies.ts @@ -13,11 +13,12 @@ import { import type { EditorStorePatched } from '../../components/editor/store/editor-state' import type { BuiltInDependencies } from '../es-modules/package-manager/built-in-dependencies-list' import { fetchNodeModules } from '../es-modules/package-manager/fetch-packages' -import type { RequestedNpmDependency } from './npm-dependency-types' +import type { PackageStatusMap, RequestedNpmDependency } from './npm-dependency-types' import { objectFilter } from './object-utils' import type { NodeModules } from './project-file-types' import { isTextFile } from './project-file-types' import { fastForEach } from './utils' +import { notifyOperationFinished } from '../../components/editor/import-wizard/import-wizard-service' export function removeModulesFromNodeModules( modulesToRemove: Array, @@ -91,6 +92,11 @@ export async function refreshDependencies( updateNodeModulesContents(fetchNodeModulesResult.nodeModules), ]) + notifyOperationFinished( + { type: 'refreshDependencies' }, + getDependenciesStatus(loadedPackagesStatus), + ) + return updatedNodeModulesFiles } @@ -100,6 +106,18 @@ export async function refreshDependencies( }) } +function getDependenciesStatus(loadedPackagesStatus: PackageStatusMap) { + return Object.entries(loadedPackagesStatus).some( + ([_, status]) => status.status === 'error' || status.status === 'not-found', + ) + ? Object.entries(loadedPackagesStatus).every( + ([_, status]) => status.status === 'error' || status.status === 'not-found', + ) + ? 'error' + : 'partial' + : 'success' +} + export const projectDependenciesSelector = createSelector( (store: EditorStorePatched) => store.editor.projectContents, (projectContents): Array => { diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index d817719701ef..211c1fc418e5 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -230,9 +230,7 @@ export const updateProjectWithBranchContent = currentDeps, builtInDependencies, {}, - ).then(() => { - notifyOperationFinished({ type: 'refreshDependencies' }, 'success') - }) + ).then(() => {}) } // When the dependencies update has gone through, then indicate that the project was imported. From a5c5a2bfc8215360ab281678f01617fff2771f76 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 03:02:02 +0300 Subject: [PATCH 05/25] add requirements service --- .../src/components/editor/actions/actions.tsx | 17 +- .../editor/import-wizard/components.tsx | 235 +++++++++++++++++ .../import-wizard/import-wizard-service.ts | 55 ++-- .../editor/import-wizard/import-wizard.tsx | 244 +----------------- .../utopia-requirements-service.ts | 225 ++++++++++++++++ .../package-manager/fetch-packages.ts | 2 - editor/src/core/shared/dependencies.ts | 2 +- .../shared/github/operations/load-branch.ts | 29 +-- 8 files changed, 520 insertions(+), 289 deletions(-) create mode 100644 editor/src/components/editor/import-wizard/components.tsx create mode 100644 editor/src/components/editor/import-wizard/utopia-requirements-service.ts diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 6691e0ad89e7..c7647d5bbc05 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -633,6 +633,10 @@ import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils' import { applyValuesAtPath } from '../../canvas/commands/adjust-number-command' import { styleP } from '../../inspector/inspector-common' import { getUpdateOperationResult } from '../import-wizard/import-wizard-service' +import { + notifyCheckingUtopiaRequirement, + notifyResolveUtopiaRequirement, +} from '../import-wizard/utopia-requirements-service' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -1638,16 +1642,25 @@ export function createStoryboardFileIfNecessary( projectContents: ProjectContentTreeRoot, createPlaceholder: 'create-placeholder' | 'skip-creating-placeholder', ): ProjectContentTreeRoot { + notifyCheckingUtopiaRequirement('storyboard', 'Checking for storyboard.js') const storyboardFile = getProjectFileByFilePath(projectContents, StoryboardFilePath) if (storyboardFile != null) { + notifyResolveUtopiaRequirement('storyboard', 'found', 'Storyboard.js found') return projectContents } - return ( + const result = createStoryboardFileIfRemixProject(projectContents) ?? createStoryboardFileIfMainComponentPresent(projectContents) ?? createStoryboardFileWithPlaceholderContents(projectContents, createPlaceholder) - ) + + if (result == projectContents) { + notifyResolveUtopiaRequirement('storyboard', 'partial', 'Storyboard.js skipped') + } else { + notifyResolveUtopiaRequirement('storyboard', 'fixed', 'Storyboard.js created') + } + + return result } // JS Editor Actions: diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx new file mode 100644 index 000000000000..6e1b8c4dcfce --- /dev/null +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -0,0 +1,235 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@emotion/react' +import React from 'react' +import type { ImportOperation, ImportOperationResult } from './import-wizard-service' +import { assertNever } from '../../../core/shared/utils' +import { Icons } from '../../../uuiui' +import { GithubSpinner } from '../../../components/navigator/left-pane/github-pane/github-spinner' + +export function OperationLine({ operation }: { operation: ImportOperation }) { + const operationRunningStatus = React.useMemo(() => { + return operation.timeStarted == null + ? 'waiting' + : operation.timeDone == null + ? 'running' + : 'done' + }, [operation.timeStarted, operation.timeDone]) + + const textColor = React.useMemo(() => { + if (operationRunningStatus === 'waiting') { + return 'gray' + } else if (operationRunningStatus === 'running') { + return 'black' + } else if ( + operation.type === 'checkUtopiaRequirementAndFix' && + operation.resolution === 'fixed' + ) { + return 'var(--utopitheme-primary)' + } else if (operation.result === 'success') { + return 'green' + } else if (operation.result === 'warn') { + return 'orange' + } else { + return 'var(--utopitheme-errorForeground)' + } + }, [operationRunningStatus, operation]) + return ( + + + +
{getImportOperationText(operation)}
+
+ +
+
+ +
+ ) +} + +function OperationChildrenList({ operation }: { operation: ImportOperation }) { + if (operation.children == null || operation.children.length === 0) { + return null + } + return ( +
+ {operation.type === 'refreshDependencies' ? ( + + ) : ( + operation.children.map((child) => ( + + )) + )} +
+ ) +} + +function DependenciesStatus({ + dependenciesOperations, +}: { + dependenciesOperations: ImportOperation[] + parentOperation: ImportOperation +}) { + const doneDependencies = dependenciesOperations.filter((op) => op.result === 'success') + const restOfDependencies = dependenciesOperations.filter((op) => op.result !== 'success') + return ( + + {doneDependencies.length > 0 ? ( + + + +
{`${doneDependencies.length} dependencies fetched successfully`}
+
+
+ ) : null} + {restOfDependencies.map((operation) => ( + + ))} +
+ ) +} + +function OperationIcon({ + runningStatus, + result, +}: { + runningStatus: 'waiting' | 'running' | 'done' + result?: ImportOperationResult +}) { + if (runningStatus === 'running') { + return + } else if (runningStatus === 'done' && result === 'success') { + return + } else if (runningStatus === 'done' && result === 'warn') { + return + } else if (runningStatus === 'waiting') { + return + } else { + return + } +} + +function TimeFromInSeconds({ operation }: { operation: ImportOperation }) { + const [currentTime, setCurrentTime] = React.useState(Date.now()) + React.useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(Date.now()) + }, 1000) + return () => clearInterval(interval) + }, []) + const operationTime = React.useMemo(() => { + if (operation.timeStarted == null) { + return 0 + } + if (operation.timeDone == null) { + return currentTime - operation.timeStarted + } + return operation.timeDone - operation.timeStarted + }, [operation.timeStarted, operation.timeDone, currentTime]) + const timeInSeconds = + operation.timeDone != null + ? (operationTime / 1000).toFixed(2) + : Math.max(Math.floor(operationTime / 1000), 0) + return operation.timeStarted == null ? null : ( +
{timeInSeconds}s
+ ) +} + +function OperationLineWrapper({ + children, + className, +}: { + children: React.ReactNode + className: string +}) { + return ( +
&': { + paddingLeft: 10, + fontSize: 12, + img: { + width: 12, + height: 12, + }, + }, + '.import-wizard-operation-children .operation-done [data-short-time=true]': { + visibility: 'hidden', + }, + }} + > + {children} +
+ ) +} + +function OperationLineContent({ + children, + textColor, +}: { + children: React.ReactNode + textColor: string +}) { + return ( +
+ {children} +
+ ) +} + +function getImportOperationText(operation: ImportOperation): React.ReactNode { + if (operation.text != null) { + return operation.text + } + switch (operation.type) { + case 'loadBranch': + return ( + + Loading branch{' '} + + {operation.githubRepo?.owner}/{operation.githubRepo?.repository}@{operation.branchName} + + + ) + case 'fetchDependency': + return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}` + case 'parseFiles': + return 'Parsing files' + case 'refreshDependencies': + return 'Fetching dependencies' + case 'checkUtopiaRequirements': + return 'Checking Utopia requirements' + case 'checkUtopiaRequirementAndFix': + return operation.text + default: + assertNever(operation) + } +} diff --git a/editor/src/components/editor/import-wizard/import-wizard-service.ts b/editor/src/components/editor/import-wizard/import-wizard-service.ts index 1741952ee156..6244aa832af8 100644 --- a/editor/src/components/editor/import-wizard/import-wizard-service.ts +++ b/editor/src/components/editor/import-wizard/import-wizard-service.ts @@ -37,18 +37,10 @@ export function hideImportWizard() { editorDispatch?.([setImportWizardOpen(false)]) } -export function updateOperation(operation: ImportOperation) { +export function dispatchUpdateOperation(operation: ImportOperation) { editorDispatch?.([updateImportOperations([operation], 'update')]) } -export function addOperation(operation: ImportOperation) { - editorDispatch?.([updateImportOperations([operation], 'add')]) -} - -export function removeOperation(operation: ImportOperation) { - editorDispatch?.([updateImportOperations([operation], 'remove')]) -} - export function notifyOperationStarted(operation: ImportOperation) { const operationWithTime = { ...operation, @@ -76,6 +68,7 @@ export function areSameOperation(existing: ImportOperation, incoming: ImportOper } type ImportOperationData = { + text?: string id?: string | null timeStarted?: number | null timeDone?: number | null @@ -85,7 +78,9 @@ type ImportOperationData = { children?: ImportOperation[] } -type ImportOperationResult = 'success' | 'error' | 'partial' +type UtopiaRequirementResolution = 'found' | 'fixed' | 'critical' | 'partial' + +export type ImportOperationResult = 'success' | 'error' | 'warn' type ImportLoadBranch = { type: 'loadBranch' @@ -97,23 +92,34 @@ type ImportRefreshDependencies = { type: 'refreshDependencies' } & ImportOperationData -type ImportFetchDependency = { +type ImportFetchDependency = ImportOperationData & { type: 'fetchDependency' dependencyName: string dependencyVersion: string -} & ImportOperationData + id: string +} type ImportParseFiles = { type: 'parseFiles' } & ImportOperationData -type ImportCreateStoryboard = { - type: 'createStoryboard' -} & ImportOperationData +type ImportCheckUtopiaRequirementAndFix = ImportOperationData & { + type: 'checkUtopiaRequirementAndFix' + resolution?: UtopiaRequirementResolution + text: string + id: string +} -type ImportCreatePackageJsonEntry = { - type: 'createPackageJsonEntry' -} & ImportOperationData +export function importCheckUtopiaRequirementAndFix( + id: string, + text: string, +): ImportCheckUtopiaRequirementAndFix { + return { + type: 'checkUtopiaRequirementAndFix', + text: text, + id: id, + } +} type ImportCheckUtopiaRequirements = { type: 'checkUtopiaRequirements' @@ -123,20 +129,25 @@ export type ImportOperation = | ImportLoadBranch | ImportRefreshDependencies | ImportParseFiles - | ImportCreateStoryboard | ImportFetchDependency - | ImportCreatePackageJsonEntry + | ImportCheckUtopiaRequirementAndFix | ImportCheckUtopiaRequirements type ImportOperationType = ImportOperation['type'] export type ImportOperationAction = 'add' | 'remove' | 'update' | 'replace' +export const defaultParentTypes: Partial> = { + checkUtopiaRequirementAndFix: 'checkUtopiaRequirements', + fetchDependency: 'refreshDependencies', +} + function getParentArray(root: ImportOperation[], operation: ImportOperation): ImportOperation[] { - if (operation.parentOperationType == null) { + const parentOperationType = operation.parentOperationType ?? defaultParentTypes[operation.type] + if (parentOperationType == null) { return root } - const parentIndex = root.findIndex((op) => op.type === operation.parentOperationType) + const parentIndex = root.findIndex((op) => op.type === parentOperationType) if (parentIndex === -1) { return root } diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index fb9467dc41a9..1ee56e00ad1a 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -1,15 +1,10 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' -import React, { useMemo } from 'react' +import React from 'react' import { getProjectID } from '../../../common/env-vars' import { Button, Icons, useColorTheme, UtopiaStyles } from '../../../uuiui' -import { GithubSpinner } from '../../navigator/left-pane/github-pane/github-spinner' import { useEditorState, Substores } from '../store/store-hook' import { when } from '../../../utils/react-conditionals' -import type { ImportOperation } from './import-wizard-service' import { hideImportWizard } from './import-wizard-service' -import { assertNever } from '../../../core/shared/utils' +import { OperationLine } from './components' export const ImportWizard = React.memo(() => { const colorTheme = useColorTheme() @@ -63,9 +58,8 @@ export const ImportWizard = React.memo(() => { background: colorTheme.bg0.value, boxShadow: UtopiaStyles.popup.boxShadow, borderRadius: 10, - width: 610, - minHeight: 500, - maxHeight: 770, + width: 600, + height: 500, position: 'relative', display: 'flex', flexDirection: 'column', @@ -95,7 +89,7 @@ export const ImportWizard = React.memo(() => { style={{ display: 'flex', flexDirection: 'column', - gap: 10, + gap: 15, overflow: 'scroll', height: '100%', width: '100%', @@ -111,231 +105,3 @@ export const ImportWizard = React.memo(() => { ) }) ImportWizard.displayName = 'ImportWizard' - -function OperationLine({ operation }: { operation: ImportOperation }) { - const operationStatus = useMemo(() => { - if (operation.timeStarted == null) { - return 'not started' - } - if (operation.timeDone == null) { - return 'running' - } - return `done` - }, [operation.timeStarted, operation.timeDone]) - const textColor = React.useMemo(() => { - if (operationStatus === 'not started') { - return 'gray' - } else if (operationStatus === 'running') { - return 'black' - } else { - return operation.result === 'success' - ? 'green' - : operation.result === 'partial' - ? 'orange' - : 'red' - } - }, [operationStatus, operation.result]) - return ( - - - -
{getImportOperationText(operation)}
-
- -
-
- -
- ) -} - -function OperationChildrenList({ operation }: { operation: ImportOperation }) { - if (operation.children == null || operation.children.length === 0) { - return null - } - if (operation.type === 'refreshDependencies') { - return ( -
- -
- ) - } - return ( -
- {operation.children.map((child) => ( - - ))} -
- ) -} - -function DependenciesStatus({ - dependenciesOperations, -}: { - dependenciesOperations: ImportOperation[] - parentOperation: ImportOperation -}) { - const doneDependencies = dependenciesOperations.filter((op) => op.result === 'success') - const restOfDependencies = dependenciesOperations.filter((op) => op.result !== 'success') - const doneDependenciesText = - doneDependencies.length === 0 - ? '' - : `${doneDependencies.length} dependencies fetched successfully` - return ( - - - - -
{doneDependenciesText}
-
-
- {restOfDependencies.map((operation) => ( - - ))} -
- ) -} - -function OperationIcon({ status, result }: { status: string; result?: string }) { - if (status === 'running') { - return - } else if (status === 'done' && result === 'success') { - return - } else if (status === 'done' && result === 'partial') { - return - } else if (status === 'not started') { - return - } else { - return - } -} - -function TimeFromInSeconds({ operation }: { operation: ImportOperation }) { - const [currentTime, setCurrentTime] = React.useState(Date.now()) - React.useEffect(() => { - const interval = setInterval(() => { - setCurrentTime(Date.now()) - }, 1000) - return () => clearInterval(interval) - }, []) - const operationTime = useMemo(() => { - if (operation.timeStarted == null) { - return 0 - } - if (operation.timeDone == null) { - return currentTime - operation.timeStarted - } - return operation.timeDone - operation.timeStarted - }, [operation.timeStarted, operation.timeDone, currentTime]) - const timeInSeconds = - operation.timeDone != null - ? (operationTime / 1000).toFixed(2) - : Math.max(Math.floor(operationTime / 1000), 0) - return operation.timeStarted == null ? null : ( -
{timeInSeconds}s
- ) -} - -function getImportOperationText(operation: ImportOperation): React.ReactNode { - switch (operation.type) { - case 'loadBranch': - return ( - - Loading branch{' '} - - {operation.githubRepo?.owner}/{operation.githubRepo?.repository}@{operation.branchName} - - - ) - case 'fetchDependency': - return `Fetching ${operation.dependencyName}@${operation.dependencyVersion}` - case 'parseFiles': - return 'Parsing files' - case 'createStoryboard': - return 'Creating storyboard file' - case 'refreshDependencies': - return 'Fetching dependencies' - case 'createPackageJsonEntry': - return 'Creating package.json entry' - case 'checkUtopiaRequirements': - return 'Checking Utopia requirements' - default: - assertNever(operation) - } -} - -function OperationLineWrapper({ - children, - className, -}: { - children: React.ReactNode - className: string -}) { - return ( -
&': { - paddingLeft: 10, - fontSize: 12, - img: { - width: 12, - height: 12, - }, - }, - '.import-wizard-operation-children .operation-done [data-short-time=true]': { - visibility: 'hidden', - }, - }} - > - {children} -
- ) -} - -function OperationLineContent({ - children, - textColor, -}: { - children: React.ReactNode - textColor: string -}) { - return ( -
- {children} -
- ) -} diff --git a/editor/src/components/editor/import-wizard/utopia-requirements-service.ts b/editor/src/components/editor/import-wizard/utopia-requirements-service.ts new file mode 100644 index 000000000000..8a287499cb33 --- /dev/null +++ b/editor/src/components/editor/import-wizard/utopia-requirements-service.ts @@ -0,0 +1,225 @@ +import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' +import type { ImportOperationResult } from './import-wizard-service' +import { + importCheckUtopiaRequirementAndFix, + notifyOperationFinished, + notifyOperationStarted, +} from './import-wizard-service' +import { + addFileToProjectContents, + packageJsonFileFromProjectContents, +} from '../../../components/assets' +import { codeFile, isTextFile, RevisionsState } from '../../../core/shared/project-file-types' +import { applyToAllUIJSFiles } from '../../../core/model/project-file-utils' + +let utopiaRequirementsResolutions: Record = { + storyboard: initialResolution(), + packageJsonEntries: initialResolution(), + language: initialResolution(), + reactVersion: initialResolution(), +} + +type UtopiaRequirement = keyof typeof utopiaRequirementsResolutions + +const initialTexts: Record = { + storyboard: 'Checking storyboard.js', + packageJsonEntries: 'Checking package.json', + language: 'Checking project language', + reactVersion: 'Checking React version', +} + +function initialResolution(): UtopiaRequirementResolution { + return { + status: 'not-started', + } +} + +export function resetUtopiaRequirementsResolutions() { + utopiaRequirementsResolutions = Object.fromEntries( + Object.keys(utopiaRequirementsResolutions).map((key) => [key, initialResolution()]), + ) as Record + notifyOperationStarted({ + type: 'checkUtopiaRequirements', + children: Object.keys(utopiaRequirementsResolutions).map((key) => + importCheckUtopiaRequirementAndFix( + key as UtopiaRequirement, + initialTexts[key as UtopiaRequirement], + ), + ), + }) +} + +export function notifyCheckingUtopiaRequirement(requirement: UtopiaRequirement, text: string) { + utopiaRequirementsResolutions[requirement].status = 'pending' + notifyOperationStarted({ + type: 'checkUtopiaRequirementAndFix', + id: requirement, + text: text, + }) +} + +export function notifyResolveUtopiaRequirement( + requirementName: UtopiaRequirement, + resolution: UtopiaRequirementResolutionResult, + text: string, + value?: string, +) { + utopiaRequirementsResolutions[requirementName] = { + status: 'done', + resolution: resolution, + value: value, + } + const result = + resolution === 'found' || resolution === 'fixed' + ? 'success' + : resolution === 'partial' + ? 'warn' + : 'error' + notifyOperationFinished( + { + type: 'checkUtopiaRequirementAndFix', + id: requirementName, + text: text, + resolution: resolution, + }, + result, + ) + const aggregatedStatus = getAggregatedStatus() + if (aggregatedStatus != 'pending') { + notifyOperationFinished({ type: 'checkUtopiaRequirements' }, aggregatedStatus) + } +} + +function getAggregatedStatus(): ImportOperationResult | 'pending' { + for (const resolution of Object.values(utopiaRequirementsResolutions)) { + if (resolution.status != 'done') { + return 'pending' + } + if (resolution.resolution == 'critical') { + return 'error' + } + if (resolution.resolution == 'partial') { + return 'warn' + } + } + return 'success' +} + +type UtopiaRequirementResolutionStatus = 'not-started' | 'pending' | 'done' +type UtopiaRequirementResolutionResult = 'found' | 'fixed' | 'partial' | 'critical' + +type UtopiaRequirementResolution = { + status: UtopiaRequirementResolutionStatus + value?: string + resolution?: UtopiaRequirementResolutionResult +} + +export function checkAndFixUtopiaRequirements( + parsedProjectContents: ProjectContentTreeRoot, +): ProjectContentTreeRoot { + let projectContents = parsedProjectContents + // check package.json + projectContents = checkAndFixPackageJson(projectContents) + // check language + projectContents = checkProjectLanguage(projectContents) + // check react version + checkReactVersion(projectContents) + return projectContents +} + +function getPackageJson( + projectContents: ProjectContentTreeRoot, +): { utopia?: Record; dependencies?: Record } | null { + const packageJson = packageJsonFileFromProjectContents(projectContents) + if (packageJson != null && isTextFile(packageJson)) { + return JSON.parse(packageJson.fileContents.code) + } + return null +} + +function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { + notifyCheckingUtopiaRequirement('packageJsonEntries', 'Checking package.json') + const parsedPackageJson = getPackageJson(projectContents) + if (parsedPackageJson == null) { + notifyResolveUtopiaRequirement( + 'packageJsonEntries', + 'critical', + 'The file package.json was not found', + ) + return projectContents + } + if (parsedPackageJson.utopia == null) { + parsedPackageJson.utopia = { + 'main-ui': 'utopia/storyboard.js', + } + const result = addFileToProjectContents( + projectContents, + '/package.json', + codeFile( + JSON.stringify(parsedPackageJson, null, 2), + null, + 0, + RevisionsState.CodeAheadButPleaseTellVSCodeAboutIt, + ), + ) + notifyResolveUtopiaRequirement( + 'packageJsonEntries', + 'fixed', + 'Fixed utopia entry in package.json', + ) + return result + } else { + notifyResolveUtopiaRequirement('packageJsonEntries', 'found', 'Valid package.json found') + } + + return projectContents +} + +function checkProjectLanguage(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { + notifyCheckingUtopiaRequirement('language', 'Checking project language') + let jsCount = 0 + let tsCount = 0 + applyToAllUIJSFiles(projectContents, (filename, uiJSFile) => { + if (filename.endsWith('.ts') || filename.endsWith('.tsx')) { + tsCount++ + } else if (filename.endsWith('.js') || filename.endsWith('.jsx')) { + jsCount++ + } + return uiJSFile + }) + if (tsCount > jsCount) { + notifyResolveUtopiaRequirement( + 'language', + 'critical', + 'Majority of project files are in TS/TSX', + 'typescript', + ) + } else { + notifyResolveUtopiaRequirement('language', 'found', 'Project uses JS/JSX', 'javascript') + } + return projectContents +} + +function checkReactVersion(projectContents: ProjectContentTreeRoot): void { + notifyCheckingUtopiaRequirement('reactVersion', 'Checking React version') + const parsedPackageJson = getPackageJson(projectContents) + if ( + parsedPackageJson == null || + parsedPackageJson.dependencies == null || + parsedPackageJson.dependencies.react == null + ) { + return notifyResolveUtopiaRequirement( + 'reactVersion', + 'critical', + 'React is not in dependencies', + ) + } + const reactVersion = parsedPackageJson.dependencies.react + // TODO: check react version + return notifyResolveUtopiaRequirement( + 'reactVersion', + 'found', + 'React version is ok', + reactVersion, + ) +} diff --git a/editor/src/core/es-modules/package-manager/fetch-packages.ts b/editor/src/core/es-modules/package-manager/fetch-packages.ts index 377a36e623b9..651f771a568a 100644 --- a/editor/src/core/es-modules/package-manager/fetch-packages.ts +++ b/editor/src/core/es-modules/package-manager/fetch-packages.ts @@ -296,7 +296,6 @@ export async function fetchNodeModules( { type: 'fetchDependency', id: `${newDep.name}@${newDep.version}`, - parentOperationType: 'refreshDependencies', dependencyName: newDep.name, dependencyVersion: newDep.version, }, @@ -307,7 +306,6 @@ export async function fetchNodeModules( notifyOperationStarted({ type: 'fetchDependency', id: `${newDep.name}@${newDep.version}`, - parentOperationType: 'refreshDependencies', dependencyName: newDep.name, dependencyVersion: newDep.version, }) diff --git a/editor/src/core/shared/dependencies.ts b/editor/src/core/shared/dependencies.ts index 34938fce58bc..487959539e15 100644 --- a/editor/src/core/shared/dependencies.ts +++ b/editor/src/core/shared/dependencies.ts @@ -114,7 +114,7 @@ function getDependenciesStatus(loadedPackagesStatus: PackageStatusMap) { ([_, status]) => status.status === 'error' || status.status === 'not-found', ) ? 'error' - : 'partial' + : 'warn' : 'success' } diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index 211c1fc418e5..f5abc8e1e3a8 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -44,6 +44,10 @@ import { notifyOperationStarted, startImportWizard, } from '../../../../components/editor/import-wizard/import-wizard-service' +import { + checkAndFixUtopiaRequirements, + resetUtopiaRequirementsResolutions, +} from '../../../../components/editor/import-wizard/utopia-requirements-service' export const saveAssetsToProject = (operationContext: GithubOperationContext) => @@ -158,45 +162,24 @@ export const updateProjectWithBranchContent = if (resetBranches) { newGithubData.branches = null } - notifyOperationFinished({ type: 'loadBranch' }, 'success') notifyOperationStarted({ type: 'parseFiles' }) - // Push any code through the parser so that the representations we end up with are in a state of `BOTH_MATCH`. // So that it will override any existing files that might already exist in the project when sending them to VS Code. const parseResults = await updateProjectContentsWithParseResults( workers, responseBody.branch.content, ) - notifyOperationFinished({ type: 'parseFiles' }, 'success') - notifyOperationStarted({ type: 'checkUtopiaRequirements' }) - notifyOperationStarted({ - type: 'createStoryboard', - parentOperationType: 'checkUtopiaRequirements', - }) - + resetUtopiaRequirementsResolutions() const parsedProjectContents = createStoryboardFileIfNecessary( parseResults, 'create-placeholder', ) - notifyOperationFinished( - { type: 'createStoryboard', parentOperationType: 'checkUtopiaRequirements' }, - 'success', - ) - notifyOperationStarted({ - type: 'createPackageJsonEntry', - parentOperationType: 'checkUtopiaRequirements', - }) - // here will be the code to create package.json entry - notifyOperationFinished( - { type: 'createPackageJsonEntry', parentOperationType: 'checkUtopiaRequirements' }, - 'success', - ) - notifyOperationFinished({ type: 'checkUtopiaRequirements' }, 'success') + const fixedParsedProjectContents = checkAndFixUtopiaRequirements(parsedProjectContents) // Update the editor with everything so that if anything else fails past this point // there's no loss of data from the user's perspective. From 8ad412f85345218ba02ddbdc96f84ded464c6204 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 03:08:41 +0300 Subject: [PATCH 06/25] cleanup --- .../components/editor/editor-component.tsx | 20 ++++++++++++------- .../editor/import-wizard/import-wizard.tsx | 4 ++-- .../components/editor/store/editor-state.ts | 17 ++++++++-------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/editor/src/components/editor/editor-component.tsx b/editor/src/components/editor/editor-component.tsx index 48edb31f5bae..d99307e37ef1 100644 --- a/editor/src/components/editor/editor-component.tsx +++ b/editor/src/components/editor/editor-component.tsx @@ -680,11 +680,17 @@ const LockedOverlay = React.memo(() => { const editorLocked = React.useMemo(() => githubOperations.length > 0, [githubOperations]) const refreshingDependencies = false - // useEditorState( - // Substores.restOfEditor, - // (store) => store.editor.refreshingDependencies, - // 'LockedOverlay refreshingDependencies', - // ) + useEditorState( + Substores.restOfEditor, + (store) => store.editor.refreshingDependencies, + 'LockedOverlay refreshingDependencies', + ) + + const importWizardOpen = useEditorState( + Substores.restOfEditor, + (store) => store.editor.importWizardOpen, + 'LockedOverlay importWizardOpen', + ) const forking = useEditorState( Substores.restOfEditor, @@ -702,8 +708,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) { diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 1ee56e00ad1a..67db5b26d43d 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -43,11 +43,11 @@ export const ImportWizard = React.memo(() => { left: 0, bottom: 0, right: 0, - pointerEvents: !importWizardOpen ? 'none' : 'all', + pointerEvents: importWizardOpen ? 'all' : 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', - backgroundColor: !importWizardOpen ? 'transparent' : '#00000033', + backgroundColor: importWizardOpen ? '#00000033' : 'transparent', }} onClick={handleDismiss} > diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index b29aeb398367..fff1fafc36bf 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -349,15 +349,14 @@ export type GithubOperation = | GithubSearchRepository export function githubOperationLocksEditor(op: GithubOperation): boolean { - return false - // switch (op.name) { - // case 'listBranches': - // case 'loadRepositories': - // case 'listPullRequestsForBranch': - // return false - // default: - // return true - // } + switch (op.name) { + case 'listBranches': + case 'loadRepositories': + case 'listPullRequestsForBranch': + return false + default: + return true + } } export function isGithubLoadingBranch( From 96649acaec8c50128f58ee061f1d73b86816d3dd Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 03:10:17 +0300 Subject: [PATCH 07/25] dont close on click outside --- editor/src/components/editor/import-wizard/import-wizard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 67db5b26d43d..e454c8a03de6 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -49,7 +49,6 @@ export const ImportWizard = React.memo(() => { justifyContent: 'center', backgroundColor: importWizardOpen ? '#00000033' : 'transparent', }} - onClick={handleDismiss} > {when( importWizardOpen, From 7bdc946b694b5ce312cac2a95cb43066e0e0a6ab Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 03:20:50 +0300 Subject: [PATCH 08/25] move files around --- editor/src/components/editor/action-types.ts | 5 +- .../editor/actions/action-creators.ts | 5 +- .../src/components/editor/actions/actions.tsx | 4 +- .../editor/import-wizard/components.tsx | 5 +- .../editor/import-wizard/import-wizard.tsx | 2 +- .../components/editor/store/editor-state.ts | 2 +- .../store/store-deep-equality-instances.ts | 2 +- .../navigator/left-pane/github-pane/index.tsx | 2 +- .../package-manager/fetch-packages.ts | 2 +- editor/src/core/shared/dependencies.ts | 2 +- .../shared/github/operations/load-branch.ts | 4 +- .../import/import-operation-service.ts} | 102 +++--------------- .../shared/import/import-operation-types.ts | 71 ++++++++++++ .../import}/utopia-requirements-service.ts | 11 +- 14 files changed, 114 insertions(+), 105 deletions(-) rename editor/src/{components/editor/import-wizard/import-wizard-service.ts => core/shared/import/import-operation-service.ts} (62%) create mode 100644 editor/src/core/shared/import/import-operation-types.ts rename editor/src/{components/editor/import-wizard => core/shared/import}/utopia-requirements-service.ts (95%) diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index 1ff2abe37d2d..9a85929d173c 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -87,7 +87,10 @@ 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 './import-wizard/import-wizard-service' +import type { + ImportOperation, + ImportOperationAction, +} from '../../core/shared/import/import-operation-types' export { isLoggedIn, loggedInUser, notLoggedIn } from '../../common/user' export type { LoginState, UserDetails } from '../../common/user' diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 6c29c8caaeab..9ffd3e5e3edf 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -270,7 +270,10 @@ 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 '../import-wizard/import-wizard-service' +import type { + ImportOperation, + ImportOperationAction, +} from '../../../core/shared/import/import-operation-types' export function clearSelection(): EditorAction { return { diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index c7647d5bbc05..051b98d217f2 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -632,11 +632,11 @@ import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-ut import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils' import { applyValuesAtPath } from '../../canvas/commands/adjust-number-command' import { styleP } from '../../inspector/inspector-common' -import { getUpdateOperationResult } from '../import-wizard/import-wizard-service' +import { getUpdateOperationResult } from '../../../core/shared/import/import-operation-service' import { notifyCheckingUtopiaRequirement, notifyResolveUtopiaRequirement, -} from '../import-wizard/utopia-requirements-service' +} from '../../../core/shared/import/utopia-requirements-service' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx index 6e1b8c4dcfce..add5449715bb 100644 --- a/editor/src/components/editor/import-wizard/components.tsx +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -2,7 +2,10 @@ /** @jsx jsx */ import { jsx } from '@emotion/react' import React from 'react' -import type { ImportOperation, ImportOperationResult } from './import-wizard-service' +import type { + ImportOperation, + ImportOperationResult, +} from '../../../core/shared/import/import-operation-types' import { assertNever } from '../../../core/shared/utils' import { Icons } from '../../../uuiui' import { GithubSpinner } from '../../../components/navigator/left-pane/github-pane/github-spinner' diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index e454c8a03de6..72971b4e28c3 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -3,7 +3,7 @@ import { getProjectID } from '../../../common/env-vars' import { Button, Icons, useColorTheme, UtopiaStyles } from '../../../uuiui' import { useEditorState, Substores } from '../store/store-hook' import { when } from '../../../utils/react-conditionals' -import { hideImportWizard } from './import-wizard-service' +import { hideImportWizard } from '../../../core/shared/import/import-operation-service' import { OperationLine } from './components' export const ImportWizard = React.memo(() => { diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index fff1fafc36bf..8db8b1a4810c 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -189,7 +189,7 @@ 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 '../import-wizard/import-wizard-service' +import type { ImportOperation } from '../../../core/shared/import/import-operation-types' const ObjectPathImmutable: any = OPI 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 532ddf4aceb7..48ad8c11fcc0 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,7 @@ 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 '../import-wizard/import-wizard-service' +import type { ImportOperation } from '../../../core/shared/import/import-operation-types' export function ElementPropertyPathKeepDeepEquality(): KeepDeepEqualityCall { return combine2EqualityCalls( diff --git a/editor/src/components/navigator/left-pane/github-pane/index.tsx b/editor/src/components/navigator/left-pane/github-pane/index.tsx index f94e8dd31039..1830e144aa72 100644 --- a/editor/src/components/navigator/left-pane/github-pane/index.tsx +++ b/editor/src/components/navigator/left-pane/github-pane/index.tsx @@ -58,7 +58,7 @@ import { OperationContext } from '../../../../core/shared/github/operations/gith import { showImportWizard, startImportWizard, -} from '../../../../components/editor/import-wizard/import-wizard-service' +} from '../../../../core/shared/import/import-operation-service' const compactTimeagoFormatter = (value: number, unit: string) => { return `${value}${unit.charAt(0)}` diff --git a/editor/src/core/es-modules/package-manager/fetch-packages.ts b/editor/src/core/es-modules/package-manager/fetch-packages.ts index 651f771a568a..899e753350c7 100644 --- a/editor/src/core/es-modules/package-manager/fetch-packages.ts +++ b/editor/src/core/es-modules/package-manager/fetch-packages.ts @@ -50,7 +50,7 @@ import { getJsDelivrFileUrl, getPackagerUrl } from './packager-url' import { notifyOperationFinished, notifyOperationStarted, -} from '../../../components/editor/import-wizard/import-wizard-service' +} from '../../shared/import/import-operation-service' let depPackagerCache: { [key: string]: PackagerServerResponse } = {} diff --git a/editor/src/core/shared/dependencies.ts b/editor/src/core/shared/dependencies.ts index 487959539e15..f176c6fa27be 100644 --- a/editor/src/core/shared/dependencies.ts +++ b/editor/src/core/shared/dependencies.ts @@ -18,7 +18,7 @@ import { objectFilter } from './object-utils' import type { NodeModules } from './project-file-types' import { isTextFile } from './project-file-types' import { fastForEach } from './utils' -import { notifyOperationFinished } from '../../components/editor/import-wizard/import-wizard-service' +import { notifyOperationFinished } from './import/import-operation-service' export function removeModulesFromNodeModules( modulesToRemove: Array, diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index f5abc8e1e3a8..c9e66e89f056 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -43,11 +43,11 @@ import { notifyOperationFinished, notifyOperationStarted, startImportWizard, -} from '../../../../components/editor/import-wizard/import-wizard-service' +} from '../../import/import-operation-service' import { checkAndFixUtopiaRequirements, resetUtopiaRequirementsResolutions, -} from '../../../../components/editor/import-wizard/utopia-requirements-service' +} from '../../import/utopia-requirements-service' export const saveAssetsToProject = (operationContext: GithubOperationContext) => diff --git a/editor/src/components/editor/import-wizard/import-wizard-service.ts b/editor/src/core/shared/import/import-operation-service.ts similarity index 62% rename from editor/src/components/editor/import-wizard/import-wizard-service.ts rename to editor/src/core/shared/import/import-operation-service.ts index 6244aa832af8..a6d22cbf6c9f 100644 --- a/editor/src/components/editor/import-wizard/import-wizard-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -1,7 +1,15 @@ -import { assertNever } from '../../../core/shared/utils' -import type { EditorDispatch } from '../action-types' -import { setImportWizardOpen, updateImportOperations } from '../actions/action-creators' -import type { GithubRepo } from '../store/editor-state' +import { assertNever } from '../utils' +import type { EditorDispatch } from '../../../components/editor/action-types' +import { + setImportWizardOpen, + updateImportOperations, +} from '../../../components/editor/actions/action-creators' +import type { + ImportOperation, + ImportOperationAction, + ImportOperationResult, + ImportOperationType, +} from './import-operation-types' let editorDispatch: EditorDispatch | null = null @@ -11,18 +19,10 @@ export function startImportWizard(dispatch: EditorDispatch) { setImportWizardOpen(true), updateImportOperations( [ - { - type: 'loadBranch', - }, - { - type: 'parseFiles', - }, - { - type: 'checkUtopiaRequirements', - }, - { - type: 'refreshDependencies', - }, + { type: 'loadBranch' }, + { type: 'parseFiles' }, + { type: 'checkUtopiaRequirements' }, + { type: 'refreshDependencies' }, ], 'replace', ), @@ -67,76 +67,6 @@ export function areSameOperation(existing: ImportOperation, incoming: ImportOper return existing.id === incoming.id } -type ImportOperationData = { - text?: string - id?: string | null - timeStarted?: number | null - timeDone?: number | null - result?: ImportOperationResult - error?: string - parentOperationType?: ImportOperationType - children?: ImportOperation[] -} - -type UtopiaRequirementResolution = 'found' | 'fixed' | 'critical' | 'partial' - -export type ImportOperationResult = 'success' | 'error' | 'warn' - -type ImportLoadBranch = { - type: 'loadBranch' - branchName?: string - githubRepo?: GithubRepo -} & ImportOperationData - -type ImportRefreshDependencies = { - type: 'refreshDependencies' -} & ImportOperationData - -type ImportFetchDependency = ImportOperationData & { - type: 'fetchDependency' - dependencyName: string - dependencyVersion: string - id: string -} - -type ImportParseFiles = { - type: 'parseFiles' -} & ImportOperationData - -type ImportCheckUtopiaRequirementAndFix = ImportOperationData & { - type: 'checkUtopiaRequirementAndFix' - resolution?: UtopiaRequirementResolution - text: string - id: string -} - -export function importCheckUtopiaRequirementAndFix( - id: string, - text: string, -): ImportCheckUtopiaRequirementAndFix { - return { - type: 'checkUtopiaRequirementAndFix', - text: text, - id: id, - } -} - -type ImportCheckUtopiaRequirements = { - type: 'checkUtopiaRequirements' -} & ImportOperationData - -export type ImportOperation = - | ImportLoadBranch - | ImportRefreshDependencies - | ImportParseFiles - | ImportFetchDependency - | ImportCheckUtopiaRequirementAndFix - | ImportCheckUtopiaRequirements - -type ImportOperationType = ImportOperation['type'] - -export type ImportOperationAction = 'add' | 'remove' | 'update' | 'replace' - export const defaultParentTypes: Partial> = { checkUtopiaRequirementAndFix: 'checkUtopiaRequirements', fetchDependency: 'refreshDependencies', diff --git a/editor/src/core/shared/import/import-operation-types.ts b/editor/src/core/shared/import/import-operation-types.ts new file mode 100644 index 000000000000..b429a0b9ed02 --- /dev/null +++ b/editor/src/core/shared/import/import-operation-types.ts @@ -0,0 +1,71 @@ +import type { GithubRepo } from '../../../components/editor/store/editor-state' + +type ImportOperationData = { + text?: string + id?: string | null + timeStarted?: number | null + timeDone?: number | null + result?: ImportOperationResult + error?: string + parentOperationType?: ImportOperationType + children?: ImportOperation[] +} + +type UtopiaRequirementResolution = 'found' | 'fixed' | 'critical' | 'partial' + +export type ImportOperationResult = 'success' | 'error' | 'warn' + +type ImportLoadBranch = { + type: 'loadBranch' + branchName?: string + githubRepo?: GithubRepo +} & ImportOperationData + +type ImportRefreshDependencies = { + type: 'refreshDependencies' +} & ImportOperationData + +type ImportFetchDependency = ImportOperationData & { + type: 'fetchDependency' + dependencyName: string + dependencyVersion: string + id: string +} + +type ImportParseFiles = { + type: 'parseFiles' +} & ImportOperationData + +type ImportCheckUtopiaRequirementAndFix = ImportOperationData & { + type: 'checkUtopiaRequirementAndFix' + resolution?: UtopiaRequirementResolution + text: string + id: string +} + +export function importCheckUtopiaRequirementAndFix( + id: string, + text: string, +): ImportCheckUtopiaRequirementAndFix { + return { + type: 'checkUtopiaRequirementAndFix', + text: text, + id: id, + } +} + +type ImportCheckUtopiaRequirements = { + type: 'checkUtopiaRequirements' +} & ImportOperationData + +export type ImportOperation = + | ImportLoadBranch + | ImportRefreshDependencies + | ImportParseFiles + | ImportFetchDependency + | ImportCheckUtopiaRequirementAndFix + | ImportCheckUtopiaRequirements + +export type ImportOperationType = ImportOperation['type'] + +export type ImportOperationAction = 'add' | 'remove' | 'update' | 'replace' diff --git a/editor/src/components/editor/import-wizard/utopia-requirements-service.ts b/editor/src/core/shared/import/utopia-requirements-service.ts similarity index 95% rename from editor/src/components/editor/import-wizard/utopia-requirements-service.ts rename to editor/src/core/shared/import/utopia-requirements-service.ts index 8a287499cb33..4ec7aa62c5fd 100644 --- a/editor/src/components/editor/import-wizard/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/utopia-requirements-service.ts @@ -1,16 +1,15 @@ import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' -import type { ImportOperationResult } from './import-wizard-service' import { importCheckUtopiaRequirementAndFix, - notifyOperationFinished, - notifyOperationStarted, -} from './import-wizard-service' + type ImportOperationResult, +} from './import-operation-types' +import { notifyOperationFinished, notifyOperationStarted } from './import-operation-service' import { addFileToProjectContents, packageJsonFileFromProjectContents, } from '../../../components/assets' -import { codeFile, isTextFile, RevisionsState } from '../../../core/shared/project-file-types' -import { applyToAllUIJSFiles } from '../../../core/model/project-file-utils' +import { codeFile, isTextFile, RevisionsState } from '../project-file-types' +import { applyToAllUIJSFiles } from '../../model/project-file-utils' let utopiaRequirementsResolutions: Record = { storyboard: initialResolution(), From 120a6f21e6ffd96d5c239f3c26fce3f66a1501a9 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 12:15:36 +0300 Subject: [PATCH 09/25] move to enums --- .../src/components/editor/actions/actions.tsx | 21 +- .../editor/import-wizard/components.tsx | 56 +++-- .../package-manager/fetch-packages.ts | 11 +- editor/src/core/shared/dependencies.ts | 29 ++- .../shared/github/operations/load-branch.ts | 19 +- .../shared/import/import-operation-service.ts | 22 +- .../shared/import/import-operation-types.ts | 36 +-- .../check-utopia-requirements.ts | 131 +++++++++++ .../import/utopia-requirements-service.ts | 218 +++++------------- 9 files changed, 298 insertions(+), 245 deletions(-) create mode 100644 editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 051b98d217f2..dd56ff64e90b 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -634,8 +634,9 @@ import { applyValuesAtPath } from '../../canvas/commands/adjust-number-command' import { styleP } from '../../inspector/inspector-common' import { getUpdateOperationResult } from '../../../core/shared/import/import-operation-service' import { - notifyCheckingUtopiaRequirement, - notifyResolveUtopiaRequirement, + notifyCheckingRequirement, + notifyResolveRequirement, + RequirementResolutionResult, } from '../../../core/shared/import/utopia-requirements-service' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -1642,10 +1643,10 @@ export function createStoryboardFileIfNecessary( projectContents: ProjectContentTreeRoot, createPlaceholder: 'create-placeholder' | 'skip-creating-placeholder', ): ProjectContentTreeRoot { - notifyCheckingUtopiaRequirement('storyboard', 'Checking for storyboard.js') + notifyCheckingRequirement('storyboard', 'Checking for storyboard.js') const storyboardFile = getProjectFileByFilePath(projectContents, StoryboardFilePath) if (storyboardFile != null) { - notifyResolveUtopiaRequirement('storyboard', 'found', 'Storyboard.js found') + notifyResolveRequirement('storyboard', RequirementResolutionResult.Found, 'Storyboard.js found') return projectContents } @@ -1655,9 +1656,17 @@ export function createStoryboardFileIfNecessary( createStoryboardFileWithPlaceholderContents(projectContents, createPlaceholder) if (result == projectContents) { - notifyResolveUtopiaRequirement('storyboard', 'partial', 'Storyboard.js skipped') + notifyResolveRequirement( + 'storyboard', + RequirementResolutionResult.Partial, + 'Storyboard.js skipped', + ) } else { - notifyResolveUtopiaRequirement('storyboard', 'fixed', 'Storyboard.js created') + notifyResolveRequirement( + 'storyboard', + RequirementResolutionResult.Fixed, + 'Storyboard.js created', + ) } return result diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx index add5449715bb..fdd777911532 100644 --- a/editor/src/components/editor/import-wizard/components.tsx +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -2,13 +2,12 @@ /** @jsx jsx */ import { jsx } from '@emotion/react' import React from 'react' -import type { - ImportOperation, - ImportOperationResult, -} from '../../../core/shared/import/import-operation-types' +import type { ImportOperation } from '../../../core/shared/import/import-operation-types' +import { ImportOperationResult } from '../../../core/shared/import/import-operation-types' import { assertNever } from '../../../core/shared/utils' import { Icons } from '../../../uuiui' import { GithubSpinner } from '../../../components/navigator/left-pane/github-pane/github-spinner' +import { RequirementResolutionResult } from '../../../core/shared/import/utopia-requirements-service' export function OperationLine({ operation }: { operation: ImportOperation }) { const operationRunningStatus = React.useMemo(() => { @@ -19,24 +18,11 @@ export function OperationLine({ operation }: { operation: ImportOperation }) { : 'done' }, [operation.timeStarted, operation.timeDone]) - const textColor = React.useMemo(() => { - if (operationRunningStatus === 'waiting') { - return 'gray' - } else if (operationRunningStatus === 'running') { - return 'black' - } else if ( - operation.type === 'checkUtopiaRequirementAndFix' && - operation.resolution === 'fixed' - ) { - return 'var(--utopitheme-primary)' - } else if (operation.result === 'success') { - return 'green' - } else if (operation.result === 'warn') { - return 'orange' - } else { - return 'var(--utopitheme-errorForeground)' - } - }, [operationRunningStatus, operation]) + const textColor = React.useMemo( + () => getTextColor(operationRunningStatus, operation), + [operationRunningStatus, operation], + ) + return ( > => { - function notifyEnd(result: 'success' | 'error') { + function notifyFetchEnd(result: ImportOperationResult) { notifyOperationFinished( { type: 'fetchDependency', @@ -315,7 +316,7 @@ export async function fetchNodeModules( 'skipFetch', ) if (isPackageNotFound(matchingVersionResponse)) { - notifyEnd('error') + notifyFetchEnd(ImportOperationResult.Error) return left(failNotFound(newDep)) } @@ -345,15 +346,15 @@ export async function fetchNodeModules( * the real nice solution would be to apply npm's module resolution logic that * pulls up shared transitive dependencies to the main /node_modules/ folder. */ - notifyEnd('success') + notifyFetchEnd(ImportOperationResult.Success) return right(mangleNodeModulePaths(newDep.name, packagerResponse)) } else { - notifyEnd('error') + notifyFetchEnd(ImportOperationResult.Error) return left(failError(newDep)) } } catch (e) { // TODO: proper error handling, now we don't show error for a missing package. The error will be visible when you try to import - notifyEnd('error') + notifyFetchEnd(ImportOperationResult.Error) return left(failError(newDep)) } }, diff --git a/editor/src/core/shared/dependencies.ts b/editor/src/core/shared/dependencies.ts index f176c6fa27be..24ea88c9ad46 100644 --- a/editor/src/core/shared/dependencies.ts +++ b/editor/src/core/shared/dependencies.ts @@ -13,12 +13,17 @@ import { import type { EditorStorePatched } from '../../components/editor/store/editor-state' import type { BuiltInDependencies } from '../es-modules/package-manager/built-in-dependencies-list' import { fetchNodeModules } from '../es-modules/package-manager/fetch-packages' -import type { PackageStatusMap, RequestedNpmDependency } from './npm-dependency-types' +import type { + PackageDetails, + PackageStatusMap, + RequestedNpmDependency, +} from './npm-dependency-types' import { objectFilter } from './object-utils' import type { NodeModules } from './project-file-types' import { isTextFile } from './project-file-types' import { fastForEach } from './utils' import { notifyOperationFinished } from './import/import-operation-service' +import { ImportOperationResult } from './import/import-operation-types' export function removeModulesFromNodeModules( modulesToRemove: Array, @@ -106,16 +111,18 @@ export async function refreshDependencies( }) } -function getDependenciesStatus(loadedPackagesStatus: PackageStatusMap) { - return Object.entries(loadedPackagesStatus).some( - ([_, status]) => status.status === 'error' || status.status === 'not-found', - ) - ? Object.entries(loadedPackagesStatus).every( - ([_, status]) => status.status === 'error' || status.status === 'not-found', - ) - ? 'error' - : 'warn' - : 'success' +function isPackageMissing(status: PackageDetails): boolean { + return status.status === 'error' || status.status === 'not-found' +} + +function getDependenciesStatus(loadedPackagesStatus: PackageStatusMap): ImportOperationResult { + if (Object.values(loadedPackagesStatus).every(isPackageMissing)) { + return ImportOperationResult.Error + } + if (Object.values(loadedPackagesStatus).some(isPackageMissing)) { + return ImportOperationResult.Warn + } + return ImportOperationResult.Success } export const projectDependenciesSelector = createSelector( diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index c9e66e89f056..ba6e7d50db37 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -44,10 +44,9 @@ import { notifyOperationStarted, startImportWizard, } from '../../import/import-operation-service' -import { - checkAndFixUtopiaRequirements, - resetUtopiaRequirementsResolutions, -} from '../../import/utopia-requirements-service' +import { resetRequirementsResolutions } from '../../import/utopia-requirements-service' +import { checkAndFixUtopiaRequirements } from '../../import/proejct-health-check/check-utopia-requirements' +import { ImportOperationResult } from '../../import/import-operation-types' export const saveAssetsToProject = (operationContext: GithubOperationContext) => @@ -162,7 +161,7 @@ export const updateProjectWithBranchContent = if (resetBranches) { newGithubData.branches = null } - notifyOperationFinished({ type: 'loadBranch' }, 'success') + notifyOperationFinished({ type: 'loadBranch' }, ImportOperationResult.Success) notifyOperationStarted({ type: 'parseFiles' }) // Push any code through the parser so that the representations we end up with are in a state of `BOTH_MATCH`. @@ -171,15 +170,17 @@ export const updateProjectWithBranchContent = workers, responseBody.branch.content, ) - notifyOperationFinished({ type: 'parseFiles' }, 'success') + notifyOperationFinished({ type: 'parseFiles' }, ImportOperationResult.Success) - resetUtopiaRequirementsResolutions() - const parsedProjectContents = createStoryboardFileIfNecessary( + resetRequirementsResolutions() + const parsedProjectContentsInitial = createStoryboardFileIfNecessary( parseResults, 'create-placeholder', ) - const fixedParsedProjectContents = checkAndFixUtopiaRequirements(parsedProjectContents) + const parsedProjectContents = checkAndFixUtopiaRequirements( + parsedProjectContentsInitial, + ) // Update the editor with everything so that if anything else fails past this point // there's no loss of data from the user's perspective. diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index a6d22cbf6c9f..4e6c58b7d818 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -4,9 +4,9 @@ import { setImportWizardOpen, updateImportOperations, } from '../../../components/editor/actions/action-creators' +import { ImportOperationAction } from './import-operation-types' import type { ImportOperation, - ImportOperationAction, ImportOperationResult, ImportOperationType, } from './import-operation-types' @@ -21,10 +21,10 @@ export function startImportWizard(dispatch: EditorDispatch) { [ { type: 'loadBranch' }, { type: 'parseFiles' }, - { type: 'checkUtopiaRequirements' }, + { type: 'checkRequirements' }, { type: 'refreshDependencies' }, ], - 'replace', + ImportOperationAction.Replace, ), ]) } @@ -38,7 +38,7 @@ export function hideImportWizard() { } export function dispatchUpdateOperation(operation: ImportOperation) { - editorDispatch?.([updateImportOperations([operation], 'update')]) + editorDispatch?.([updateImportOperations([operation], ImportOperationAction.Update)]) } export function notifyOperationStarted(operation: ImportOperation) { @@ -47,7 +47,7 @@ export function notifyOperationStarted(operation: ImportOperation) { timeStarted: Date.now(), timeDone: null, } - editorDispatch?.([updateImportOperations([operationWithTime], 'update')]) + editorDispatch?.([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) } export function notifyOperationFinished(operation: ImportOperation, result: ImportOperationResult) { @@ -57,7 +57,7 @@ export function notifyOperationFinished(operation: ImportOperation, result: Impo timeDone: timeDone, result: result, } - editorDispatch?.([updateImportOperations([operationWithTime], 'update')]) + editorDispatch?.([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) } export function areSameOperation(existing: ImportOperation, incoming: ImportOperation): boolean { @@ -68,7 +68,7 @@ export function areSameOperation(existing: ImportOperation, incoming: ImportOper } export const defaultParentTypes: Partial> = { - checkUtopiaRequirementAndFix: 'checkUtopiaRequirements', + checkRequirementAndFix: 'checkRequirements', fetchDependency: 'refreshDependencies', } @@ -101,13 +101,13 @@ export function getUpdateOperationResult( children: [...(operation.children ?? [])], })) switch (type) { - case 'add': + case ImportOperationAction.Add: incomingOperations.forEach((operation) => { const parent = getParentArray(operations, operation) parent.push(operation) }) break - case 'remove': + case ImportOperationAction.Remove: incomingOperations.forEach((operation) => { const parent = getParentArray(operations, operation) const idx = parent.findIndex((op) => areSameOperation(op, operation)) @@ -116,7 +116,7 @@ export function getUpdateOperationResult( } }) break - case 'update': + case ImportOperationAction.Update: incomingOperations.forEach((operation) => { const parent = getParentArray(operations, operation) const idx = parent.findIndex((op) => areSameOperation(op, operation)) @@ -132,7 +132,7 @@ export function getUpdateOperationResult( } }) break - case 'replace': + case ImportOperationAction.Replace: operations = [...incomingOperations] break default: diff --git a/editor/src/core/shared/import/import-operation-types.ts b/editor/src/core/shared/import/import-operation-types.ts index b429a0b9ed02..30781889abb7 100644 --- a/editor/src/core/shared/import/import-operation-types.ts +++ b/editor/src/core/shared/import/import-operation-types.ts @@ -1,4 +1,5 @@ import type { GithubRepo } from '../../../components/editor/store/editor-state' +import type { RequirementResolutionResult } from './utopia-requirements-service' type ImportOperationData = { text?: string @@ -11,9 +12,11 @@ type ImportOperationData = { children?: ImportOperation[] } -type UtopiaRequirementResolution = 'found' | 'fixed' | 'critical' | 'partial' - -export type ImportOperationResult = 'success' | 'error' | 'warn' +export enum ImportOperationResult { + Success = 'success', + Error = 'error', + Warn = 'warn', +} type ImportLoadBranch = { type: 'loadBranch' @@ -36,26 +39,26 @@ type ImportParseFiles = { type: 'parseFiles' } & ImportOperationData -type ImportCheckUtopiaRequirementAndFix = ImportOperationData & { - type: 'checkUtopiaRequirementAndFix' - resolution?: UtopiaRequirementResolution +type ImportCheckRequirementAndFix = ImportOperationData & { + type: 'checkRequirementAndFix' + resolution?: RequirementResolutionResult text: string id: string } -export function importCheckUtopiaRequirementAndFix( +export function importCheckRequirementAndFix( id: string, text: string, -): ImportCheckUtopiaRequirementAndFix { +): ImportCheckRequirementAndFix { return { - type: 'checkUtopiaRequirementAndFix', + type: 'checkRequirementAndFix', text: text, id: id, } } -type ImportCheckUtopiaRequirements = { - type: 'checkUtopiaRequirements' +type ImportCheckRequirements = { + type: 'checkRequirements' } & ImportOperationData export type ImportOperation = @@ -63,9 +66,14 @@ export type ImportOperation = | ImportRefreshDependencies | ImportParseFiles | ImportFetchDependency - | ImportCheckUtopiaRequirementAndFix - | ImportCheckUtopiaRequirements + | ImportCheckRequirementAndFix + | ImportCheckRequirements export type ImportOperationType = ImportOperation['type'] -export type ImportOperationAction = 'add' | 'remove' | 'update' | 'replace' +export enum ImportOperationAction { + Add = 'add', + Remove = 'remove', + Update = 'update', + Replace = 'replace', +} diff --git a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts new file mode 100644 index 000000000000..011994b81edb --- /dev/null +++ b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts @@ -0,0 +1,131 @@ +import { + addFileToProjectContents, + packageJsonFileFromProjectContents, +} from '../../../../components/assets' +import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' +import { codeFile, isTextFile, RevisionsState } from '../../project-file-types' +import { + notifyCheckingRequirement, + notifyResolveRequirement, + RequirementResolutionResult, +} from '../utopia-requirements-service' +import { applyToAllUIJSFiles } from '../../../../core/model/project-file-utils' + +export function checkAndFixUtopiaRequirements( + parsedProjectContents: ProjectContentTreeRoot, +): ProjectContentTreeRoot { + let projectContents = parsedProjectContents + // check package.json + projectContents = checkAndFixPackageJson(projectContents) + // check language + projectContents = checkProjectLanguage(projectContents) + // check react version + checkReactVersion(projectContents) + return projectContents +} + +function getPackageJson( + projectContents: ProjectContentTreeRoot, +): { utopia?: Record; dependencies?: Record } | null { + const packageJson = packageJsonFileFromProjectContents(projectContents) + if (packageJson != null && isTextFile(packageJson)) { + return JSON.parse(packageJson.fileContents.code) + } + return null +} + +function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { + notifyCheckingRequirement('packageJsonEntries', 'Checking package.json') + const parsedPackageJson = getPackageJson(projectContents) + if (parsedPackageJson == null) { + notifyResolveRequirement( + 'packageJsonEntries', + RequirementResolutionResult.Critical, + 'The file package.json was not found', + ) + return projectContents + } + if (parsedPackageJson.utopia == null) { + parsedPackageJson.utopia = { + 'main-ui': 'utopia/storyboard.js', + } + const result = addFileToProjectContents( + projectContents, + '/package.json', + codeFile( + JSON.stringify(parsedPackageJson, null, 2), + null, + 0, + RevisionsState.CodeAheadButPleaseTellVSCodeAboutIt, + ), + ) + notifyResolveRequirement( + 'packageJsonEntries', + RequirementResolutionResult.Fixed, + 'Fixed utopia entry in package.json', + ) + return result + } else { + notifyResolveRequirement( + 'packageJsonEntries', + RequirementResolutionResult.Found, + 'Valid package.json found', + ) + } + + return projectContents +} + +function checkProjectLanguage(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { + notifyCheckingRequirement('language', 'Checking project language') + let jsCount = 0 + let tsCount = 0 + applyToAllUIJSFiles(projectContents, (filename, uiJSFile) => { + if (filename.endsWith('.ts') || filename.endsWith('.tsx')) { + tsCount++ + } else if (filename.endsWith('.js') || filename.endsWith('.jsx')) { + jsCount++ + } + return uiJSFile + }) + if (tsCount > jsCount) { + notifyResolveRequirement( + 'language', + RequirementResolutionResult.Critical, + 'Majority of project files are in TS/TSX', + 'typescript', + ) + } else { + notifyResolveRequirement( + 'language', + RequirementResolutionResult.Found, + 'Project uses JS/JSX', + 'javascript', + ) + } + return projectContents +} + +function checkReactVersion(projectContents: ProjectContentTreeRoot): void { + notifyCheckingRequirement('reactVersion', 'Checking React version') + const parsedPackageJson = getPackageJson(projectContents) + if ( + parsedPackageJson == null || + parsedPackageJson.dependencies == null || + parsedPackageJson.dependencies.react == null + ) { + return notifyResolveRequirement( + 'reactVersion', + RequirementResolutionResult.Critical, + 'React is not in dependencies', + ) + } + const reactVersion = parsedPackageJson.dependencies.react + // TODO: check react version + return notifyResolveRequirement( + 'reactVersion', + RequirementResolutionResult.Found, + 'React version is ok', + reactVersion, + ) +} diff --git a/editor/src/core/shared/import/utopia-requirements-service.ts b/editor/src/core/shared/import/utopia-requirements-service.ts index 4ec7aa62c5fd..bcc1d28300ea 100644 --- a/editor/src/core/shared/import/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/utopia-requirements-service.ts @@ -1,224 +1,112 @@ -import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' -import { - importCheckUtopiaRequirementAndFix, - type ImportOperationResult, -} from './import-operation-types' +import { importCheckRequirementAndFix, ImportOperationResult } from './import-operation-types' import { notifyOperationFinished, notifyOperationStarted } from './import-operation-service' -import { - addFileToProjectContents, - packageJsonFileFromProjectContents, -} from '../../../components/assets' -import { codeFile, isTextFile, RevisionsState } from '../project-file-types' -import { applyToAllUIJSFiles } from '../../model/project-file-utils' -let utopiaRequirementsResolutions: Record = { +let requirementsResolutions: Record = { storyboard: initialResolution(), packageJsonEntries: initialResolution(), language: initialResolution(), reactVersion: initialResolution(), } -type UtopiaRequirement = keyof typeof utopiaRequirementsResolutions +type Requirement = keyof typeof requirementsResolutions -const initialTexts: Record = { +const initialTexts: Record = { storyboard: 'Checking storyboard.js', packageJsonEntries: 'Checking package.json', language: 'Checking project language', reactVersion: 'Checking React version', } -function initialResolution(): UtopiaRequirementResolution { +function initialResolution(): RequirementResolution { return { - status: 'not-started', + status: RequirementResolutionStatus.NotStarted, } } -export function resetUtopiaRequirementsResolutions() { - utopiaRequirementsResolutions = Object.fromEntries( - Object.keys(utopiaRequirementsResolutions).map((key) => [key, initialResolution()]), - ) as Record +export function resetRequirementsResolutions() { + requirementsResolutions = Object.fromEntries( + Object.keys(requirementsResolutions).map((key) => [key, initialResolution()]), + ) as Record notifyOperationStarted({ - type: 'checkUtopiaRequirements', - children: Object.keys(utopiaRequirementsResolutions).map((key) => - importCheckUtopiaRequirementAndFix( - key as UtopiaRequirement, - initialTexts[key as UtopiaRequirement], - ), + type: 'checkRequirements', + children: Object.keys(requirementsResolutions).map((key) => + importCheckRequirementAndFix(key as Requirement, initialTexts[key as Requirement]), ), }) } -export function notifyCheckingUtopiaRequirement(requirement: UtopiaRequirement, text: string) { - utopiaRequirementsResolutions[requirement].status = 'pending' +export function notifyCheckingRequirement(requirement: Requirement, text: string) { + requirementsResolutions[requirement].status = RequirementResolutionStatus.Pending notifyOperationStarted({ - type: 'checkUtopiaRequirementAndFix', + type: 'checkRequirementAndFix', id: requirement, text: text, }) } -export function notifyResolveUtopiaRequirement( - requirementName: UtopiaRequirement, - resolution: UtopiaRequirementResolutionResult, +export function notifyResolveRequirement( + requirementName: Requirement, + resolution: RequirementResolutionResult, text: string, value?: string, ) { - utopiaRequirementsResolutions[requirementName] = { - status: 'done', + requirementsResolutions[requirementName] = { + status: RequirementResolutionStatus.Done, resolution: resolution, value: value, } const result = - resolution === 'found' || resolution === 'fixed' - ? 'success' - : resolution === 'partial' - ? 'warn' - : 'error' + resolution === RequirementResolutionResult.Found || + resolution === RequirementResolutionResult.Fixed + ? ImportOperationResult.Success + : resolution === RequirementResolutionResult.Partial + ? ImportOperationResult.Warn + : ImportOperationResult.Error notifyOperationFinished( { - type: 'checkUtopiaRequirementAndFix', + type: 'checkRequirementAndFix', id: requirementName, text: text, resolution: resolution, }, result, ) - const aggregatedStatus = getAggregatedStatus() - if (aggregatedStatus != 'pending') { - notifyOperationFinished({ type: 'checkUtopiaRequirements' }, aggregatedStatus) + const aggregatedDoneStatus = getAggregatedStatus() + if (aggregatedDoneStatus != null) { + notifyOperationFinished({ type: 'checkRequirements' }, aggregatedDoneStatus) } } -function getAggregatedStatus(): ImportOperationResult | 'pending' { - for (const resolution of Object.values(utopiaRequirementsResolutions)) { - if (resolution.status != 'done') { - return 'pending' +function getAggregatedStatus(): ImportOperationResult | null { + for (const resolution of Object.values(requirementsResolutions)) { + if (resolution.status != RequirementResolutionStatus.Done) { + return null } - if (resolution.resolution == 'critical') { - return 'error' + if (resolution.resolution == RequirementResolutionResult.Critical) { + return ImportOperationResult.Error } - if (resolution.resolution == 'partial') { - return 'warn' + if (resolution.resolution == RequirementResolutionResult.Partial) { + return ImportOperationResult.Warn } } - return 'success' + return ImportOperationResult.Success } -type UtopiaRequirementResolutionStatus = 'not-started' | 'pending' | 'done' -type UtopiaRequirementResolutionResult = 'found' | 'fixed' | 'partial' | 'critical' - -type UtopiaRequirementResolution = { - status: UtopiaRequirementResolutionStatus - value?: string - resolution?: UtopiaRequirementResolutionResult +enum RequirementResolutionStatus { + NotStarted = 'not-started', + Pending = 'pending', + Done = 'done', } -export function checkAndFixUtopiaRequirements( - parsedProjectContents: ProjectContentTreeRoot, -): ProjectContentTreeRoot { - let projectContents = parsedProjectContents - // check package.json - projectContents = checkAndFixPackageJson(projectContents) - // check language - projectContents = checkProjectLanguage(projectContents) - // check react version - checkReactVersion(projectContents) - return projectContents -} - -function getPackageJson( - projectContents: ProjectContentTreeRoot, -): { utopia?: Record; dependencies?: Record } | null { - const packageJson = packageJsonFileFromProjectContents(projectContents) - if (packageJson != null && isTextFile(packageJson)) { - return JSON.parse(packageJson.fileContents.code) - } - return null +export enum RequirementResolutionResult { + Found = 'found', + Fixed = 'fixed', + Partial = 'partial', + Critical = 'critical', } -function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { - notifyCheckingUtopiaRequirement('packageJsonEntries', 'Checking package.json') - const parsedPackageJson = getPackageJson(projectContents) - if (parsedPackageJson == null) { - notifyResolveUtopiaRequirement( - 'packageJsonEntries', - 'critical', - 'The file package.json was not found', - ) - return projectContents - } - if (parsedPackageJson.utopia == null) { - parsedPackageJson.utopia = { - 'main-ui': 'utopia/storyboard.js', - } - const result = addFileToProjectContents( - projectContents, - '/package.json', - codeFile( - JSON.stringify(parsedPackageJson, null, 2), - null, - 0, - RevisionsState.CodeAheadButPleaseTellVSCodeAboutIt, - ), - ) - notifyResolveUtopiaRequirement( - 'packageJsonEntries', - 'fixed', - 'Fixed utopia entry in package.json', - ) - return result - } else { - notifyResolveUtopiaRequirement('packageJsonEntries', 'found', 'Valid package.json found') - } - - return projectContents -} - -function checkProjectLanguage(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { - notifyCheckingUtopiaRequirement('language', 'Checking project language') - let jsCount = 0 - let tsCount = 0 - applyToAllUIJSFiles(projectContents, (filename, uiJSFile) => { - if (filename.endsWith('.ts') || filename.endsWith('.tsx')) { - tsCount++ - } else if (filename.endsWith('.js') || filename.endsWith('.jsx')) { - jsCount++ - } - return uiJSFile - }) - if (tsCount > jsCount) { - notifyResolveUtopiaRequirement( - 'language', - 'critical', - 'Majority of project files are in TS/TSX', - 'typescript', - ) - } else { - notifyResolveUtopiaRequirement('language', 'found', 'Project uses JS/JSX', 'javascript') - } - return projectContents -} - -function checkReactVersion(projectContents: ProjectContentTreeRoot): void { - notifyCheckingUtopiaRequirement('reactVersion', 'Checking React version') - const parsedPackageJson = getPackageJson(projectContents) - if ( - parsedPackageJson == null || - parsedPackageJson.dependencies == null || - parsedPackageJson.dependencies.react == null - ) { - return notifyResolveUtopiaRequirement( - 'reactVersion', - 'critical', - 'React is not in dependencies', - ) - } - const reactVersion = parsedPackageJson.dependencies.react - // TODO: check react version - return notifyResolveUtopiaRequirement( - 'reactVersion', - 'found', - 'React version is ok', - reactVersion, - ) +type RequirementResolution = { + status: RequirementResolutionStatus + value?: string + resolution?: RequirementResolutionResult } From 4e969293fba4763e8e3718689a8259e98958e119 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 19:59:36 +0300 Subject: [PATCH 10/25] move state to editorState --- editor/src/components/editor/action-types.ts | 9 +- .../editor/actions/action-creators.ts | 25 +++-- .../components/editor/actions/action-utils.ts | 1 + .../src/components/editor/actions/actions.tsx | 27 +++-- .../components/editor/store/editor-state.ts | 70 ++++++++++++ .../components/editor/store/editor-update.tsx | 2 + .../store/store-deep-equality-instances.ts | 47 +++++++- .../store/store-hook-substore-helpers.ts | 5 +- .../editor/store/store-hook-substore-types.ts | 1 + .../shared/github/operations/load-branch.ts | 2 +- .../import/utopia-requirements-service.ts | 105 ++++++++++-------- 11 files changed, 226 insertions(+), 68 deletions(-) diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index 9a85929d173c..b177e8c452f6 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -63,6 +63,7 @@ import type { ColorSwatch, PostActionMenuData, ErrorBoundaryHandling, + ProjectRequirements, } from './store/editor-state' import type { Notice } from '../common/notice' import type { LoginState } from '../../common/user' @@ -1007,6 +1008,11 @@ export interface UpdateImportOperations { type: ImportOperationAction } +export interface UpdateProjectRequirements { + action: 'UPDATE_PROJECT_REQUIREMENTS' + requirements: Partial +} + export interface SetRefreshingDependencies { action: 'SET_REFRESHING_DEPENDENCIES' value: boolean @@ -1370,6 +1376,8 @@ export type EditorAction = | SetImageDragSessionState | UpdateGithubOperations | UpdateImportOperations + | UpdateProjectRequirements + | SetImportWizardOpen | UpdateBranchContents | SetRefreshingDependencies | ApplyCommandsAction @@ -1393,7 +1401,6 @@ export type EditorAction = | SetCollaborators | ExtractPropertyControlsFromDescriptorFiles | SetSharingDialogOpen - | SetImportWizardOpen | ResetOnlineState | IncreaseOnlineStateFailureCount | SetErrorBoundaryHandling diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 9ffd3e5e3edf..505c7e6d498f 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -239,6 +239,7 @@ import type { SetErrorBoundaryHandling, SetImportWizardOpen, UpdateImportOperations, + UpdateProjectRequirements, } from '../action-types' import type { InsertionSubjectWrapper, Mode } from '../editor-modes' import { EditorModes, insertionSubject } from '../editor-modes' @@ -259,6 +260,7 @@ import type { ColorSwatch, PostActionMenuData, ErrorBoundaryHandling, + ProjectRequirements, } from '../store/editor-state' import type { InsertionPath } from '../store/insertion-path' import type { TextProp } from '../../text-editor/text-editor' @@ -1608,6 +1610,22 @@ export function updateImportOperations( } } +export function updateProjectRequirements( + requirements: Partial, +): 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', @@ -1893,13 +1911,6 @@ export function setSharingDialogOpen(open: boolean): SetSharingDialogOpen { } } -export function setImportWizardOpen(open: boolean): SetImportWizardOpen { - return { - action: 'SET_IMPORT_WIZARD_OPEN', - open: open, - } -} - export function resetOnlineState(): ResetOnlineState { return { action: 'RESET_ONLINE_STATE', diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index 09b87f449f15..283f6f7a4c28 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -139,6 +139,7 @@ export function isTransientAction(action: EditorAction): boolean { case 'SET_ERROR_BOUNDARY_HANDLING': case 'SET_IMPORT_WIZARD_OPEN': case 'UPDATE_IMPORT_OPERATIONS': + case 'UPDATE_PROJECT_REQUIREMENTS': return true case 'TRUE_UP_ELEMENTS': diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index dd56ff64e90b..2ceac20d2cdb 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -354,6 +354,7 @@ import type { SetErrorBoundaryHandling, SetImportWizardOpen, UpdateImportOperations, + UpdateProjectRequirements, } from '../action-types' import { isAlignment, isLoggedIn } from '../action-types' import type { Mode } from '../editor-modes' @@ -637,6 +638,7 @@ import { notifyCheckingRequirement, notifyResolveRequirement, RequirementResolutionResult, + updateRequirements, } from '../../../core/shared/import/utopia-requirements-service' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -1042,6 +1044,8 @@ export function restoreEditorState( imageDragSessionState: currentEditor.imageDragSessionState, githubOperations: currentEditor.githubOperations, importOperations: currentEditor.importOperations, + projectRequirements: currentEditor.projectRequirements, + importWizardOpen: currentEditor.importWizardOpen, branchOriginContents: currentEditor.branchOriginContents, githubData: currentEditor.githubData, refreshingDependencies: currentEditor.refreshingDependencies, @@ -1053,7 +1057,6 @@ export function restoreEditorState( forking: currentEditor.forking, collaborators: currentEditor.collaborators, sharingDialogOpen: currentEditor.sharingDialogOpen, - importWizardOpen: currentEditor.importWizardOpen, editorRemixConfig: currentEditor.editorRemixConfig, } } @@ -2270,6 +2273,22 @@ export const UPDATE_FNS = { importOperations: resultImportOperations, } }, + UPDATE_PROJECT_REQUIREMENTS: ( + action: UpdateProjectRequirements, + editor: EditorModel, + ): EditorModel => { + const result = updateRequirements(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, @@ -6159,12 +6178,6 @@ export const UPDATE_FNS = { sharingDialogOpen: action.open, } }, - SET_IMPORT_WIZARD_OPEN: (action: SetImportWizardOpen, editor: EditorModel): EditorModel => { - return { - ...editor, - importWizardOpen: action.open, - } - }, SET_ERROR_BOUNDARY_HANDLING: ( action: SetErrorBoundaryHandling, editor: EditorModel, diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 8db8b1a4810c..eeacbb5a8f9c 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -190,6 +190,7 @@ 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 type { RequirementResolutionResult } from '../../../core/shared/import/utopia-requirements-service' const ObjectPathImmutable: any = OPI @@ -1163,6 +1164,70 @@ export interface PullRequest { number: number } +export interface RequirementResolution { + status: RequirementResolutionStatus + value?: string | null + resolution?: RequirementResolutionResult | null +} + +export function requirementResolution( + status: RequirementResolutionStatus, + value?: string | null, + resolution?: RequirementResolutionResult | null, +): RequirementResolution { + return { + status, + value, + resolution, + } +} + +export interface ProjectRequirements { + storyboard: RequirementResolution + packageJsonEntries: RequirementResolution + language: RequirementResolution + reactVersion: RequirementResolution +} + +export type ProjectRequirement = keyof ProjectRequirements + +export function newProjectRequirements( + storyboard: RequirementResolution, + packageJsonEntries: RequirementResolution, + language: RequirementResolution, + reactVersion: RequirementResolution, +): ProjectRequirements { + return { + storyboard, + packageJsonEntries, + language, + reactVersion, + } +} + +export enum RequirementResolutionStatus { + NotStarted = 'not-started', + Pending = 'pending', + Done = 'done', +} + +export function emptyRequirementResolution(): RequirementResolution { + return { + status: RequirementResolutionStatus.NotStarted, + value: null, + resolution: null, + } +} + +export function emptyProjectRequirements(): ProjectRequirements { + return newProjectRequirements( + emptyRequirementResolution(), + emptyRequirementResolution(), + emptyRequirementResolution(), + emptyRequirementResolution(), + ) +} + export interface ProjectGithubSettings { targetRepository: GithubRepo | null originCommit: string | null @@ -1454,6 +1519,7 @@ export interface EditorState { imageDragSessionState: ImageDragSessionState githubOperations: Array importOperations: Array + projectRequirements: ProjectRequirements githubData: GithubData refreshingDependencies: boolean colorSwatches: Array @@ -1551,6 +1617,7 @@ export function editorState( collaborators: Collaborator[], sharingDialogOpen: boolean, importWizardOpen: boolean, + projectRequirements: ProjectRequirements, remixConfig: EditorRemixConfig, ): EditorState { return { @@ -1636,6 +1703,7 @@ export function editorState( collaborators: collaborators, sharingDialogOpen: sharingDialogOpen, importWizardOpen: importWizardOpen, + projectRequirements: projectRequirements, editorRemixConfig: remixConfig, } } @@ -2719,6 +2787,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { collaborators: [], sharingDialogOpen: false, importWizardOpen: false, + projectRequirements: emptyProjectRequirements(), editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, @@ -3088,6 +3157,7 @@ export function editorModelFromPersistentModel( collaborators: [], sharingDialogOpen: false, importWizardOpen: false, + projectRequirements: emptyProjectRequirements(), editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index 8c0634e4be53..d2400561b29a 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -224,6 +224,8 @@ export function runSimpleLocalEditorAction( return UPDATE_FNS.UPDATE_IMPORT_OPERATIONS(action, state) case 'SET_IMPORT_WIZARD_OPEN': return UPDATE_FNS.SET_IMPORT_WIZARD_OPEN(action, state) + case 'UPDATE_PROJECT_REQUIREMENTS': + return UPDATE_FNS.UPDATE_PROJECT_REQUIREMENTS(action, state) case 'REMOVE_TOAST': return UPDATE_FNS.REMOVE_TOAST(action, state) case 'SET_HIGHLIGHTED_VIEWS': 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 48ad8c11fcc0..26788d4c07f5 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -362,6 +362,8 @@ import type { EditorRemixConfig, ErrorBoundaryHandling, GridControlData, + ProjectRequirements, + RequirementResolution, } from './editor-state' import { trueUpGroupElementChanged, @@ -374,6 +376,8 @@ import { newGithubData, renderedAtPropertyPath, renderedAtChildNode, + requirementResolution, + newProjectRequirements, } from './editor-state' import { editorStateNodeModules, @@ -4756,6 +4760,30 @@ export const ProjectGithubSettingsKeepDeepEquality: KeepDeepEqualityCall = + combine3EqualityCalls( + (resolution) => resolution.status, + createCallWithTripleEquals(), + (resolution) => resolution.value, + createCallWithTripleEquals(), + (resolution) => resolution.resolution, + createCallWithTripleEquals(), + requirementResolution, + ) + +export const ProjectRequirementsKeepDeepEquality: KeepDeepEqualityCall = + combine4EqualityCalls( + (requirements) => requirements.storyboard, + ProjectRequirementResolutionKeepDeepEquality, + (requirements) => requirements.packageJsonEntries, + ProjectRequirementResolutionKeepDeepEquality, + (requirements) => requirements.language, + ProjectRequirementResolutionKeepDeepEquality, + (requirements) => requirements.reactVersion, + ProjectRequirementResolutionKeepDeepEquality, + newProjectRequirements, + ) + export const GithubFileChangesKeepDeepEquality: KeepDeepEqualityCall = combine3EqualityCalls( (settings) => settings.modified, @@ -5379,6 +5407,16 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( newValue.importOperations, ) + const importWizardOpenResults = BooleanKeepDeepEquality( + oldValue.importWizardOpen, + newValue.importWizardOpen, + ) + + const projectRequirementsResults = ProjectRequirementsKeepDeepEquality( + oldValue.projectRequirements, + newValue.projectRequirements, + ) + const branchContentsResults = nullableDeepEquality(ProjectContentTreeRootKeepDeepEquality())( oldValue.branchOriginContents, newValue.branchOriginContents, @@ -5426,11 +5464,6 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( newValue.sharingDialogOpen, ) - const importWizardOpenResults = BooleanKeepDeepEquality( - oldValue.importWizardOpen, - newValue.importWizardOpen, - ) - const remixConfigResults = RemixConfigKeepDeepEquality( oldValue.editorRemixConfig, newValue.editorRemixConfig, @@ -5506,6 +5539,8 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( imageDragSessionStateEqual.areEqual && githubOperationsResults.areEqual && importOperationsResults.areEqual && + importWizardOpenResults.areEqual && + projectRequirementsResults.areEqual && branchContentsResults.areEqual && githubDataResults.areEqual && refreshingDependenciesResults.areEqual && @@ -5517,7 +5552,6 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( forkingResults.areEqual && collaboratorsResults.areEqual && sharingDialogOpenResults.areEqual && - importWizardOpenResults.areEqual && remixConfigResults.areEqual if (areEqual) { @@ -5606,6 +5640,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( collaboratorsResults.value, sharingDialogOpenResults.value, importWizardOpenResults.value, + projectRequirementsResults.value, remixConfigResults.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 686dbffc1c01..cf15902bbebc 100644 --- a/editor/src/components/editor/store/store-hook-substore-helpers.ts +++ b/editor/src/components/editor/store/store-hook-substore-helpers.ts @@ -1,4 +1,4 @@ -import type { EditorState } from './editor-state' +import { emptyProjectRequirements, type EditorState } from './editor-state' export const EmptyEditorStateForKeysOnly: EditorState = { id: null, @@ -158,6 +158,8 @@ export const EmptyEditorStateForKeysOnly: EditorState = { imageDragSessionState: null as any, githubOperations: [], importOperations: [], + importWizardOpen: false, + projectRequirements: emptyProjectRequirements(), branchOriginContents: null, githubData: null as any, refreshingDependencies: false, @@ -172,7 +174,6 @@ export const EmptyEditorStateForKeysOnly: EditorState = { forking: false, collaborators: [], sharingDialogOpen: false, - importWizardOpen: false, editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, 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 ce685e2d7dfe..b08ce4a6da76 100644 --- a/editor/src/components/editor/store/store-hook-substore-types.ts +++ b/editor/src/components/editor/store/store-hook-substore-types.ts @@ -179,6 +179,7 @@ export const githubSubstateKeys = [ 'githubOperations', 'githubData', 'importOperations', + 'projectRequirements', ] as const export const emptyGithubSubstate = { editor: pick(githubSubstateKeys, EmptyEditorStateForKeysOnly), diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index ba6e7d50db37..3e4086d20663 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -172,7 +172,7 @@ export const updateProjectWithBranchContent = ) notifyOperationFinished({ type: 'parseFiles' }, ImportOperationResult.Success) - resetRequirementsResolutions() + resetRequirementsResolutions(dispatch) const parsedProjectContentsInitial = createStoryboardFileIfNecessary( parseResults, 'create-placeholder', diff --git a/editor/src/core/shared/import/utopia-requirements-service.ts b/editor/src/core/shared/import/utopia-requirements-service.ts index bcc1d28300ea..17e96e6f6b28 100644 --- a/editor/src/core/shared/import/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/utopia-requirements-service.ts @@ -1,42 +1,46 @@ import { importCheckRequirementAndFix, ImportOperationResult } from './import-operation-types' import { notifyOperationFinished, notifyOperationStarted } from './import-operation-service' +import type { ProjectRequirements } from '../../../components/editor/store/editor-state' +import { + emptyProjectRequirements, + RequirementResolutionStatus, + type ProjectRequirement, +} from '../../../components/editor/store/editor-state' +import type { EditorDispatch } from '../../../components/editor/action-types' +import { updateProjectRequirements } from '../../../components/editor/actions/action-creators' -let requirementsResolutions: Record = { - storyboard: initialResolution(), - packageJsonEntries: initialResolution(), - language: initialResolution(), - reactVersion: initialResolution(), -} - -type Requirement = keyof typeof requirementsResolutions +let editorDispatch: EditorDispatch | null = null -const initialTexts: Record = { +const initialTexts: Record = { storyboard: 'Checking storyboard.js', packageJsonEntries: 'Checking package.json', language: 'Checking project language', reactVersion: 'Checking React version', } -function initialResolution(): RequirementResolution { - return { - status: RequirementResolutionStatus.NotStarted, - } -} - -export function resetRequirementsResolutions() { - requirementsResolutions = Object.fromEntries( - Object.keys(requirementsResolutions).map((key) => [key, initialResolution()]), - ) as Record +export function resetRequirementsResolutions(dispatch: EditorDispatch) { + editorDispatch = dispatch + let projectRequirements = emptyProjectRequirements() + editorDispatch?.([updateProjectRequirements(projectRequirements)]) notifyOperationStarted({ type: 'checkRequirements', - children: Object.keys(requirementsResolutions).map((key) => - importCheckRequirementAndFix(key as Requirement, initialTexts[key as Requirement]), + children: Object.keys(projectRequirements).map((key) => + importCheckRequirementAndFix( + key as ProjectRequirement, + initialTexts[key as ProjectRequirement], + ), ), }) } -export function notifyCheckingRequirement(requirement: Requirement, text: string) { - requirementsResolutions[requirement].status = RequirementResolutionStatus.Pending +export function notifyCheckingRequirement(requirement: ProjectRequirement, text: string) { + editorDispatch?.([ + updateProjectRequirements({ + [requirement]: { + status: RequirementResolutionStatus.Pending, + }, + }), + ]) notifyOperationStarted({ type: 'checkRequirementAndFix', id: requirement, @@ -45,16 +49,20 @@ export function notifyCheckingRequirement(requirement: Requirement, text: string } export function notifyResolveRequirement( - requirementName: Requirement, + requirementName: ProjectRequirement, resolution: RequirementResolutionResult, text: string, value?: string, ) { - requirementsResolutions[requirementName] = { - status: RequirementResolutionStatus.Done, - resolution: resolution, - value: value, - } + editorDispatch?.([ + updateProjectRequirements({ + [requirementName]: { + status: RequirementResolutionStatus.Done, + resolution: resolution, + value: value, + }, + }), + ]) const result = resolution === RequirementResolutionResult.Found || resolution === RequirementResolutionResult.Fixed @@ -71,13 +79,34 @@ export function notifyResolveRequirement( }, result, ) - const aggregatedDoneStatus = getAggregatedStatus() +} + +export function updateRequirements( + existingRequirements: ProjectRequirements, + incomingRequirements: Partial, +): ProjectRequirements { + let result = { ...existingRequirements } + for (const incomingRequirement of Object.keys(incomingRequirements)) { + const incomingRequirementName = incomingRequirement as ProjectRequirement + result[incomingRequirementName] = { + ...result[incomingRequirementName], + ...incomingRequirements[incomingRequirementName], + } + } + + const aggregatedDoneStatus = getAggregatedStatus(result) if (aggregatedDoneStatus != null) { - notifyOperationFinished({ type: 'checkRequirements' }, aggregatedDoneStatus) + setTimeout(() => { + notifyOperationFinished({ type: 'checkRequirements' }, aggregatedDoneStatus) + }, 0) } + + return result } -function getAggregatedStatus(): ImportOperationResult | null { +function getAggregatedStatus( + requirementsResolutions: ProjectRequirements, +): ImportOperationResult | null { for (const resolution of Object.values(requirementsResolutions)) { if (resolution.status != RequirementResolutionStatus.Done) { return null @@ -92,21 +121,9 @@ function getAggregatedStatus(): ImportOperationResult | null { return ImportOperationResult.Success } -enum RequirementResolutionStatus { - NotStarted = 'not-started', - Pending = 'pending', - Done = 'done', -} - export enum RequirementResolutionResult { Found = 'found', Fixed = 'fixed', Partial = 'partial', Critical = 'critical', } - -type RequirementResolution = { - status: RequirementResolutionStatus - value?: string - resolution?: RequirementResolutionResult -} From 809a8ef68991075f08b75d5b6cd1f496eb820c54 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 20:06:01 +0300 Subject: [PATCH 11/25] organize --- editor/src/components/editor/action-types.ts | 10 +++--- .../components/editor/store/editor-state.ts | 18 +++++----- .../store/store-deep-equality-instances.ts | 34 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index b177e8c452f6..afb03820a563 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -1013,6 +1013,11 @@ export interface UpdateProjectRequirements { requirements: Partial } +export interface SetImportWizardOpen { + action: 'SET_IMPORT_WIZARD_OPEN' + open: boolean +} + export interface SetRefreshingDependencies { action: 'SET_REFRESHING_DEPENDENCIES' value: boolean @@ -1194,11 +1199,6 @@ export interface SetSharingDialogOpen { open: boolean } -export interface SetImportWizardOpen { - action: 'SET_IMPORT_WIZARD_OPEN' - open: boolean -} - export interface ResetOnlineState { action: 'RESET_ONLINE_STATE' } diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index eeacbb5a8f9c..dd6463e1edc1 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -1520,6 +1520,7 @@ export interface EditorState { githubOperations: Array importOperations: Array projectRequirements: ProjectRequirements + importWizardOpen: boolean githubData: GithubData refreshingDependencies: boolean colorSwatches: Array @@ -1530,7 +1531,6 @@ export interface EditorState { forking: boolean collaborators: Collaborator[] sharingDialogOpen: boolean - importWizardOpen: boolean editorRemixConfig: EditorRemixConfig } @@ -1605,6 +1605,8 @@ export function editorState( imageDragSessionState: ImageDragSessionState, githubOperations: Array, importOperations: Array, + importWizardOpen: boolean, + projectRequirements: ProjectRequirements, branchOriginContents: ProjectContentTreeRoot | null, githubData: GithubData, refreshingDependencies: boolean, @@ -1616,8 +1618,6 @@ export function editorState( forking: boolean, collaborators: Collaborator[], sharingDialogOpen: boolean, - importWizardOpen: boolean, - projectRequirements: ProjectRequirements, remixConfig: EditorRemixConfig, ): EditorState { return { @@ -1692,6 +1692,8 @@ export function editorState( imageDragSessionState: imageDragSessionState, githubOperations: githubOperations, importOperations: importOperations, + importWizardOpen: importWizardOpen, + projectRequirements: projectRequirements, githubData: githubData, refreshingDependencies: refreshingDependencies, colorSwatches: colorSwatches, @@ -1702,8 +1704,6 @@ export function editorState( forking: forking, collaborators: collaborators, sharingDialogOpen: sharingDialogOpen, - importWizardOpen: importWizardOpen, - projectRequirements: projectRequirements, editorRemixConfig: remixConfig, } } @@ -2772,6 +2772,8 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { imageDragSessionState: notDragging(), githubOperations: [], importOperations: [], + importWizardOpen: false, + projectRequirements: emptyProjectRequirements(), branchOriginContents: null, githubData: emptyGithubData(), refreshingDependencies: false, @@ -2786,8 +2788,6 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { forking: false, collaborators: [], sharingDialogOpen: false, - importWizardOpen: false, - projectRequirements: emptyProjectRequirements(), editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, @@ -3142,6 +3142,8 @@ export function editorModelFromPersistentModel( imageDragSessionState: notDragging(), githubOperations: [], importOperations: [], + importWizardOpen: false, + projectRequirements: emptyProjectRequirements(), refreshingDependencies: false, branchOriginContents: null, githubData: emptyGithubData(), @@ -3156,8 +3158,6 @@ export function editorModelFromPersistentModel( forking: false, collaborators: [], sharingDialogOpen: false, - importWizardOpen: false, - projectRequirements: emptyProjectRequirements(), editorRemixConfig: { errorBoundaryHandling: 'ignore-error-boundaries', }, 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 26788d4c07f5..99ba69a1e231 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -4784,6 +4784,21 @@ export const ProjectRequirementsKeepDeepEquality: KeepDeepEqualityCall = ( + oldValue, + newValue, +) => { + if (oldValue.type !== newValue.type) { + return keepDeepEqualityResult(newValue, false) + } else if (oldValue.id !== newValue.id) { + return keepDeepEqualityResult(newValue, false) + } + return keepDeepEqualityResult(oldValue, true) +} + +export const ImportOperationsKeepDeepEquality: KeepDeepEqualityCall> = + arrayDeepEquality(ImportOperationKeepDeepEquality) + export const GithubFileChangesKeepDeepEquality: KeepDeepEqualityCall = combine3EqualityCalls( (settings) => settings.modified, @@ -4854,24 +4869,9 @@ export const GithubOperationKeepDeepEquality: KeepDeepEqualityCall = ( - oldValue, - newValue, -) => { - if (oldValue.type !== newValue.type) { - return keepDeepEqualityResult(newValue, false) - } else if (oldValue.id !== newValue.id) { - return keepDeepEqualityResult(newValue, false) - } - return keepDeepEqualityResult(oldValue, true) -} - export const GithubOperationsKeepDeepEquality: KeepDeepEqualityCall> = arrayDeepEquality(GithubOperationKeepDeepEquality) -export const ImportOperationsKeepDeepEquality: KeepDeepEqualityCall> = - arrayDeepEquality(ImportOperationKeepDeepEquality) - export const ColorSwatchDeepEquality: KeepDeepEqualityCall = combine2EqualityCalls( (c) => c.id, StringKeepDeepEquality, @@ -5628,6 +5628,8 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( imageDragSessionStateEqual.value, githubOperationsResults.value, importOperationsResults.value, + importWizardOpenResults.value, + projectRequirementsResults.value, branchContentsResults.value, githubDataResults.value, refreshingDependenciesResults.value, @@ -5639,8 +5641,6 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( forkingResults.value, collaboratorsResults.value, sharingDialogOpenResults.value, - importWizardOpenResults.value, - projectRequirementsResults.value, remixConfigResults.value, ) From 37375a0d3a9a7716a59fd8140575f8712d68e3f5 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 10 Oct 2024 20:51:42 +0300 Subject: [PATCH 12/25] collapse met requirements --- .../editor/import-wizard/components.tsx | 52 +++++++++++++------ .../shared/import/import-operation-types.ts | 15 +++--- .../import/utopia-requirements-service.ts | 4 +- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx index fdd777911532..d2f331e3b60b 100644 --- a/editor/src/components/editor/import-wizard/components.tsx +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -2,7 +2,11 @@ /** @jsx jsx */ import { jsx } from '@emotion/react' import React from 'react' -import type { ImportOperation } from '../../../core/shared/import/import-operation-types' +import type { + ImportCheckRequirementAndFix, + ImportFetchDependency, + ImportOperation, +} from '../../../core/shared/import/import-operation-types' import { ImportOperationResult } from '../../../core/shared/import/import-operation-types' import { assertNever } from '../../../core/shared/utils' import { Icons } from '../../../uuiui' @@ -40,6 +44,9 @@ export function OperationLine({ operation }: { operation: ImportOperation }) { } function OperationChildrenList({ operation }: { operation: ImportOperation }) { + if (operation.type !== 'checkRequirements' && operation.type !== 'refreshDependencies') { + return null + } if (operation.children == null || operation.children.length === 0) { return null } @@ -53,34 +60,47 @@ function OperationChildrenList({ operation }: { operation: ImportOperation }) { }} > {operation.type === 'refreshDependencies' ? ( - - ) : ( - operation.children.map((child) => ( - - )) - )} + ) : operation.type === 'checkRequirements' ? ( + + ) : null}
) } +const dependenciesSuccessFn = (op: ImportFetchDependency) => + op.result === ImportOperationResult.Success +const dependenciesSuccessTextFn = (successCount: number) => + `${successCount} dependencies fetched successfully` +const requirementsSuccessFn = (op: ImportCheckRequirementAndFix) => + op.resolution === RequirementResolutionResult.Found +const requirementsSuccessTextFn = (successCount: number) => `${successCount} requirements met` -function DependenciesStatus({ - dependenciesOperations, +function AggregatedChildrenStatus({ + childOperations, + successFn, + successTextFn, }: { - dependenciesOperations: ImportOperation[] - parentOperation: ImportOperation + childOperations: T[] + successFn: (operation: T) => boolean + successTextFn: (successCount: number) => string }) { - const doneDependencies = dependenciesOperations.filter((op) => op.result === 'success') - const restOfDependencies = dependenciesOperations.filter((op) => op.result !== 'success') + const doneDependencies = childOperations.filter(successFn) + const restOfDependencies = childOperations.filter((op) => !successFn(op)) return ( {doneDependencies.length > 0 ? ( -
{`${doneDependencies.length} dependencies fetched successfully`}
+
{successTextFn(doneDependencies.length)}
) : null} diff --git a/editor/src/core/shared/import/import-operation-types.ts b/editor/src/core/shared/import/import-operation-types.ts index 30781889abb7..62c0714e8864 100644 --- a/editor/src/core/shared/import/import-operation-types.ts +++ b/editor/src/core/shared/import/import-operation-types.ts @@ -9,7 +9,6 @@ type ImportOperationData = { result?: ImportOperationResult error?: string parentOperationType?: ImportOperationType - children?: ImportOperation[] } export enum ImportOperationResult { @@ -24,11 +23,12 @@ type ImportLoadBranch = { githubRepo?: GithubRepo } & ImportOperationData -type ImportRefreshDependencies = { +type ImportRefreshDependencies = ImportOperationData & { type: 'refreshDependencies' -} & ImportOperationData + children?: ImportFetchDependency[] +} -type ImportFetchDependency = ImportOperationData & { +export type ImportFetchDependency = ImportOperationData & { type: 'fetchDependency' dependencyName: string dependencyVersion: string @@ -39,7 +39,7 @@ type ImportParseFiles = { type: 'parseFiles' } & ImportOperationData -type ImportCheckRequirementAndFix = ImportOperationData & { +export type ImportCheckRequirementAndFix = ImportOperationData & { type: 'checkRequirementAndFix' resolution?: RequirementResolutionResult text: string @@ -57,9 +57,10 @@ export function importCheckRequirementAndFix( } } -type ImportCheckRequirements = { +type ImportCheckRequirements = ImportOperationData & { type: 'checkRequirements' -} & ImportOperationData + children?: ImportCheckRequirementAndFix[] +} export type ImportOperation = | ImportLoadBranch diff --git a/editor/src/core/shared/import/utopia-requirements-service.ts b/editor/src/core/shared/import/utopia-requirements-service.ts index 17e96e6f6b28..a4075282f3cf 100644 --- a/editor/src/core/shared/import/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/utopia-requirements-service.ts @@ -96,9 +96,9 @@ export function updateRequirements( const aggregatedDoneStatus = getAggregatedStatus(result) if (aggregatedDoneStatus != null) { - setTimeout(() => { + requestIdleCallback(() => { notifyOperationFinished({ type: 'checkRequirements' }, aggregatedDoneStatus) - }, 0) + }) } return result From 8eb5f4f37be5f325575f0019249fea52d54da05c Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sun, 13 Oct 2024 12:15:55 +0300 Subject: [PATCH 13/25] colors --- .../editor/import-wizard/components.tsx | 79 +++++++++++++------ .../editor/import-wizard/import-wizard.tsx | 47 ++++++++--- .../shared/import/import-operation-service.ts | 4 +- .../shared/import/import-operation-types.ts | 4 +- 4 files changed, 91 insertions(+), 43 deletions(-) diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx index d2f331e3b60b..97dbacdfa570 100644 --- a/editor/src/components/editor/import-wizard/components.tsx +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -35,7 +35,7 @@ export function OperationLine({ operation }: { operation: ImportOperation }) {
{getImportOperationText(operation)}
- +
@@ -44,9 +44,6 @@ export function OperationLine({ operation }: { operation: ImportOperation }) { } function OperationChildrenList({ operation }: { operation: ImportOperation }) { - if (operation.type !== 'checkRequirements' && operation.type !== 'refreshDependencies') { - return null - } if (operation.children == null || operation.children.length === 0) { return null } @@ -61,13 +58,13 @@ function OperationChildrenList({ operation }: { operation: ImportOperation }) { > {operation.type === 'refreshDependencies' ? ( ) : operation.type === 'checkRequirements' ? ( @@ -98,8 +95,8 @@ function AggregatedChildrenStatus({ {doneDependencies.length > 0 ? ( - - + +
{successTextFn(doneDependencies.length)}
@@ -118,20 +115,30 @@ function OperationIcon({ runningStatus: 'waiting' | 'running' | 'done' result?: ImportOperationResult }) { + const iconColorStyle = React.useMemo( + () => (result != null ? getIconColorStyle(result) : {}), + [result], + ) if (runningStatus === 'running') { return } else if (runningStatus === 'done' && result === 'success') { - return + return } else if (runningStatus === 'done' && result === 'warn') { - return + return } else if (runningStatus === 'waiting') { return } else { - return + return } } -function TimeFromInSeconds({ operation }: { operation: ImportOperation }) { +function TimeFromInSeconds({ + operation, + runningStatus, +}: { + operation: ImportOperation + runningStatus: 'waiting' | 'running' | 'done' +}) { const [currentTime, setCurrentTime] = React.useState(Date.now()) React.useEffect(() => { const interval = setInterval(() => { @@ -153,7 +160,15 @@ function TimeFromInSeconds({ operation }: { operation: ImportOperation }) { ? (operationTime / 1000).toFixed(2) : Math.max(Math.floor(operationTime / 1000), 0) return operation.timeStarted == null ? null : ( -
{timeInSeconds}s
+
+ {timeInSeconds}s +
) } @@ -249,18 +264,32 @@ function getTextColor( ) { if (operationRunningStatus === 'waiting') { return 'gray' - } else if (operationRunningStatus === 'running') { - return 'black' - } else if ( - operation.type === 'checkRequirementAndFix' && - operation.resolution === RequirementResolutionResult.Fixed - ) { - return 'var(--utopitheme-primary)' - } else if (operation.result === ImportOperationResult.Success) { - return 'green' - } else if (operation.result === ImportOperationResult.Warn) { - return 'orange' } else { - return 'var(--utopitheme-errorForeground)' + return 'black' + } +} + +function getIconColorStyle(result: ImportOperationResult) { + // temp solution since we currently only have black icons + // https://codepen.io/sosuke/pen/Pjoqqp + if (result === ImportOperationResult.Error) { + return { + // our error red + filter: + 'invert(14%) sepia(99%) saturate(4041%) hue-rotate(328deg) brightness(101%) contrast(115%)', + } + } else if (result === ImportOperationResult.Warn) { + return { + // orange + filter: + 'invert(72%) sepia(90%) saturate(3088%) hue-rotate(1deg) brightness(105%) contrast(104%)', + } + } else if (result === ImportOperationResult.Success) { + return { + // green + filter: + 'invert(72%) sepia(60%) saturate(3628%) hue-rotate(126deg) brightness(104%) contrast(76%)', + } } + return {} } diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 72971b4e28c3..1deadf730c4e 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -1,6 +1,9 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ import React from 'react' +import { jsx } from '@emotion/react' import { getProjectID } from '../../../common/env-vars' -import { Button, Icons, useColorTheme, UtopiaStyles } from '../../../uuiui' +import { Button, FlexRow, H2, Icons, 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' @@ -66,25 +69,32 @@ export const ImportWizard = React.memo(() => { fontSize: '14px', lineHeight: 'normal', letterSpacing: 'normal', - padding: 40, + padding: 20, overflow: 'hidden', }} onClick={stopPropagation} > - +
Project Import
+ +
{ overflow: 'scroll', height: '100%', width: '100%', + // padding: 20, + marginTop: 20, }} > {operations.map((operation) => ( ))}
+
, )} diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index 4e6c58b7d818..0536c59194da 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -70,10 +70,10 @@ export function areSameOperation(existing: ImportOperation, incoming: ImportOper export const defaultParentTypes: Partial> = { checkRequirementAndFix: 'checkRequirements', fetchDependency: 'refreshDependencies', -} +} as const function getParentArray(root: ImportOperation[], operation: ImportOperation): ImportOperation[] { - const parentOperationType = operation.parentOperationType ?? defaultParentTypes[operation.type] + const parentOperationType = defaultParentTypes[operation.type] if (parentOperationType == null) { return root } diff --git a/editor/src/core/shared/import/import-operation-types.ts b/editor/src/core/shared/import/import-operation-types.ts index 62c0714e8864..e0e1fcb0186b 100644 --- a/editor/src/core/shared/import/import-operation-types.ts +++ b/editor/src/core/shared/import/import-operation-types.ts @@ -8,7 +8,7 @@ type ImportOperationData = { timeDone?: number | null result?: ImportOperationResult error?: string - parentOperationType?: ImportOperationType + children?: ImportOperation[] } export enum ImportOperationResult { @@ -25,7 +25,6 @@ type ImportLoadBranch = { type ImportRefreshDependencies = ImportOperationData & { type: 'refreshDependencies' - children?: ImportFetchDependency[] } export type ImportFetchDependency = ImportOperationData & { @@ -59,7 +58,6 @@ export function importCheckRequirementAndFix( type ImportCheckRequirements = ImportOperationData & { type: 'checkRequirements' - children?: ImportCheckRequirementAndFix[] } export type ImportOperation = From d0fb49d5b730ad72451f37a8912c940f7907bccd Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 14 Oct 2024 18:20:40 +0200 Subject: [PATCH 14/25] design --- .../editor/import-wizard/components.tsx | 41 ++++++-- .../editor/import-wizard/import-wizard.tsx | 97 ++++++++++++++++++- .../import/utopia-requirements-service.ts | 4 +- 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx index 97dbacdfa570..1259c8807d0e 100644 --- a/editor/src/components/editor/import-wizard/components.tsx +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -27,9 +27,25 @@ export function OperationLine({ operation }: { operation: ImportOperation }) { [operationRunningStatus, operation], ) + const [childrenShown, serChildrenShown] = React.useState(false) + const shouldShowChildren = React.useMemo( + () => childrenShown || operation.timeDone == null, + [childrenShown, operation.timeDone], + ) + const hasChildren = React.useMemo( + () => operation.children != null && operation.children.length > 0, + [operation.children], + ) + const toggleShowChildren = React.useCallback(() => { + if (hasChildren) { + serChildrenShown((shown) => !shown) + } + }, [hasChildren]) + return ( @@ -37,8 +53,13 @@ export function OperationLine({ operation }: { operation: ImportOperation }) {
+ {hasChildren ? ( +
+ {shouldShowChildren ? : } +
+ ) : null}
- + {shouldShowChildren && hasChildren ? : null}
) } @@ -63,11 +84,12 @@ function OperationChildrenList({ operation }: { operation: ImportOperation }) { successTextFn={dependenciesSuccessTextFn} /> ) : operation.type === 'checkRequirements' ? ( - + operation.children.map((childOperation) => ( + + )) ) : null} ) @@ -175,9 +197,11 @@ function TimeFromInSeconds({ function OperationLineWrapper({ children, className, + onClick, }: { children: React.ReactNode className: string + onClick?: () => void }) { return (
&': { - paddingLeft: 10, + paddingLeft: 26, fontSize: 12, img: { width: 12, @@ -200,6 +224,7 @@ function OperationLineWrapper({ visibility: 'hidden', }, }} + onClick={onClick} > {children}
@@ -218,7 +243,7 @@ function OperationLineContent({ className='import-wizard-operation-line-content' style={{ display: 'grid', - gridTemplateColumns: '15px 1fr 50px', + gridTemplateColumns: '15px max-content 1fr 14px', gap: 10, alignItems: 'center', color: textColor, diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 1deadf730c4e..670750a0f62f 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -3,15 +3,19 @@ import React from 'react' import { jsx } from '@emotion/react' import { getProjectID } from '../../../common/env-vars' -import { Button, FlexRow, H2, Icons, useColorTheme, UtopiaStyles } from '../../../uuiui' +import { Button, FlexRow, Icons, 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 { OperationLine } from './components' +import { + ImportOperationResult, + type ImportOperation, +} from '../../../core/shared/import/import-operation-types' +import { assertNever } from '../../../core/shared/utils' export const ImportWizard = React.memo(() => { const colorTheme = useColorTheme() - const projectId = getProjectID() const importWizardOpen: boolean = useEditorState( @@ -114,14 +118,99 @@ export const ImportWizard = React.memo(() => { className='import-wizard-footer' css={{ display: 'flex', - justifyContent: 'flex-end', + justifyContent: 'space-between', + alignItems: 'center', width: '100%', marginTop: 20, }} - > + > + + , )} ) }) ImportWizard.displayName = 'ImportWizard' + +function ActionButtons({ operations }: { operations: ImportOperation[] }) { + let totalResult: ImportOperationResult | null = React.useMemo(() => { + let result = ImportOperationResult.Success + for (const operation of operations) { + if (operation.timeDone == null || operation.result == null) { + return null + } + if (operation.result == ImportOperationResult.Error) { + return ImportOperationResult.Error + } + if (operation.result == ImportOperationResult.Warn) { + result = ImportOperationResult.Warn + } + } + return result + }, [operations]) + const textColor = React.useMemo(() => { + switch (totalResult) { + case ImportOperationResult.Success: + return 'green' + case ImportOperationResult.Warn: + return 'orange' + case ImportOperationResult.Error: + return 'var(--utopitheme-githubIndicatorFailed)' + case null: + return 'black' + default: + assertNever(totalResult) + } + }, [totalResult]) + const buttonColor = React.useMemo(() => { + switch (totalResult) { + case ImportOperationResult.Success: + return 'var(--utopitheme-green)' + case ImportOperationResult.Warn: + return 'var(--utopitheme-githubMUDModified)' + case ImportOperationResult.Error: + return 'var(--utopitheme-githubIndicatorFailed)' + case null: + return 'black' + default: + assertNever(totalResult) + } + }, [totalResult]) + const textStyle = { + color: textColor, + fontSize: 16, + } + const buttonStyle = { + backgroundColor: buttonColor, + color: 'white', + padding: 20, + fontSize: 14, + cursor: 'pointer', + } + if (totalResult == ImportOperationResult.Success) { + return ( + +
Project Imported Successfully
+ +
+ ) + } + if (totalResult == ImportOperationResult.Warn) { + return ( + +
Project Imported With Warnings
+ +
+ ) + } + if (totalResult == ImportOperationResult.Error) { + return ( + +
Error Importing Project
+ +
+ ) + } + return null +} diff --git a/editor/src/core/shared/import/utopia-requirements-service.ts b/editor/src/core/shared/import/utopia-requirements-service.ts index a4075282f3cf..17e96e6f6b28 100644 --- a/editor/src/core/shared/import/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/utopia-requirements-service.ts @@ -96,9 +96,9 @@ export function updateRequirements( const aggregatedDoneStatus = getAggregatedStatus(result) if (aggregatedDoneStatus != null) { - requestIdleCallback(() => { + setTimeout(() => { notifyOperationFinished({ type: 'checkRequirements' }, aggregatedDoneStatus) - }) + }, 0) } return result From 3e3b7963ed132fe647db44bd32495ff1cc708cb4 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 14 Oct 2024 18:52:00 +0200 Subject: [PATCH 15/25] refactor enum --- editor/src/components/editor/store/editor-state.ts | 7 +------ .../src/core/shared/import/utopia-requirements-service.ts | 7 ++++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index dd6463e1edc1..522a2afb6570 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -190,6 +190,7 @@ 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 { RequirementResolutionStatus } from '../../../core/shared/import/utopia-requirements-service' import type { RequirementResolutionResult } from '../../../core/shared/import/utopia-requirements-service' const ObjectPathImmutable: any = OPI @@ -1205,12 +1206,6 @@ export function newProjectRequirements( } } -export enum RequirementResolutionStatus { - NotStarted = 'not-started', - Pending = 'pending', - Done = 'done', -} - export function emptyRequirementResolution(): RequirementResolution { return { status: RequirementResolutionStatus.NotStarted, diff --git a/editor/src/core/shared/import/utopia-requirements-service.ts b/editor/src/core/shared/import/utopia-requirements-service.ts index 17e96e6f6b28..65c207aba7bb 100644 --- a/editor/src/core/shared/import/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/utopia-requirements-service.ts @@ -3,7 +3,6 @@ import { notifyOperationFinished, notifyOperationStarted } from './import-operat import type { ProjectRequirements } from '../../../components/editor/store/editor-state' import { emptyProjectRequirements, - RequirementResolutionStatus, type ProjectRequirement, } from '../../../components/editor/store/editor-state' import type { EditorDispatch } from '../../../components/editor/action-types' @@ -127,3 +126,9 @@ export enum RequirementResolutionResult { Partial = 'partial', Critical = 'critical', } + +export enum RequirementResolutionStatus { + NotStarted = 'not-started', + Pending = 'pending', + Done = 'done', +} From 9fe1a99cc904e4bf9c5bb3b0a6768560a123d68f Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 14 Oct 2024 23:48:09 +0200 Subject: [PATCH 16/25] fix imports --- editor/src/components/editor/action-types.ts | 2 +- .../editor/actions/action-creators.ts | 2 +- .../src/components/editor/actions/actions.tsx | 4 +- .../editor/import-wizard/components.tsx | 2 +- .../components/editor/store/editor-state.ts | 64 +--------------- .../store/store-deep-equality-instances.ts | 12 ++- .../store/store-hook-substore-helpers.ts | 3 +- .../shared/github/operations/load-branch.ts | 2 +- .../shared/import/import-operation-types.ts | 2 +- .../check-utopia-requirements.ts | 7 +- .../utopia-requirements-service.ts | 29 +++---- .../utopia-requirements-types.ts | 76 +++++++++++++++++++ 12 files changed, 108 insertions(+), 97 deletions(-) rename editor/src/core/shared/import/{ => proejct-health-check}/utopia-requirements-service.ts (83%) create mode 100644 editor/src/core/shared/import/proejct-health-check/utopia-requirements-types.ts diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index afb03820a563..97d9ed588648 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -63,7 +63,6 @@ import type { ColorSwatch, PostActionMenuData, ErrorBoundaryHandling, - ProjectRequirements, } from './store/editor-state' import type { Notice } from '../common/notice' import type { LoginState } from '../../common/user' @@ -92,6 +91,7 @@ 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' diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 505c7e6d498f..0f6acf5df9d3 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -260,7 +260,6 @@ import type { ColorSwatch, PostActionMenuData, ErrorBoundaryHandling, - ProjectRequirements, } from '../store/editor-state' import type { InsertionPath } from '../store/insertion-path' import type { TextProp } from '../../text-editor/text-editor' @@ -276,6 +275,7 @@ 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 { diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 2ceac20d2cdb..12113bb39b8a 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -637,9 +637,9 @@ import { getUpdateOperationResult } from '../../../core/shared/import/import-ope import { notifyCheckingRequirement, notifyResolveRequirement, - RequirementResolutionResult, updateRequirements, -} from '../../../core/shared/import/utopia-requirements-service' +} from '../../../core/shared/import/proejct-health-check/utopia-requirements-service' +import { RequirementResolutionResult } from '../../../core/shared/import/proejct-health-check/utopia-requirements-types' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 diff --git a/editor/src/components/editor/import-wizard/components.tsx b/editor/src/components/editor/import-wizard/components.tsx index 1259c8807d0e..b21b9956e78d 100644 --- a/editor/src/components/editor/import-wizard/components.tsx +++ b/editor/src/components/editor/import-wizard/components.tsx @@ -11,7 +11,7 @@ import { ImportOperationResult } from '../../../core/shared/import/import-operat import { assertNever } from '../../../core/shared/utils' import { Icons } from '../../../uuiui' import { GithubSpinner } from '../../../components/navigator/left-pane/github-pane/github-spinner' -import { RequirementResolutionResult } from '../../../core/shared/import/utopia-requirements-service' +import { RequirementResolutionResult } from '../../../core/shared/import/proejct-health-check/utopia-requirements-types' export function OperationLine({ operation }: { operation: ImportOperation }) { const operationRunningStatus = React.useMemo(() => { diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 522a2afb6570..bce7097d5f49 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -190,8 +190,10 @@ 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 { RequirementResolutionStatus } from '../../../core/shared/import/utopia-requirements-service' -import type { RequirementResolutionResult } from '../../../core/shared/import/utopia-requirements-service' +import { + emptyProjectRequirements, + type ProjectRequirements, +} from '../../../core/shared/import/proejct-health-check/utopia-requirements-types' const ObjectPathImmutable: any = OPI @@ -1165,64 +1167,6 @@ export interface PullRequest { number: number } -export interface RequirementResolution { - status: RequirementResolutionStatus - value?: string | null - resolution?: RequirementResolutionResult | null -} - -export function requirementResolution( - status: RequirementResolutionStatus, - value?: string | null, - resolution?: RequirementResolutionResult | null, -): RequirementResolution { - return { - status, - value, - resolution, - } -} - -export interface ProjectRequirements { - storyboard: RequirementResolution - packageJsonEntries: RequirementResolution - language: RequirementResolution - reactVersion: RequirementResolution -} - -export type ProjectRequirement = keyof ProjectRequirements - -export function newProjectRequirements( - storyboard: RequirementResolution, - packageJsonEntries: RequirementResolution, - language: RequirementResolution, - reactVersion: RequirementResolution, -): ProjectRequirements { - return { - storyboard, - packageJsonEntries, - language, - reactVersion, - } -} - -export function emptyRequirementResolution(): RequirementResolution { - return { - status: RequirementResolutionStatus.NotStarted, - value: null, - resolution: null, - } -} - -export function emptyProjectRequirements(): ProjectRequirements { - return newProjectRequirements( - emptyRequirementResolution(), - emptyRequirementResolution(), - emptyRequirementResolution(), - emptyRequirementResolution(), - ) -} - export interface ProjectGithubSettings { targetRepository: GithubRepo | null originCommit: string | null 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 99ba69a1e231..4e8b1ffad952 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -362,8 +362,6 @@ import type { EditorRemixConfig, ErrorBoundaryHandling, GridControlData, - ProjectRequirements, - RequirementResolution, } from './editor-state' import { trueUpGroupElementChanged, @@ -376,8 +374,6 @@ import { newGithubData, renderedAtPropertyPath, renderedAtChildNode, - requirementResolution, - newProjectRequirements, } from './editor-state' import { editorStateNodeModules, @@ -648,6 +644,14 @@ import type { 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 type { + ProjectRequirements, + RequirementResolution, +} from '../../../core/shared/import/proejct-health-check/utopia-requirements-types' +import { + newProjectRequirements, + requirementResolution, +} from '../../../core/shared/import/proejct-health-check/utopia-requirements-types' export function ElementPropertyPathKeepDeepEquality(): KeepDeepEqualityCall { return combine2EqualityCalls( 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 cf15902bbebc..90a8cd6c7d7b 100644 --- a/editor/src/components/editor/store/store-hook-substore-helpers.ts +++ b/editor/src/components/editor/store/store-hook-substore-helpers.ts @@ -1,4 +1,5 @@ -import { emptyProjectRequirements, type EditorState } from './editor-state' +import { type EditorState } from './editor-state' +import { emptyProjectRequirements } from '../../../core/shared/import/proejct-health-check/utopia-requirements-types' export const EmptyEditorStateForKeysOnly: EditorState = { id: null, diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index 3e4086d20663..98cdc0bf57e8 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -44,7 +44,7 @@ import { notifyOperationStarted, startImportWizard, } from '../../import/import-operation-service' -import { resetRequirementsResolutions } from '../../import/utopia-requirements-service' +import { resetRequirementsResolutions } from '../../import/proejct-health-check/utopia-requirements-service' import { checkAndFixUtopiaRequirements } from '../../import/proejct-health-check/check-utopia-requirements' import { ImportOperationResult } from '../../import/import-operation-types' diff --git a/editor/src/core/shared/import/import-operation-types.ts b/editor/src/core/shared/import/import-operation-types.ts index e0e1fcb0186b..1e929e5e46c4 100644 --- a/editor/src/core/shared/import/import-operation-types.ts +++ b/editor/src/core/shared/import/import-operation-types.ts @@ -1,5 +1,5 @@ import type { GithubRepo } from '../../../components/editor/store/editor-state' -import type { RequirementResolutionResult } from './utopia-requirements-service' +import type { RequirementResolutionResult } from './proejct-health-check/utopia-requirements-types' type ImportOperationData = { text?: string diff --git a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts index 011994b81edb..9bab9f6bf121 100644 --- a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts +++ b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts @@ -4,11 +4,8 @@ import { } from '../../../../components/assets' import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' import { codeFile, isTextFile, RevisionsState } from '../../project-file-types' -import { - notifyCheckingRequirement, - notifyResolveRequirement, - RequirementResolutionResult, -} from '../utopia-requirements-service' +import { notifyCheckingRequirement, notifyResolveRequirement } from './utopia-requirements-service' +import { RequirementResolutionResult } from './utopia-requirements-types' import { applyToAllUIJSFiles } from '../../../../core/model/project-file-utils' export function checkAndFixUtopiaRequirements( diff --git a/editor/src/core/shared/import/utopia-requirements-service.ts b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts similarity index 83% rename from editor/src/core/shared/import/utopia-requirements-service.ts rename to editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts index 65c207aba7bb..70b1644255ff 100644 --- a/editor/src/core/shared/import/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts @@ -1,12 +1,13 @@ -import { importCheckRequirementAndFix, ImportOperationResult } from './import-operation-types' -import { notifyOperationFinished, notifyOperationStarted } from './import-operation-service' -import type { ProjectRequirements } from '../../../components/editor/store/editor-state' +import { importCheckRequirementAndFix, ImportOperationResult } from '../import-operation-types' +import { notifyOperationFinished, notifyOperationStarted } from '../import-operation-service' +import type { EditorDispatch } from '../../../../components/editor/action-types' +import { updateProjectRequirements } from '../../../../components/editor/actions/action-creators' +import type { ProjectRequirement, ProjectRequirements } from './utopia-requirements-types' import { emptyProjectRequirements, - type ProjectRequirement, -} from '../../../components/editor/store/editor-state' -import type { EditorDispatch } from '../../../components/editor/action-types' -import { updateProjectRequirements } from '../../../components/editor/actions/action-creators' + RequirementResolutionResult, + RequirementResolutionStatus, +} from './utopia-requirements-types' let editorDispatch: EditorDispatch | null = null @@ -119,16 +120,4 @@ function getAggregatedStatus( } return ImportOperationResult.Success } - -export enum RequirementResolutionResult { - Found = 'found', - Fixed = 'fixed', - Partial = 'partial', - Critical = 'critical', -} - -export enum RequirementResolutionStatus { - NotStarted = 'not-started', - Pending = 'pending', - Done = 'done', -} +export { RequirementResolutionResult } diff --git a/editor/src/core/shared/import/proejct-health-check/utopia-requirements-types.ts b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-types.ts new file mode 100644 index 000000000000..6af5548346f4 --- /dev/null +++ b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-types.ts @@ -0,0 +1,76 @@ +export const RequirementResolutionResult = { + Found: 'found', + Fixed: 'fixed', + Partial: 'partial', + Critical: 'critical', +} as const + +export type RequirementResolutionResult = + (typeof RequirementResolutionResult)[keyof typeof RequirementResolutionResult] + +export const RequirementResolutionStatus = { + NotStarted: 'not-started', + Pending: 'pending', + Done: 'done', +} + +export type RequirementResolutionStatus = + (typeof RequirementResolutionStatus)[keyof typeof RequirementResolutionStatus] + +export function emptyRequirementResolution(): RequirementResolution { + return { + status: RequirementResolutionStatus.NotStarted, + value: null, + resolution: null, + } +} + +export function emptyProjectRequirements(): ProjectRequirements { + return newProjectRequirements( + emptyRequirementResolution(), + emptyRequirementResolution(), + emptyRequirementResolution(), + emptyRequirementResolution(), + ) +} + +export interface RequirementResolution { + status: RequirementResolutionStatus + value?: string | null + resolution?: RequirementResolutionResult | null +} + +export function requirementResolution( + status: RequirementResolutionStatus, + value?: string | null, + resolution?: RequirementResolutionResult | null, +): RequirementResolution { + return { + status, + value, + resolution, + } +} + +export interface ProjectRequirements { + storyboard: RequirementResolution + packageJsonEntries: RequirementResolution + language: RequirementResolution + reactVersion: RequirementResolution +} + +export type ProjectRequirement = keyof ProjectRequirements + +export function newProjectRequirements( + storyboard: RequirementResolution, + packageJsonEntries: RequirementResolution, + language: RequirementResolution, + reactVersion: RequirementResolution, +): ProjectRequirements { + return { + storyboard, + packageJsonEntries, + language, + reactVersion, + } +} From cb7f0d7708d05e5cef86ef93decd828effd3f4ce Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 15 Oct 2024 14:19:04 +0200 Subject: [PATCH 17/25] add feature switch --- .../editor/import-wizard/import-wizard.tsx | 81 ++++++++++--------- .../navigator/left-pane/github-pane/index.tsx | 4 - .../shared/github/operations/load-branch.ts | 4 +- .../shared/import/import-operation-service.ts | 14 ++-- editor/src/utils/feature-switches.ts | 3 + 5 files changed, 55 insertions(+), 51 deletions(-) diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 670750a0f62f..af3638a4c740 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -8,10 +8,7 @@ import { useEditorState, Substores } from '../store/store-hook' import { when } from '../../../utils/react-conditionals' import { hideImportWizard } from '../../../core/shared/import/import-operation-service' import { OperationLine } from './components' -import { - ImportOperationResult, - type ImportOperation, -} from '../../../core/shared/import/import-operation-types' +import { ImportOperationResult } from '../../../core/shared/import/import-operation-types' import { assertNever } from '../../../core/shared/utils' export const ImportWizard = React.memo(() => { @@ -38,6 +35,22 @@ export const ImportWizard = React.memo(() => { e.stopPropagation() }, []) + const totalImportResult: ImportOperationResult | null = React.useMemo(() => { + let result = ImportOperationResult.Success + for (const operation of operations) { + if (operation.timeDone == null || operation.result == null) { + return null + } + if (operation.result == ImportOperationResult.Error) { + return ImportOperationResult.Error + } + if (operation.result == ImportOperationResult.Warn) { + result = ImportOperationResult.Warn + } + } + return result + }, [operations]) + if (projectId == null) { return null } @@ -86,16 +99,19 @@ export const ImportWizard = React.memo(() => { }} >
Project Import
- + {when( + totalImportResult != null, + , + )}
{ marginTop: 20, }} > - +
, )} @@ -133,24 +149,9 @@ export const ImportWizard = React.memo(() => { }) ImportWizard.displayName = 'ImportWizard' -function ActionButtons({ operations }: { operations: ImportOperation[] }) { - let totalResult: ImportOperationResult | null = React.useMemo(() => { - let result = ImportOperationResult.Success - for (const operation of operations) { - if (operation.timeDone == null || operation.result == null) { - return null - } - if (operation.result == ImportOperationResult.Error) { - return ImportOperationResult.Error - } - if (operation.result == ImportOperationResult.Warn) { - result = ImportOperationResult.Warn - } - } - return result - }, [operations]) +function ActionButtons({ importResult }: { importResult: ImportOperationResult | null }) { const textColor = React.useMemo(() => { - switch (totalResult) { + switch (importResult) { case ImportOperationResult.Success: return 'green' case ImportOperationResult.Warn: @@ -160,11 +161,11 @@ function ActionButtons({ operations }: { operations: ImportOperation[] }) { case null: return 'black' default: - assertNever(totalResult) + assertNever(importResult) } - }, [totalResult]) + }, [importResult]) const buttonColor = React.useMemo(() => { - switch (totalResult) { + switch (importResult) { case ImportOperationResult.Success: return 'var(--utopitheme-green)' case ImportOperationResult.Warn: @@ -174,9 +175,9 @@ function ActionButtons({ operations }: { operations: ImportOperation[] }) { case null: return 'black' default: - assertNever(totalResult) + assertNever(importResult) } - }, [totalResult]) + }, [importResult]) const textStyle = { color: textColor, fontSize: 16, @@ -188,7 +189,7 @@ function ActionButtons({ operations }: { operations: ImportOperation[] }) { fontSize: 14, cursor: 'pointer', } - if (totalResult == ImportOperationResult.Success) { + if (importResult == ImportOperationResult.Success) { return (
Project Imported Successfully
@@ -196,7 +197,7 @@ function ActionButtons({ operations }: { operations: ImportOperation[] }) {
) } - if (totalResult == ImportOperationResult.Warn) { + if (importResult == ImportOperationResult.Warn) { return (
Project Imported With Warnings
@@ -204,7 +205,7 @@ function ActionButtons({ operations }: { operations: ImportOperation[] }) {
) } - if (totalResult == ImportOperationResult.Error) { + if (importResult == ImportOperationResult.Error) { return (
Error Importing Project
diff --git a/editor/src/components/navigator/left-pane/github-pane/index.tsx b/editor/src/components/navigator/left-pane/github-pane/index.tsx index 1830e144aa72..14e09ea9f02f 100644 --- a/editor/src/components/navigator/left-pane/github-pane/index.tsx +++ b/editor/src/components/navigator/left-pane/github-pane/index.tsx @@ -55,10 +55,6 @@ import { GithubOperations } from '../../../../core/shared/github/operations' import { useOnClickAuthenticateWithGithub } from '../../../../utils/github-auth-hooks' import { setFocus } from '../../../common/actions' import { OperationContext } from '../../../../core/shared/github/operations/github-operation-context' -import { - showImportWizard, - startImportWizard, -} from '../../../../core/shared/import/import-operation-service' const compactTimeagoFormatter = (value: number, unit: string) => { return `${value}${unit.charAt(0)}` diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index 98cdc0bf57e8..aabbc6fad48b 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -42,7 +42,7 @@ import { updateProjectContentsWithParseResults } from '../../parser-projectconte import { notifyOperationFinished, notifyOperationStarted, - startImportWizard, + startImportProcess, } from '../../import/import-operation-service' import { resetRequirementsResolutions } from '../../import/proejct-health-check/utopia-requirements-service' import { checkAndFixUtopiaRequirements } from '../../import/proejct-health-check/check-utopia-requirements' @@ -123,7 +123,7 @@ export const updateProjectWithBranchContent = currentProjectContents: ProjectContentTreeRoot, initiator: GithubOperationSource, ): Promise => { - startImportWizard(dispatch) + startImportProcess(dispatch) notifyOperationStarted({ type: 'loadBranch', branchName: branchName, diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index 0536c59194da..b040366e59aa 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -1,5 +1,5 @@ import { assertNever } from '../utils' -import type { EditorDispatch } from '../../../components/editor/action-types' +import type { EditorAction, EditorDispatch } from '../../../components/editor/action-types' import { setImportWizardOpen, updateImportOperations, @@ -10,13 +10,13 @@ import type { ImportOperationResult, ImportOperationType, } from './import-operation-types' +import { isFeatureEnabled } from '../../../utils/feature-switches' let editorDispatch: EditorDispatch | null = null -export function startImportWizard(dispatch: EditorDispatch) { +export function startImportProcess(dispatch: EditorDispatch) { editorDispatch = dispatch - dispatch([ - setImportWizardOpen(true), + const actions: EditorAction[] = [ updateImportOperations( [ { type: 'loadBranch' }, @@ -26,7 +26,11 @@ export function startImportWizard(dispatch: EditorDispatch) { ], ImportOperationAction.Replace, ), - ]) + ] + if (isFeatureEnabled('Import Wizard')) { + actions.push(setImportWizardOpen(true)) + } + dispatch(actions) } export function showImportWizard() { diff --git a/editor/src/utils/feature-switches.ts b/editor/src/utils/feature-switches.ts index 4f996e50e1df..0044642a5ccf 100644 --- a/editor/src/utils/feature-switches.ts +++ b/editor/src/utils/feature-switches.ts @@ -22,6 +22,7 @@ export type FeatureName = | 'Debug - Arbitrary Code Cache' | 'Canvas Fast Selection Hack' | 'Tailwind' + | 'Import Wizard' | 'Show Debug Features' export const AllFeatureNames: FeatureName[] = [ @@ -46,6 +47,7 @@ export const AllFeatureNames: FeatureName[] = [ 'Condensed Navigator Entries', 'Canvas Fast Selection Hack', 'Tailwind', + 'Import Wizard', ] let FeatureSwitches: { [feature in FeatureName]: boolean } = { @@ -68,6 +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, 'Show Debug Features': false, } From 231b46dbffe18214d0160fed973672006586ab16 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 15 Oct 2024 15:39:28 +0200 Subject: [PATCH 18/25] fix --- editor/src/components/editor/editor-component.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/editor/src/components/editor/editor-component.tsx b/editor/src/components/editor/editor-component.tsx index d99307e37ef1..c7bcd6d1cbe1 100644 --- a/editor/src/components/editor/editor-component.tsx +++ b/editor/src/components/editor/editor-component.tsx @@ -679,8 +679,7 @@ const LockedOverlay = React.memo(() => { const editorLocked = React.useMemo(() => githubOperations.length > 0, [githubOperations]) - const refreshingDependencies = false - useEditorState( + const refreshingDependencies = useEditorState( Substores.restOfEditor, (store) => store.editor.refreshingDependencies, 'LockedOverlay refreshingDependencies', From e07ea3214d1b6079659a3601bd89c0f936dbf1ea Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 15 Oct 2024 16:04:15 +0200 Subject: [PATCH 19/25] add event handlers --- .../src/components/editor/import-wizard/import-wizard.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index af3638a4c740..bd7f756c356b 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -193,7 +193,9 @@ function ActionButtons({ importResult }: { importResult: ImportOperationResult | return (
Project Imported Successfully
- +
) } @@ -201,7 +203,9 @@ function ActionButtons({ importResult }: { importResult: ImportOperationResult | return (
Project Imported With Warnings
- +
) } From 888354be914d12bc6daaf3005282f625a2d7c632 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Wed, 16 Oct 2024 17:27:18 +0200 Subject: [PATCH 20/25] pr comments --- .../shared/import/import-operation-service.ts | 8 -------- .../check-utopia-requirements.ts | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index b040366e59aa..20b8a9ea7ba4 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -33,18 +33,10 @@ export function startImportProcess(dispatch: EditorDispatch) { dispatch(actions) } -export function showImportWizard() { - editorDispatch?.([setImportWizardOpen(true)]) -} - export function hideImportWizard() { editorDispatch?.([setImportWizardOpen(false)]) } -export function dispatchUpdateOperation(operation: ImportOperation) { - editorDispatch?.([updateImportOperations([operation], ImportOperationAction.Update)]) -} - export function notifyOperationStarted(operation: ImportOperation) { const operationWithTime = { ...operation, diff --git a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts index 9bab9f6bf121..2855d753b2b6 100644 --- a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts +++ b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts @@ -12,10 +12,10 @@ export function checkAndFixUtopiaRequirements( parsedProjectContents: ProjectContentTreeRoot, ): ProjectContentTreeRoot { let projectContents = parsedProjectContents - // check package.json + // check and fix package.json projectContents = checkAndFixPackageJson(projectContents) // check language - projectContents = checkProjectLanguage(projectContents) + checkProjectLanguage(projectContents) // check react version checkReactVersion(projectContents) return projectContents @@ -73,7 +73,7 @@ function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): Projec return projectContents } -function checkProjectLanguage(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { +function checkProjectLanguage(projectContents: ProjectContentTreeRoot): void { notifyCheckingRequirement('language', 'Checking project language') let jsCount = 0 let tsCount = 0 @@ -85,13 +85,21 @@ function checkProjectLanguage(projectContents: ProjectContentTreeRoot): ProjectC } return uiJSFile }) - if (tsCount > jsCount) { + if (tsCount > 0) { notifyResolveRequirement( 'language', RequirementResolutionResult.Critical, 'Majority of project files are in TS/TSX', 'typescript', ) + } else if (jsCount == 0) { + // in case it's a .coffee project, python, etc + notifyResolveRequirement( + 'language', + RequirementResolutionResult.Critical, + 'No JS/JSX files found', + 'javascript', + ) } else { notifyResolveRequirement( 'language', @@ -100,7 +108,6 @@ function checkProjectLanguage(projectContents: ProjectContentTreeRoot): ProjectC 'javascript', ) } - return projectContents } function checkReactVersion(projectContents: ProjectContentTreeRoot): void { From 378deb9e0f4e519835dab3885fd543a31fd01aed Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sat, 19 Oct 2024 22:39:44 -0400 Subject: [PATCH 21/25] refactor (cr) --- .../package-manager/fetch-packages.ts | 127 ++++++++---------- 1 file changed, 58 insertions(+), 69 deletions(-) diff --git a/editor/src/core/es-modules/package-manager/fetch-packages.ts b/editor/src/core/es-modules/package-manager/fetch-packages.ts index ac77c317ea69..70f84f7f42c8 100644 --- a/editor/src/core/es-modules/package-manager/fetch-packages.ts +++ b/editor/src/core/es-modules/package-manager/fetch-packages.ts @@ -281,6 +281,49 @@ function failError(dependency: RequestedNpmDependency): DependencyFetchError { } } +export async function fetchNodeModule( + dep: RequestedNpmDependency, + shouldRetry: boolean = true, +): Promise> { + try { + const matchingVersionResponse = await findMatchingVersion(dep.name, dep.version, 'skipFetch') + if (isPackageNotFound(matchingVersionResponse)) { + return left(failNotFound(dep)) + } + + const fetchResolvedDependency = shouldRetry + ? fetchPackagerResponseWithRetry + : fetchPackagerResponse + + const packagerResponse = await fetchResolvedDependency(dep, matchingVersionResponse.version) + + if (packagerResponse != null) { + /** + * to avoid clashing transitive dependencies, + * we "move" all transitive dependencies into a subfolder at + * /node_modules//node_modules// + * + * the module resolution won't mind this, the only downside to this approach is + * that if two main dependencies share the exact same version of a transitive + * dependency, they will not share that transitive dependency in memory, + * so this is wasting a bit of memory. + * + * but it avoids two of the same transitive dependencies with different versions from + * overwriting each other. + * + * the real nice solution would be to apply npm's module resolution logic that + * pulls up shared transitive dependencies to the main /node_modules/ folder. + */ + return right(mangleNodeModulePaths(dep.name, packagerResponse)) + } else { + return left(failError(dep)) + } + } catch (e) { + // TODO: proper error handling, now we don't show error for a missing package. The error will be visible when you try to import + return left(failError(dep)) + } +} + export async function fetchNodeModules( newDeps: Array, builtInDependencies: BuiltInDependencies, @@ -290,75 +333,21 @@ export async function fetchNodeModules( (d) => !isBuiltInDependency(builtInDependencies, d.name), ) const nodeModulesArr = await Promise.all( - dependenciesToDownload.map( - async (newDep): Promise> => { - function notifyFetchEnd(result: ImportOperationResult) { - notifyOperationFinished( - { - type: 'fetchDependency', - id: `${newDep.name}@${newDep.version}`, - dependencyName: newDep.name, - dependencyVersion: newDep.version, - }, - result, - ) - } - try { - notifyOperationStarted({ - type: 'fetchDependency', - id: `${newDep.name}@${newDep.version}`, - dependencyName: newDep.name, - dependencyVersion: newDep.version, - }) - const matchingVersionResponse = await findMatchingVersion( - newDep.name, - newDep.version, - 'skipFetch', - ) - if (isPackageNotFound(matchingVersionResponse)) { - notifyFetchEnd(ImportOperationResult.Error) - return left(failNotFound(newDep)) - } - - const fetchResolvedDependency = shouldRetry - ? fetchPackagerResponseWithRetry - : fetchPackagerResponse - - const packagerResponse = await fetchResolvedDependency( - newDep, - matchingVersionResponse.version, - ) - - if (packagerResponse != null) { - /** - * to avoid clashing transitive dependencies, - * we "move" all transitive dependencies into a subfolder at - * /node_modules//node_modules// - * - * the module resolution won't mind this, the only downside to this approach is - * that if two main dependencies share the exact same version of a transitive - * dependency, they will not share that transitive dependency in memory, - * so this is wasting a bit of memory. - * - * but it avoids two of the same transitive dependencies with different versions from - * overwriting each other. - * - * the real nice solution would be to apply npm's module resolution logic that - * pulls up shared transitive dependencies to the main /node_modules/ folder. - */ - notifyFetchEnd(ImportOperationResult.Success) - return right(mangleNodeModulePaths(newDep.name, packagerResponse)) - } else { - notifyFetchEnd(ImportOperationResult.Error) - return left(failError(newDep)) - } - } catch (e) { - // TODO: proper error handling, now we don't show error for a missing package. The error will be visible when you try to import - notifyFetchEnd(ImportOperationResult.Error) - return left(failError(newDep)) - } - }, - ), + dependenciesToDownload.map(async (dep) => { + const fetchDependencyOperation = { + type: 'fetchDependency', + id: `${dep.name}@${dep.version}`, + dependencyName: dep.name, + dependencyVersion: dep.version, + } as const + notifyOperationStarted(fetchDependencyOperation) + const fetchResult = await fetchNodeModule(dep, shouldRetry) + const fetchStatus = isLeft(fetchResult) + ? ImportOperationResult.Error + : ImportOperationResult.Success + notifyOperationFinished(fetchDependencyOperation, fetchStatus) + return fetchResult + }), ) const errors = nodeModulesArr .filter(isLeft) From 36cadbab90aef63b1b1c788ee73b3584fb24a8f8 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sat, 19 Oct 2024 22:55:16 -0400 Subject: [PATCH 22/25] refactor: pass dispatch down instead of a global state --- .../editor/actions/actions.spec.tsx | 2 + .../src/components/editor/actions/actions.tsx | 27 +++++++++++--- .../editor/import-wizard/import-wizard.tsx | 15 ++++++-- .../src/components/editor/store/dispatch.tsx | 2 +- .../components/editor/store/editor-update.tsx | 4 +- .../components/navigator/dependency-list.tsx | 35 ++++++++++-------- .../package-manager/fetch-packages.ts | 6 ++- .../package-manager/package-manager.spec.ts | 8 ++++ editor/src/core/shared/dependencies.ts | 7 +++- .../shared/github/operations/load-branch.ts | 12 +++--- .../shared/import/import-operation-service.ts | 19 +++++----- .../check-utopia-requirements.ts | 37 ++++++++++++++----- .../utopia-requirements-service.ts | 24 +++++++----- 13 files changed, 134 insertions(+), 64 deletions(-) diff --git a/editor/src/components/editor/actions/actions.spec.tsx b/editor/src/components/editor/actions/actions.spec.tsx index d30e9ff2d7c2..037c806da975 100644 --- a/editor/src/components/editor/actions/actions.spec.tsx +++ b/editor/src/components/editor/actions/actions.spec.tsx @@ -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. @@ -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. diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 12113bb39b8a..f818fc1e1f79 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -1643,13 +1643,19 @@ function createStoryboardFileWithPlaceholderContents( } export function createStoryboardFileIfNecessary( + dispatch: EditorDispatch, projectContents: ProjectContentTreeRoot, createPlaceholder: 'create-placeholder' | 'skip-creating-placeholder', ): ProjectContentTreeRoot { - notifyCheckingRequirement('storyboard', 'Checking for storyboard.js') + notifyCheckingRequirement(dispatch, 'storyboard', 'Checking for storyboard.js') const storyboardFile = getProjectFileByFilePath(projectContents, StoryboardFilePath) if (storyboardFile != null) { - notifyResolveRequirement('storyboard', RequirementResolutionResult.Found, 'Storyboard.js found') + notifyResolveRequirement( + dispatch, + 'storyboard', + RequirementResolutionResult.Found, + 'Storyboard.js found', + ) return projectContents } @@ -1660,12 +1666,14 @@ export function createStoryboardFileIfNecessary( if (result == projectContents) { notifyResolveRequirement( + dispatch, 'storyboard', RequirementResolutionResult.Partial, 'Storyboard.js skipped', ) } else { notifyResolveRequirement( + dispatch, 'storyboard', RequirementResolutionResult.Fixed, 'Storyboard.js created', @@ -2276,8 +2284,9 @@ export const UPDATE_FNS = { UPDATE_PROJECT_REQUIREMENTS: ( action: UpdateProjectRequirements, editor: EditorModel, + dispatch: EditorDispatch, ): EditorModel => { - const result = updateRequirements(editor.projectRequirements, action.requirements) + const result = updateRequirements(dispatch, editor.projectRequirements, action.requirements) return { ...editor, projectRequirements: result, @@ -4007,6 +4016,7 @@ export const UPDATE_FNS = { action: UpdateFromWorker, editor: EditorModel, userState: UserState, + dispatch: EditorDispatch, ): EditorModel => { let workingProjectContents: ProjectContentTreeRoot = editor.projectContents let anyParsedUpdates: boolean = false @@ -4043,6 +4053,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 @@ -4868,7 +4879,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, @@ -5066,6 +5081,7 @@ export const UPDATE_FNS = { updateFromWorker(workerUpdates), withFileChanges.unpatchedEditor, withFileChanges.userState, + dispatch, ) return { ...withFileChanges, @@ -5680,7 +5696,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, @@ -6390,6 +6406,7 @@ export async function load( const migratedModel = applyMigrations(model) const npmDependencies = dependenciesWithEditorRequirements(migratedModel.projectContents) const fetchNodeModulesResult = await fetchNodeModules( + dispatch, npmDependencies, builtInDependencies, retryFetchNodeModules, diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index bd7f756c356b..860d03755fe2 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -10,6 +10,7 @@ import { hideImportWizard } from '../../../core/shared/import/import-operation-s import { OperationLine } from './components' import { ImportOperationResult } from '../../../core/shared/import/import-operation-types' import { assertNever } from '../../../core/shared/utils' +import { useDispatch } from '../store/dispatch-context' export const ImportWizard = React.memo(() => { const colorTheme = useColorTheme() @@ -27,9 +28,11 @@ export const ImportWizard = React.memo(() => { 'ImportWizard operations', ) + const dispatch = useDispatch() + const handleDismiss = React.useCallback(() => { - hideImportWizard() - }, []) + hideImportWizard(dispatch) + }, [dispatch]) const stopPropagation = React.useCallback((e: React.MouseEvent) => { e.stopPropagation() @@ -189,11 +192,15 @@ function ActionButtons({ importResult }: { importResult: ImportOperationResult | fontSize: 14, cursor: 'pointer', } + const dispatch = useDispatch() + const hideWizard = React.useCallback(() => { + hideImportWizard(dispatch) + }, [dispatch]) if (importResult == ImportOperationResult.Success) { return (
Project Imported Successfully
-
@@ -203,7 +210,7 @@ function ActionButtons({ importResult }: { importResult: ImportOperationResult | return (
Project Imported With Warnings
-
diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index e8a522d2d7f1..cb15a76158e4 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -190,7 +190,7 @@ function processAction( } if (action.action === 'UPDATE_TEXT') { - working = UPDATE_FNS.UPDATE_TEXT(action, working) + working = UPDATE_FNS.UPDATE_TEXT(action, working, dispatchEvent) } if (action.action === 'TRUNCATE_HISTORY') { diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index d2400561b29a..2c7ded3d89bc 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -225,7 +225,7 @@ export function runSimpleLocalEditorAction( case 'SET_IMPORT_WIZARD_OPEN': return UPDATE_FNS.SET_IMPORT_WIZARD_OPEN(action, state) case 'UPDATE_PROJECT_REQUIREMENTS': - return UPDATE_FNS.UPDATE_PROJECT_REQUIREMENTS(action, state) + return UPDATE_FNS.UPDATE_PROJECT_REQUIREMENTS(action, state, dispatch) case 'REMOVE_TOAST': return UPDATE_FNS.REMOVE_TOAST(action, state) case 'SET_HIGHLIGHTED_VIEWS': @@ -293,7 +293,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, userState) + return UPDATE_FNS.UPDATE_FROM_WORKER(action, state, userState, dispatch) case 'UPDATE_FROM_CODE_EDITOR': return UPDATE_FNS.UPDATE_FROM_CODE_EDITOR( action, diff --git a/editor/src/components/navigator/dependency-list.tsx b/editor/src/components/navigator/dependency-list.tsx index b17107b3c26a..0460ec7a355c 100644 --- a/editor/src/components/navigator/dependency-list.tsx +++ b/editor/src/components/navigator/dependency-list.tsx @@ -194,22 +194,24 @@ class DependencyListInner extends React.PureComponent { - if (fetchNodeModulesResult.dependenciesWithError.length > 0) { - this.packagesUpdateFailed( - `Failed to download the following dependencies: ${JSON.stringify( - fetchNodeModulesResult.dependenciesWithError.map((d) => d.name), - )}`, - fetchNodeModulesResult.dependenciesWithError[0]?.name, - ) - } - this.setState({ dependencyLoadingStatus: 'not-loading' }) - this.props.editorDispatch([ - EditorActions.updateNodeModulesContents(fetchNodeModulesResult.nodeModules), - ]) - }, - ) + void fetchNodeModules( + this.props.editorDispatch, + npmDependencies, + this.props.builtInDependencies, + ).then((fetchNodeModulesResult) => { + if (fetchNodeModulesResult.dependenciesWithError.length > 0) { + this.packagesUpdateFailed( + `Failed to download the following dependencies: ${JSON.stringify( + fetchNodeModulesResult.dependenciesWithError.map((d) => d.name), + )}`, + fetchNodeModulesResult.dependenciesWithError[0]?.name, + ) + } + this.setState({ dependencyLoadingStatus: 'not-loading' }) + this.props.editorDispatch([ + EditorActions.updateNodeModulesContents(fetchNodeModulesResult.nodeModules), + ]) + }) this.setState({ dependencyLoadingStatus: 'removing' }) } @@ -358,6 +360,7 @@ class DependencyListInner extends React.PureComponent, builtInDependencies: BuiltInDependencies, shouldRetry: boolean = true, @@ -340,12 +342,12 @@ export async function fetchNodeModules( dependencyName: dep.name, dependencyVersion: dep.version, } as const - notifyOperationStarted(fetchDependencyOperation) + notifyOperationStarted(dispatch, fetchDependencyOperation) const fetchResult = await fetchNodeModule(dep, shouldRetry) const fetchStatus = isLeft(fetchResult) ? ImportOperationResult.Error : ImportOperationResult.Success - notifyOperationFinished(fetchDependencyOperation, fetchStatus) + notifyOperationFinished(dispatch, fetchDependencyOperation, fetchStatus) return fetchResult }), ) diff --git a/editor/src/core/es-modules/package-manager/package-manager.spec.ts b/editor/src/core/es-modules/package-manager/package-manager.spec.ts index a587ebe1619d..b8a1b9ccfb4c 100644 --- a/editor/src/core/es-modules/package-manager/package-manager.spec.ts +++ b/editor/src/core/es-modules/package-manager/package-manager.spec.ts @@ -174,6 +174,7 @@ describe('ES Dependency Manager — Real-life packages', () => { }, ) const fetchNodeModulesResult = await fetchNodeModules( + NO_OP, [requestedNpmDependency('react-spring', '8.0.27')], createBuiltInDependenciesList(null), ) @@ -209,6 +210,7 @@ describe('ES Dependency Manager — Real-life packages', () => { }, ) void fetchNodeModules( + NO_OP, [requestedNpmDependency('antd', '4.2.5')], createBuiltInDependenciesList(null), ).then((fetchNodeModulesResult) => { @@ -280,6 +282,7 @@ describe('ES Dependency Manager', () => { }, ) const fetchNodeModulesResult = await fetchNodeModules( + NO_OP, [requestedNpmDependency('broken', '1.0.0')], createBuiltInDependenciesList(null), ) @@ -315,6 +318,7 @@ describe('ES Dependency Manager — d.ts', () => { ) const fetchNodeModulesResult = await fetchNodeModules( + NO_OP, [requestedNpmDependency('react-spring', '8.0.27')], createBuiltInDependenciesList(null), ) @@ -350,6 +354,7 @@ describe('ES Dependency Manager — Downloads extra files as-needed', () => { }, ) void fetchNodeModules( + NO_OP, [requestedNpmDependency('mypackage', '0.0.1')], createBuiltInDependenciesList(null), ).then((fetchNodeModulesResult) => { @@ -416,6 +421,7 @@ describe('ES Dependency manager - retry behavior', () => { ) void fetchNodeModules( + NO_OP, [requestedNpmDependency('react-spring', '8.0.27')], createBuiltInDependenciesList(null), ).then((fetchNodeModulesResult) => { @@ -438,6 +444,7 @@ describe('ES Dependency manager - retry behavior', () => { ) void fetchNodeModules( + NO_OP, [requestedNpmDependency('react-spring', '8.0.27')], createBuiltInDependenciesList(null), ).then((fetchNodeModulesResult) => { @@ -466,6 +473,7 @@ describe('ES Dependency manager - retry behavior', () => { ) void fetchNodeModules( + NO_OP, [requestedNpmDependency('react-spring', '8.0.27')], createBuiltInDependenciesList(null), false, diff --git a/editor/src/core/shared/dependencies.ts b/editor/src/core/shared/dependencies.ts index 24ea88c9ad46..ff6c48ed11a2 100644 --- a/editor/src/core/shared/dependencies.ts +++ b/editor/src/core/shared/dependencies.ts @@ -82,7 +82,11 @@ export async function refreshDependencies( const depsToFetch = newDeps.concat(updatedDeps) - const fetchNodeModulesResult = await fetchNodeModules(depsToFetch, builtInDependencies) + const fetchNodeModulesResult = await fetchNodeModules( + dispatch, + depsToFetch, + builtInDependencies, + ) const loadedPackagesStatus = createLoadedPackageStatusMapFromDependencies( deps, @@ -98,6 +102,7 @@ export async function refreshDependencies( ]) notifyOperationFinished( + dispatch, { type: 'refreshDependencies' }, getDependenciesStatus(loadedPackagesStatus), ) diff --git a/editor/src/core/shared/github/operations/load-branch.ts b/editor/src/core/shared/github/operations/load-branch.ts index aabbc6fad48b..36a63a7d74fc 100644 --- a/editor/src/core/shared/github/operations/load-branch.ts +++ b/editor/src/core/shared/github/operations/load-branch.ts @@ -124,7 +124,7 @@ export const updateProjectWithBranchContent = initiator: GithubOperationSource, ): Promise => { startImportProcess(dispatch) - notifyOperationStarted({ + notifyOperationStarted(dispatch, { type: 'loadBranch', branchName: branchName, githubRepo: githubRepo, @@ -161,24 +161,26 @@ export const updateProjectWithBranchContent = if (resetBranches) { newGithubData.branches = null } - notifyOperationFinished({ type: 'loadBranch' }, ImportOperationResult.Success) + notifyOperationFinished(dispatch, { type: 'loadBranch' }, ImportOperationResult.Success) - notifyOperationStarted({ type: 'parseFiles' }) + notifyOperationStarted(dispatch, { type: 'parseFiles' }) // Push any code through the parser so that the representations we end up with are in a state of `BOTH_MATCH`. // So that it will override any existing files that might already exist in the project when sending them to VS Code. const parseResults = await updateProjectContentsWithParseResults( workers, responseBody.branch.content, ) - notifyOperationFinished({ type: 'parseFiles' }, ImportOperationResult.Success) + notifyOperationFinished(dispatch, { type: 'parseFiles' }, ImportOperationResult.Success) resetRequirementsResolutions(dispatch) const parsedProjectContentsInitial = createStoryboardFileIfNecessary( + dispatch, parseResults, 'create-placeholder', ) const parsedProjectContents = checkAndFixUtopiaRequirements( + dispatch, parsedProjectContentsInitial, ) @@ -207,7 +209,7 @@ export const updateProjectWithBranchContent = let dependenciesPromise: Promise = Promise.resolve() const packageJson = packageJsonFileFromProjectContents(parsedProjectContents) if (packageJson != null && isTextFile(packageJson)) { - notifyOperationStarted({ type: 'refreshDependencies' }) + notifyOperationStarted(dispatch, { type: 'refreshDependencies' }) dependenciesPromise = refreshDependencies( dispatch, packageJson.fileContents.code, diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index 20b8a9ea7ba4..6b0c83f85e26 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -12,10 +12,7 @@ import type { } from './import-operation-types' import { isFeatureEnabled } from '../../../utils/feature-switches' -let editorDispatch: EditorDispatch | null = null - export function startImportProcess(dispatch: EditorDispatch) { - editorDispatch = dispatch const actions: EditorAction[] = [ updateImportOperations( [ @@ -33,27 +30,31 @@ export function startImportProcess(dispatch: EditorDispatch) { dispatch(actions) } -export function hideImportWizard() { - editorDispatch?.([setImportWizardOpen(false)]) +export function hideImportWizard(dispatch: EditorDispatch) { + dispatch([setImportWizardOpen(false)]) } -export function notifyOperationStarted(operation: ImportOperation) { +export function notifyOperationStarted(dispatch: EditorDispatch, operation: ImportOperation) { const operationWithTime = { ...operation, timeStarted: Date.now(), timeDone: null, } - editorDispatch?.([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) + dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) } -export function notifyOperationFinished(operation: ImportOperation, result: ImportOperationResult) { +export function notifyOperationFinished( + dispatch: EditorDispatch, + operation: ImportOperation, + result: ImportOperationResult, +) { const timeDone = Date.now() const operationWithTime = { ...operation, timeDone: timeDone, result: result, } - editorDispatch?.([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) + dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) } export function areSameOperation(existing: ImportOperation, incoming: ImportOperation): boolean { diff --git a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts index 2855d753b2b6..b09810c2dc58 100644 --- a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts +++ b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts @@ -7,17 +7,19 @@ import { codeFile, isTextFile, RevisionsState } from '../../project-file-types' import { notifyCheckingRequirement, notifyResolveRequirement } from './utopia-requirements-service' import { RequirementResolutionResult } from './utopia-requirements-types' import { applyToAllUIJSFiles } from '../../../../core/model/project-file-utils' +import type { EditorDispatch } from '../../../../components/editor/action-types' export function checkAndFixUtopiaRequirements( + dispatch: EditorDispatch, parsedProjectContents: ProjectContentTreeRoot, ): ProjectContentTreeRoot { let projectContents = parsedProjectContents // check and fix package.json - projectContents = checkAndFixPackageJson(projectContents) + projectContents = checkAndFixPackageJson(dispatch, projectContents) // check language - checkProjectLanguage(projectContents) + checkProjectLanguage(dispatch, projectContents) // check react version - checkReactVersion(projectContents) + checkReactVersion(dispatch, projectContents) return projectContents } @@ -31,11 +33,15 @@ function getPackageJson( return null } -function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): ProjectContentTreeRoot { - notifyCheckingRequirement('packageJsonEntries', 'Checking package.json') +function checkAndFixPackageJson( + dispatch: EditorDispatch, + projectContents: ProjectContentTreeRoot, +): ProjectContentTreeRoot { + notifyCheckingRequirement(dispatch, 'packageJsonEntries', 'Checking package.json') const parsedPackageJson = getPackageJson(projectContents) if (parsedPackageJson == null) { notifyResolveRequirement( + dispatch, 'packageJsonEntries', RequirementResolutionResult.Critical, 'The file package.json was not found', @@ -57,6 +63,7 @@ function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): Projec ), ) notifyResolveRequirement( + dispatch, 'packageJsonEntries', RequirementResolutionResult.Fixed, 'Fixed utopia entry in package.json', @@ -64,6 +71,7 @@ function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): Projec return result } else { notifyResolveRequirement( + dispatch, 'packageJsonEntries', RequirementResolutionResult.Found, 'Valid package.json found', @@ -73,8 +81,11 @@ function checkAndFixPackageJson(projectContents: ProjectContentTreeRoot): Projec return projectContents } -function checkProjectLanguage(projectContents: ProjectContentTreeRoot): void { - notifyCheckingRequirement('language', 'Checking project language') +function checkProjectLanguage( + dispatch: EditorDispatch, + projectContents: ProjectContentTreeRoot, +): void { + notifyCheckingRequirement(dispatch, 'language', 'Checking project language') let jsCount = 0 let tsCount = 0 applyToAllUIJSFiles(projectContents, (filename, uiJSFile) => { @@ -87,6 +98,7 @@ function checkProjectLanguage(projectContents: ProjectContentTreeRoot): void { }) if (tsCount > 0) { notifyResolveRequirement( + dispatch, 'language', RequirementResolutionResult.Critical, 'Majority of project files are in TS/TSX', @@ -95,6 +107,7 @@ function checkProjectLanguage(projectContents: ProjectContentTreeRoot): void { } else if (jsCount == 0) { // in case it's a .coffee project, python, etc notifyResolveRequirement( + dispatch, 'language', RequirementResolutionResult.Critical, 'No JS/JSX files found', @@ -102,6 +115,7 @@ function checkProjectLanguage(projectContents: ProjectContentTreeRoot): void { ) } else { notifyResolveRequirement( + dispatch, 'language', RequirementResolutionResult.Found, 'Project uses JS/JSX', @@ -110,8 +124,11 @@ function checkProjectLanguage(projectContents: ProjectContentTreeRoot): void { } } -function checkReactVersion(projectContents: ProjectContentTreeRoot): void { - notifyCheckingRequirement('reactVersion', 'Checking React version') +function checkReactVersion( + dispatch: EditorDispatch, + projectContents: ProjectContentTreeRoot, +): void { + notifyCheckingRequirement(dispatch, 'reactVersion', 'Checking React version') const parsedPackageJson = getPackageJson(projectContents) if ( parsedPackageJson == null || @@ -119,6 +136,7 @@ function checkReactVersion(projectContents: ProjectContentTreeRoot): void { parsedPackageJson.dependencies.react == null ) { return notifyResolveRequirement( + dispatch, 'reactVersion', RequirementResolutionResult.Critical, 'React is not in dependencies', @@ -127,6 +145,7 @@ function checkReactVersion(projectContents: ProjectContentTreeRoot): void { const reactVersion = parsedPackageJson.dependencies.react // TODO: check react version return notifyResolveRequirement( + dispatch, 'reactVersion', RequirementResolutionResult.Found, 'React version is ok', diff --git a/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts index 70b1644255ff..19707222e245 100644 --- a/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts @@ -9,8 +9,6 @@ import { RequirementResolutionStatus, } from './utopia-requirements-types' -let editorDispatch: EditorDispatch | null = null - const initialTexts: Record = { storyboard: 'Checking storyboard.js', packageJsonEntries: 'Checking package.json', @@ -19,10 +17,9 @@ const initialTexts: Record = { } export function resetRequirementsResolutions(dispatch: EditorDispatch) { - editorDispatch = dispatch let projectRequirements = emptyProjectRequirements() - editorDispatch?.([updateProjectRequirements(projectRequirements)]) - notifyOperationStarted({ + dispatch([updateProjectRequirements(projectRequirements)]) + notifyOperationStarted(dispatch, { type: 'checkRequirements', children: Object.keys(projectRequirements).map((key) => importCheckRequirementAndFix( @@ -33,15 +30,19 @@ export function resetRequirementsResolutions(dispatch: EditorDispatch) { }) } -export function notifyCheckingRequirement(requirement: ProjectRequirement, text: string) { - editorDispatch?.([ +export function notifyCheckingRequirement( + dispatch: EditorDispatch, + requirement: ProjectRequirement, + text: string, +) { + dispatch([ updateProjectRequirements({ [requirement]: { status: RequirementResolutionStatus.Pending, }, }), ]) - notifyOperationStarted({ + notifyOperationStarted(dispatch, { type: 'checkRequirementAndFix', id: requirement, text: text, @@ -49,12 +50,13 @@ export function notifyCheckingRequirement(requirement: ProjectRequirement, text: } export function notifyResolveRequirement( + dispatch: EditorDispatch, requirementName: ProjectRequirement, resolution: RequirementResolutionResult, text: string, value?: string, ) { - editorDispatch?.([ + dispatch([ updateProjectRequirements({ [requirementName]: { status: RequirementResolutionStatus.Done, @@ -71,6 +73,7 @@ export function notifyResolveRequirement( ? ImportOperationResult.Warn : ImportOperationResult.Error notifyOperationFinished( + dispatch, { type: 'checkRequirementAndFix', id: requirementName, @@ -82,6 +85,7 @@ export function notifyResolveRequirement( } export function updateRequirements( + dispatch: EditorDispatch, existingRequirements: ProjectRequirements, incomingRequirements: Partial, ): ProjectRequirements { @@ -97,7 +101,7 @@ export function updateRequirements( const aggregatedDoneStatus = getAggregatedStatus(result) if (aggregatedDoneStatus != null) { setTimeout(() => { - notifyOperationFinished({ type: 'checkRequirements' }, aggregatedDoneStatus) + notifyOperationFinished(dispatch, { type: 'checkRequirements' }, aggregatedDoneStatus) }, 0) } From a283b4fcb6375ee4b48801025ce7c210b3deaf57 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sat, 19 Oct 2024 23:04:27 -0400 Subject: [PATCH 23/25] remove enums --- .../editor/import-wizard/import-wizard.tsx | 6 +++- .../shared/import/import-operation-types.ts | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/editor/src/components/editor/import-wizard/import-wizard.tsx b/editor/src/components/editor/import-wizard/import-wizard.tsx index 860d03755fe2..ca6282300b4d 100644 --- a/editor/src/components/editor/import-wizard/import-wizard.tsx +++ b/editor/src/components/editor/import-wizard/import-wizard.tsx @@ -39,14 +39,18 @@ export const ImportWizard = React.memo(() => { }, []) const totalImportResult: ImportOperationResult | null = React.useMemo(() => { - let result = ImportOperationResult.Success + 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 } diff --git a/editor/src/core/shared/import/import-operation-types.ts b/editor/src/core/shared/import/import-operation-types.ts index 1e929e5e46c4..4a8fccb83539 100644 --- a/editor/src/core/shared/import/import-operation-types.ts +++ b/editor/src/core/shared/import/import-operation-types.ts @@ -11,11 +11,14 @@ type ImportOperationData = { children?: ImportOperation[] } -export enum ImportOperationResult { - Success = 'success', - Error = 'error', - Warn = 'warn', -} +export const ImportOperationResult = { + Success: 'success', + Error: 'error', + Warn: 'warn', +} as const + +export type ImportOperationResult = + (typeof ImportOperationResult)[keyof typeof ImportOperationResult] type ImportLoadBranch = { type: 'loadBranch' @@ -70,9 +73,12 @@ export type ImportOperation = export type ImportOperationType = ImportOperation['type'] -export enum ImportOperationAction { - Add = 'add', - Remove = 'remove', - Update = 'update', - Replace = 'replace', -} +export const ImportOperationAction = { + Add: 'add', + Remove: 'remove', + Update: 'update', + Replace: 'replace', +} as const + +export type ImportOperationAction = + (typeof ImportOperationAction)[keyof typeof ImportOperationAction] From 20f4dc5d6ed11e4a93c5b7a5f543511f3d65288c Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sat, 19 Oct 2024 23:54:51 -0400 Subject: [PATCH 24/25] dispatch notifications asynchronously --- .../shared/import/import-operation-service.ts | 8 +++- .../check-utopia-requirements.ts | 4 +- .../utopia-requirements-service.ts | 43 ++++++++++--------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index 6b0c83f85e26..c814f70ecdf9 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -40,7 +40,9 @@ export function notifyOperationStarted(dispatch: EditorDispatch, operation: Impo timeStarted: Date.now(), timeDone: null, } - dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) + setTimeout(() => { + dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) + }, 0) } export function notifyOperationFinished( @@ -54,7 +56,9 @@ export function notifyOperationFinished( timeDone: timeDone, result: result, } - dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) + setTimeout(() => { + dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) + }, 0) } export function areSameOperation(existing: ImportOperation, incoming: ImportOperation): boolean { diff --git a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts index b09810c2dc58..ec78bdcb05e6 100644 --- a/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts +++ b/editor/src/core/shared/import/proejct-health-check/check-utopia-requirements.ts @@ -89,7 +89,7 @@ function checkProjectLanguage( let jsCount = 0 let tsCount = 0 applyToAllUIJSFiles(projectContents, (filename, uiJSFile) => { - if (filename.endsWith('.ts') || filename.endsWith('.tsx')) { + if ((filename.endsWith('.ts') || filename.endsWith('.tsx')) && !filename.endsWith('.d.ts')) { tsCount++ } else if (filename.endsWith('.js') || filename.endsWith('.jsx')) { jsCount++ @@ -101,7 +101,7 @@ function checkProjectLanguage( dispatch, 'language', RequirementResolutionResult.Critical, - 'Majority of project files are in TS/TSX', + 'There are Typescript files in the project', 'typescript', ) } else if (jsCount == 0) { diff --git a/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts index 19707222e245..7d9625922346 100644 --- a/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts @@ -16,9 +16,18 @@ const initialTexts: Record = { reactVersion: 'Checking React version', } +export function updateProjectRequirementsStatus( + dispatch: EditorDispatch, + projectRequirements: Partial, +) { + setTimeout(() => { + dispatch([updateProjectRequirements(projectRequirements)]) + }, 0) +} + export function resetRequirementsResolutions(dispatch: EditorDispatch) { let projectRequirements = emptyProjectRequirements() - dispatch([updateProjectRequirements(projectRequirements)]) + updateProjectRequirementsStatus(dispatch, projectRequirements) notifyOperationStarted(dispatch, { type: 'checkRequirements', children: Object.keys(projectRequirements).map((key) => @@ -35,13 +44,11 @@ export function notifyCheckingRequirement( requirement: ProjectRequirement, text: string, ) { - dispatch([ - updateProjectRequirements({ - [requirement]: { - status: RequirementResolutionStatus.Pending, - }, - }), - ]) + updateProjectRequirementsStatus(dispatch, { + [requirement]: { + status: RequirementResolutionStatus.Pending, + }, + }) notifyOperationStarted(dispatch, { type: 'checkRequirementAndFix', id: requirement, @@ -56,15 +63,13 @@ export function notifyResolveRequirement( text: string, value?: string, ) { - dispatch([ - updateProjectRequirements({ - [requirementName]: { - status: RequirementResolutionStatus.Done, - resolution: resolution, - value: value, - }, - }), - ]) + updateProjectRequirementsStatus(dispatch, { + [requirementName]: { + status: RequirementResolutionStatus.Done, + resolution: resolution, + value: value, + }, + }) const result = resolution === RequirementResolutionResult.Found || resolution === RequirementResolutionResult.Fixed @@ -100,9 +105,7 @@ export function updateRequirements( const aggregatedDoneStatus = getAggregatedStatus(result) if (aggregatedDoneStatus != null) { - setTimeout(() => { - notifyOperationFinished(dispatch, { type: 'checkRequirements' }, aggregatedDoneStatus) - }, 0) + notifyOperationFinished(dispatch, { type: 'checkRequirements' }, aggregatedDoneStatus) } return result From 8fb5834786a8932baa7ff2e582e689659619cfd1 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 28 Oct 2024 00:09:23 -0400 Subject: [PATCH 25/25] dont fire actions if flag is turned off --- editor/src/core/shared/import/import-operation-service.ts | 6 ++++++ .../proejct-health-check/utopia-requirements-service.ts | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/editor/src/core/shared/import/import-operation-service.ts b/editor/src/core/shared/import/import-operation-service.ts index c814f70ecdf9..e04b0cb10103 100644 --- a/editor/src/core/shared/import/import-operation-service.ts +++ b/editor/src/core/shared/import/import-operation-service.ts @@ -40,6 +40,9 @@ export function notifyOperationStarted(dispatch: EditorDispatch, operation: Impo timeStarted: Date.now(), timeDone: null, } + if (!isFeatureEnabled('Import Wizard')) { + return + } setTimeout(() => { dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) }, 0) @@ -56,6 +59,9 @@ export function notifyOperationFinished( timeDone: timeDone, result: result, } + if (!isFeatureEnabled('Import Wizard')) { + return + } setTimeout(() => { dispatch([updateImportOperations([operationWithTime], ImportOperationAction.Update)]) }, 0) diff --git a/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts index 7d9625922346..eb0d17b0dda8 100644 --- a/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts +++ b/editor/src/core/shared/import/proejct-health-check/utopia-requirements-service.ts @@ -8,6 +8,7 @@ import { RequirementResolutionResult, RequirementResolutionStatus, } from './utopia-requirements-types' +import { isFeatureEnabled } from '../../../../utils/feature-switches' const initialTexts: Record = { storyboard: 'Checking storyboard.js', @@ -20,6 +21,9 @@ export function updateProjectRequirementsStatus( dispatch: EditorDispatch, projectRequirements: Partial, ) { + if (!isFeatureEnabled('Import Wizard')) { + return + } setTimeout(() => { dispatch([updateProjectRequirements(projectRequirements)]) }, 0)