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 -}