diff --git a/cypress/e2e/functions.cy.js b/cypress/e2e/functions.cy.js index 4fac7aeb..296a5d66 100644 --- a/cypress/e2e/functions.cy.js +++ b/cypress/e2e/functions.cy.js @@ -15,6 +15,8 @@ describe("Functions", () => { cy.typeEditor(changedEditorValue); + cy.waitEvent("CONTEXT_SAVED"); + cy.checkLocalContext(this.projectId, "function", changedEditorValue); }); }); diff --git a/cypress/e2e/projects-path.cy.js b/cypress/e2e/projects-path.cy.js index a90d29c6..ee7bd91a 100644 --- a/cypress/e2e/projects-path.cy.js +++ b/cypress/e2e/projects-path.cy.js @@ -17,7 +17,7 @@ describe("Projects Path", () => { cy.waitEvent("CONTAINER_LOADED"); - cy.storageGet("ide.selected.context").then((project) => { + cy.storageGet("ide.selected.project").then((project) => { expect(project).to.exist; expect(project).to.have.property("id", cloudProjectId); expect(project).to.have.property("type", "CLOUD"); @@ -45,7 +45,7 @@ describe("Projects Path", () => { const localProjectId = "3450f289-0fc5-45e9-9a4a-606c0a63cdfe"; const selectedProject = { id: localProjectId, type: "LOCAL" }; - cy.storageSet("ide.selected.context", selectedProject); + cy.storageSet("ide.selected.project", selectedProject); cy.visit("/"); @@ -62,11 +62,11 @@ describe("Projects Path", () => { const pathParts = pathname.split("/"); const projectId = pathParts[pathParts.length - 2]; - cy.storageGet(`ide.context.${projectId}`).as("project"); + cy.storageGet(`ide.specification.${projectId}`).as("project"); cy.get("@project").should("exist"); - cy.storageGet(`ide.selected.context`).as("selectedProject"); + cy.storageGet(`ide.selected.project`).as("selectedProject"); cy.get("@selectedProject") .should((selectedProject) => { @@ -91,7 +91,7 @@ describe("Projects Path", () => { const pathParts = pathname.split("/"); const projectId = pathParts[pathParts.length - 2]; - cy.storageGet(`ide.selected.context`).then((selectedProject) => { + cy.storageGet(`ide.selected.project`).then((selectedProject) => { expect(selectedProject.id).to.equal(projectId); expect(selectedProject.type).to.equal("LOCAL"); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e25e766f..a6cd3b74 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -65,15 +65,19 @@ Cypress.Commands.add("setup", (container, fixtureType, type) => { cy.fixture("PROJECTS/LOCAL/project").then((context) => { const { project, types, functions, logic, api, declarations } = context; - cy.storageSet(`ide.context.3450f289-0fc5-45e9-9a4a-606c0a63cdfe`, { - project: project, - specification: { + cy.storageSet( + `ide.specification.3450f289-0fc5-45e9-9a4a-606c0a63cdfe`, + { api, logic, functions, types, declarations, - }, + } + ); + cy.storageSet(`ide.project.3450f289-0fc5-45e9-9a4a-606c0a63cdfe`, { + ...project, + id: "3450f289-0fc5-45e9-9a4a-606c0a63cdfe", }); }); } @@ -97,14 +101,15 @@ Cypress.Commands.add("typeEditor", (changedEditorValue) => { cy.get("section").should("be.visible"); cy.get(".monaco-editor").should("be.visible"); - cy.get('textarea[role="textbox"]').type("{selectall}{del}"); + cy.get( + "[class^='monaco-editor no-user-select showUnused showDeprecated vs-dark']" + ).click(); + cy.get('textarea[role="textbox"]').type("{selectAll}"); cy.get('textarea[role="textbox"]').type(changedEditorValue, { force: true, parseSpecialCharSequences: false, }); - - cy.wait(1000); }); Cypress.Commands.add("sendMessage", (message, fixture) => { @@ -327,14 +332,15 @@ Cypress.Commands.add( "checkLocalContext", (projectId, specification, changedEditorValue) => { let checkedSpecification; - cy.storageGet(`ide.context.${projectId}`).then((project) => { + cy.storageGet(`ide.specification.${projectId}`).then((spec) => { if (specification === "api") { - checkedSpecification = project.specification.api[0]["action"]; + checkedSpecification = spec.api[0]["action"]; } else if (specification === "declaration") { - checkedSpecification = project.specification.declarations[0].definition; + checkedSpecification = spec.declarations[0].definition; } if (specification === "function") { - checkedSpecification = project.specification.functions[0].definition; + console.log(spec.functions[0].definition); + checkedSpecification = spec.functions[0].definition; } cy.normalizeString(checkedSpecification).then((normalizedDefinition) => { diff --git a/src/components/LogoutButton/LogoutButton.jsx b/src/components/LogoutButton/LogoutButton.jsx index 29efac37..7f0b84c3 100644 --- a/src/components/LogoutButton/LogoutButton.jsx +++ b/src/components/LogoutButton/LogoutButton.jsx @@ -8,10 +8,10 @@ function LogoutButton({ onLogout }) { const handleLogout = () => { storage.remove("oauth.token"); - const latestContext = storage.get("ide", "selected", "context"); + const latestContext = storage.get("ide", "selected", "project"); publish("USER", { login: false, id: null }); if (latestContext.type === "CLOUD") { - storage.remove("ide", "selected", "context"); + storage.remove("ide", "selected", "project"); navigate("/new"); } onLogout && onLogout(); diff --git a/src/containers/IDE/IDE.jsx b/src/containers/IDE/IDE.jsx index 7283492b..bb42005a 100644 --- a/src/containers/IDE/IDE.jsx +++ b/src/containers/IDE/IDE.jsx @@ -54,14 +54,8 @@ function IDE() { }, [mobileSize]); function getContextFromStorage(projectId) { - const localContext = storage.get("ide", "context", projectId); - - if (!localContext) { - navigate("/error/api"); - return null; - } - - const { specification, project } = localContext; + const project = storage.get("ide", "project", projectId); + const specification = storage.get("ide", "specification", projectId); if (!specification && !project) { navigate("/error/api"); @@ -123,7 +117,7 @@ function IDE() { description: project.description, }; - storage.set("ide", "selected", "context", { + storage.set("ide", "selected", "project", { id: project.id, type: "CLOUD", }); @@ -138,10 +132,8 @@ function IDE() { const context = Context.withSample(); context.get = (prop) => Context.resolve(context, prop); const { specification, project } = context; - storage.set("ide", "context", project.id, { - specification: specification, - project: project, - }); + storage.set("ide", "project", project.id, project); + storage.set("ide", "specification", project.id, specification); navigate(`/${project.id}/api?mode=local`); @@ -156,6 +148,7 @@ function IDE() { } const initContext = (context) => { + console.log(context); if ( !Settings.description() || Settings.description() !== context.project.description @@ -179,9 +172,9 @@ function IDE() { if ( context.project.type === "LOCAL" && - storage.get("ide", "context", context.project.id) + storage.get("ide", "project", context.project.id) ) { - storage.set("ide", "selected", "context", { + storage.set("ide", "selected", "project", { id: context.project.id, type: "LOCAL", }); @@ -197,6 +190,7 @@ function IDE() { }; const initVfs = (context) => { + console.log(context); const files = contextToMap(context.specification); vfs.init(files); }; diff --git a/src/context/reducer.js b/src/context/reducer.js index f1a43c2f..f1c05f7f 100644 --- a/src/context/reducer.js +++ b/src/context/reducer.js @@ -1,9 +1,12 @@ import ActionTemplate from "../templates/ActionTemplate.js"; import Context from "../context"; import { publish } from "@nucleoidai/react-event"; +import service from "../service.js"; +import { storage } from "@nucleoidjs/webstorage"; import { v4 as uuid } from "uuid"; function contextReducer(context, { type, payload }) { + const selectedProject = storage.get("ide", "selected", "project"); context = Context.copy(context); const { specification, pages } = context; @@ -455,6 +458,7 @@ function contextReducer(context, { type, payload }) { } else { specification.types.push(updatedTypes); } + break; } case "ADD_TYPE": { @@ -471,6 +475,7 @@ function contextReducer(context, { type, payload }) { }, }; specification.types.push(newType); + break; } case "DELETE_TYPE": { @@ -482,6 +487,7 @@ function contextReducer(context, { type, payload }) { if (typeIndex !== -1) { specification.types.splice(typeIndex, 1); } + break; } @@ -495,6 +501,7 @@ function contextReducer(context, { type, payload }) { specification.types[typeIndex].name = newTypeName; specification.types[typeIndex].schema.name = newTypeName; } + break; } case "SAVE_API_PARAMS": { @@ -507,6 +514,7 @@ function contextReducer(context, { type, payload }) { if (apiIndex !== -1) { specification.api[apiIndex].params = params; } + break; } case "UPDATE_API_PATH_METHOD": { @@ -535,6 +543,7 @@ function contextReducer(context, { type, payload }) { default: } + service.saveSpecification(selectedProject, specification); console.debug("contextReducer", type, context); return context; } diff --git a/src/service.js b/src/service.js index caa528b1..d52bede5 100644 --- a/src/service.js +++ b/src/service.js @@ -1,5 +1,7 @@ import Settings from "./settings"; import http from "./http"; +import { publish } from "@nucleoidai/react-event"; +import { storage } from "@nucleoidjs/webstorage"; const query = async (body) => { return fetch(Settings.url.terminal(), { @@ -78,8 +80,21 @@ const getContext = (contextId) => { return http.get(`services/${contextId}/specification`); }; -const saveContext = (contextId, context) => { - return http.put(`services/${contextId}/specification`, context); +const saveSpecification = (selected, specification) => { + const { id, type } = selected; + + switch (type) { + case "LOCAL": + storage.set("ide", "specification", id, specification); + break; + case "CLOUD": + return http.put(`services/${id}/specification`, specification); + case "TERMINAL": + publish("RUNTIME_CONNECTION", { + status: true, + metrics: { total: 100, free: 50 }, + }); + } }; const getGraph = () => { @@ -114,7 +129,7 @@ const service = { deleteProject, getProjectServices, getContext, - saveContext, + saveSpecification, createSandbox, getGraph, getConfig, diff --git a/src/test/context.test.js b/src/test/context.test.js index b1c1a8a4..aebfc98a 100644 --- a/src/test/context.test.js +++ b/src/test/context.test.js @@ -1,9 +1,37 @@ import Context from "../context"; import { contextReducer } from "../context/reducer"; +import { storage } from "@nucleoidjs/webstorage"; + +jest.mock("@nucleoidjs/webstorage", () => { + const memory = new Map(); + return { + storage: { + get: jest.fn((...args) => memory.get(args.join("."))), + set: jest.fn((...args) => { + const value = args.pop(); + memory.set(args.join("."), value); + }), + clear: jest.fn(() => memory.clear()), + }, + }; +}); jest.mock("@nucleoidai/react-event", () => ({ publish: jest.fn(), })); + +beforeEach(() => { + storage.set("ide", "selected", "project", { + id: "21d2530b-4657-4ac0-b8cd-1a9f82786e32", + }); + storage.set( + "ide", + "project", + "21d2530b-4657-4ac0-b8cd-1a9f82786e32", + Context.withBlank() + ); +}); + test("Resolve context with property", () => { const state = contextReducer(Context.init(), { type: "SET_SELECTED_API", diff --git a/src/utils/Path.js b/src/utils/Path.js index 0f707f5d..15fa133d 100644 --- a/src/utils/Path.js +++ b/src/utils/Path.js @@ -24,7 +24,7 @@ const addSlashMark = (path) => { }; const getRecentProject = () => { - const recentProject = storage.get("ide", "selected", "context"); + const recentProject = storage.get("ide", "selected", "project"); if (recentProject) { return recentProject; diff --git a/src/utils/test/Path.test.js b/src/utils/test/Path.test.js index 5acc4f08..c2585800 100644 --- a/src/utils/test/Path.test.js +++ b/src/utils/test/Path.test.js @@ -66,12 +66,12 @@ test("returns false when the new path is not used", () => { test("returns the selected project in storage", () => { const projectId = "f1f04060-1ea4-46fc-bbf9-fb69c1faca8b"; - storage.set("ide", "selected", "context", projectId); + storage.set("ide", "selected", "project", projectId); expect(storage.set).toHaveBeenCalledWith( "ide", "selected", - "context", + "project", projectId ); @@ -80,7 +80,7 @@ test("returns the selected project in storage", () => { }); test("returns null when the selected project is not found", () => { - storage.set("ide", "selected", "context", null); + storage.set("ide", "selected", "project", null); const recentProject = Path.getRecentProject(); expect(recentProject).toEqual(null); }); diff --git a/src/widgets/Editor/Editor.jsx b/src/widgets/Editor/Editor.jsx index 3170a38e..67607902 100644 --- a/src/widgets/Editor/Editor.jsx +++ b/src/widgets/Editor/Editor.jsx @@ -156,29 +156,15 @@ const Editor = React.forwardRef((props, ref) => { }; } - const debouncedSave = debounce((id, specification, project) => { - if (mode === "cloud") { - service.saveContext(id, specification); - } else if (mode === "local") { - storage.set("ide", "context", id, { - specification, - project, - }); - } else if (mode === "terminal") { - publish("RUNTIME_CONNECTION", { - status: true, - metrics: { total: 100, free: 50 }, - }); - } + const debouncedSave = debounce((project, specification) => { + service.saveSpecification(project, specification); - publish("CONTEXT_SAVED", { contextId: id, to: mode }); + publish("CONTEXT_SAVED", { contextId: project.id, to: mode }); }, 300); function handleChange(e) { if (logic) { - const { - project: { id }, - } = context; + const { project, specification } = context; context.specification.declarations = context.specification.declarations.map((item) => { @@ -190,7 +176,7 @@ const Editor = React.forwardRef((props, ref) => { return item; }); - debouncedSave(id, context.specification, context.project); + debouncedSave(project, specification); } if (query) { context.pages.query.text = e; diff --git a/src/widgets/ProjectDialog/ProjectDialog.jsx b/src/widgets/ProjectDialog/ProjectDialog.jsx index cbdea678..9af80b5c 100644 --- a/src/widgets/ProjectDialog/ProjectDialog.jsx +++ b/src/widgets/ProjectDialog/ProjectDialog.jsx @@ -92,11 +92,9 @@ function ProjectDialog({ handleClose, open, setOpen }) { const projects = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); - if (key.startsWith("ide.context.")) { - const context = JSON.parse(localStorage.getItem(key)); - if (context.project) { - projects.push(context.project); - } + if (key.startsWith("ide.project.")) { + const project = JSON.parse(localStorage.getItem(key)); + projects.push(project); } } return projects; @@ -188,10 +186,14 @@ function ProjectDialog({ handleClose, open, setOpen }) { context.project.name = name; context.project.type = "LOCAL"; - storage.set("ide", "context", context.project.id, { - specification: context.specification, - project: context.project, - }); + storage.set( + "ide", + "specification", + context.project.id, + context.specification + ); + + storage.set("ide", "project", context.project.id, context.project); publish("PROJECT_CREATED", { id: context.project.id, @@ -218,8 +220,8 @@ function ProjectDialog({ handleClose, open, setOpen }) { const uploadToCloud = (projectId) => { setLoading(true); - const localContext = storage.get("ide", "context", projectId); - const { project, specification } = localContext; + const project = storage.get("ide", "project", projectId); + const specification = storage.get("ide", "specification", projectId); const context = contextToCloud(specification, project); @@ -229,7 +231,8 @@ function ProjectDialog({ handleClose, open, setOpen }) { publish("PROJECT_UPLOADED", { id: response.data.id, }); - storage.remove("ide", "context", projectId); + storage.remove("ide", "specification", projectId); + storage.remove("ide", "project", projectId); getLocalProjects(); getCloudProjects(); }) @@ -241,14 +244,17 @@ function ProjectDialog({ handleClose, open, setOpen }) { const editProject = (projectToEdit) => { const { name, type, id } = projectToEdit; if (type === "LOCAL") { - const localContext = storage.get("ide", "context", id); - const { project, specification } = localContext; + console.log(id); + const project = storage.get("ide", "project", id); + const specification = storage.get("ide", "specification", id); + project.name = name; - storage.remove("ide", "context", id); - storage.set("ide", "context", id, { - specification: specification, - project: project, - }); + + storage.remove("ide", "specification", id); + storage.remove("ide", "project", id); + + storage.set("ide", "project", id, project); + storage.set("ide", "specification", id, specification); publish("PROJECT_UPDATED", { id: project.id, @@ -285,6 +291,7 @@ function ProjectDialog({ handleClose, open, setOpen }) { }); } else { localStorage.removeItem(`ide.context.${project.id}`); + localStorage.removeItem(`ide.project.${project.id}`); getLocalProjects(); } publish("PROJECT_DELETED", { id: projectId }); diff --git a/src/widgets/VFSEditor/VFSEditor.jsx b/src/widgets/VFSEditor/VFSEditor.jsx index 53cf7a1d..ab2c965a 100644 --- a/src/widgets/VFSEditor/VFSEditor.jsx +++ b/src/widgets/VFSEditor/VFSEditor.jsx @@ -31,7 +31,7 @@ const VFSEditor = React.forwardRef((props, ref) => { const timerRef = React.useRef(); const [context] = useContext(); const editorRef = React.useRef(null); - + const selectedProject = storage.get("ide", "selected", "project"); const file = getFile(context, props); const checkFunction = React.useCallback(() => { @@ -104,33 +104,16 @@ const VFSEditor = React.forwardRef((props, ref) => { key = context.get("pages.functions.selected") + ".ts"; } - const { - project: { id }, - } = context; - - if (mode === "cloud") { - const nucContext = { ...context.specification }; - delete nucContext.project; - - service.saveContext(id, nucContext); - } else if (mode === "local") { - storage.set("ide", "context", id, { - specification: context.specification, - project: context.project, - }); - } else if (mode === "terminal") { - publish("RUNTIME_CONNECTION", { - status: true, - metrics: { total: 100, free: 50 }, - }); - } - publish("CONTEXT_SAVED", { contextId: id, to: mode }); + service.saveSpecification(selectedProject, context.specification); + + publish("CONTEXT_SAVED", { contextId: context.project.id, to: mode }); publish("CONTEXT_CHANGED", { // TODO Optimize preparing files files: contextToMap(context.specification).filter( (item) => item.key === key ), }); + //eslint-disable-next-line }, [api, context, mode]); function handleEditorDidMount(editor, monaco) {