From 1eab1840b6704eaf04bf389788bf28d3d992be0d Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 4 Oct 2021 22:03:37 -0700 Subject: [PATCH 01/14] implement controls on hover with stubbed functionality --- src/assets/images/icon-file.svg | 2 +- src/assets/images/icon-folder.svg | 4 +- src/components/Explorer.tsx | 75 ++++++++++++++++++----- src/components/FileModal.tsx | 48 ++++++++++++--- src/components/FolderStructure.tsx | 97 +++++++++++++++++++++++------- src/contexts/WorkplaceState.ts | 40 ++++++++++-- 6 files changed, 213 insertions(+), 53 deletions(-) diff --git a/src/assets/images/icon-file.svg b/src/assets/images/icon-file.svg index bf6836ea..926b71e2 100644 --- a/src/assets/images/icon-file.svg +++ b/src/assets/images/icon-file.svg @@ -1,4 +1,4 @@ - + diff --git a/src/assets/images/icon-folder.svg b/src/assets/images/icon-folder.svg index f9909beb..b90febfe 100644 --- a/src/assets/images/icon-folder.svg +++ b/src/assets/images/icon-folder.svg @@ -1,9 +1,9 @@ - + - + diff --git a/src/components/Explorer.tsx b/src/components/Explorer.tsx index 1d65e026..faae714e 100644 --- a/src/components/Explorer.tsx +++ b/src/components/Explorer.tsx @@ -1,21 +1,24 @@ import { useState, useEffect, useContext } from "react"; import styled from "styled-components"; -import iconPackage from "../assets/images/icon-package.svg"; -import iconCanister from "../assets/images/icon-canister.svg"; -import iconClose from "../assets/images/icon-close.svg"; -import iconPlus from "../assets/images/icon-plus.svg"; -import { ListButton } from "./shared/SelectList"; import { WorkplaceState, WorkplaceDispatchContext, } from "../contexts/WorkplaceState"; + +import { ListButton } from "./shared/SelectList"; import { PackageModal } from "./PackageModal"; import { FileModal } from "./FileModal"; import { CanisterModal } from "./CanisterModal"; +import { FolderStructure } from "./FolderStructure"; +import { Confirm } from "./shared/Confirm"; + import { PackageInfo } from "../workers/file"; import { ILoggingStore } from "./Logger"; import { deleteCanister } from "../build"; -import { FolderStructure } from "./FolderStructure"; +import iconPackage from "../assets/images/icon-package.svg"; +import iconCanister from "../assets/images/icon-canister.svg"; +import iconClose from "../assets/images/icon-close.svg"; +import iconPlus from "../assets/images/icon-plus.svg"; const StyledExplorer = styled.div` width: var(--explorerWidth); @@ -63,17 +66,30 @@ export function Explorer({ state, ttl, logger }: ExplorerProps) { const [showPackage, setShowPackage] = useState(false); const [showFileModal, setShowFileModal] = useState(false); const [showCanisterModal, setShowCanisterModal] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [fileToModify, setFileToModify] = useState(""); const dispatch = useContext(WorkplaceDispatchContext); - const onSelectFile = (selectedFile: string) => { + function handleRenameClick(e, selectedFile: string) { + e.stopPropagation(); + setFileToModify(selectedFile); + setShowFileModal(true); + } + function handleDeleteClick(e, selectedFile: string) { + e.stopPropagation(); + setFileToModify(selectedFile); + setShowDeleteConfirm(true); + } + + function onSelectFile(selectedFile: string) { dispatch({ type: "selectFile", payload: { path: selectedFile, }, }); - }; - const loadPackage = (pack: PackageInfo) => { + } + function loadPackage(pack: PackageInfo) { dispatch({ type: "loadPackage", payload: { @@ -81,7 +97,16 @@ export function Explorer({ state, ttl, logger }: ExplorerProps) { package: pack, }, }); - }; + } + function onDeleteFile(path) { + dispatch({ + type: "deleteFile", + payload: { + path, + }, + }); + } + const onCanister = async (selectedCanister: string, action: string) => { switch (action) { case "select": @@ -176,16 +201,36 @@ export function Explorer({ state, ttl, logger }: ExplorerProps) { return ( - setShowFileModal(false)} /> + { + setShowFileModal(false); + setFileToModify(""); + }} + filename={fileToModify} + /> setShowPackage(false)} + close={() => { + setShowPackage(false); + setFileToModify(""); + }} loadPackage={loadPackage} /> setShowCanisterModal(false)} /> + { + setShowDeleteConfirm(false); + setFileToModify(""); + }} + > + Are you sure you want to delete the file {fileToModify}? + Files setShowFileModal(true)} aria-label="Add file"> @@ -194,10 +239,12 @@ export function Explorer({ state, ttl, logger }: ExplorerProps) { - + Packages setShowPackage(true)} aria-label="Add package"> diff --git a/src/components/FileModal.tsx b/src/components/FileModal.tsx index e0800f3e..a3d1c7b4 100644 --- a/src/components/FileModal.tsx +++ b/src/components/FileModal.tsx @@ -26,19 +26,37 @@ const ButtonContainer = styled.div` margin-top: 3rem; `; -export function FileModal({ isOpen, close }) { - const [fileName, setFileName] = useState(""); +export function FileModal({ isOpen, close, filename: initialFilename = "" }) { + const [fileName, setFileName] = useState(initialFilename); const worker = useContext(WorkerContext); const dispatch = useContext(WorkplaceDispatchContext); + const isRename = Boolean(initialFilename); + const name = fileName.endsWith(".mo") ? fileName : `${fileName}.mo`; + async function addFile() { - const name = fileName.endsWith(".mo") ? fileName : `${fileName}.mo`; await worker.Moc({ type: "save", file: name, content: "" }); await dispatch({ type: "saveFile", payload: { path: name, contents: "" } }); await dispatch({ type: "selectFile", payload: { path: name } }); await close(); } + async function renameFile() { + if (initialFilename !== fileName) { + await worker.Moc({ + type: "renameFile", + file: initialFilename, + newName: name, + }); + await dispatch({ + type: "renameFile", + payload: { path: initialFilename, newPath: name }, + }); + await dispatch({ type: "selectFile", payload: { path: name } }); + } + await close(); + } + return ( -

Add a new file

+

+ {isRename ? ( + <> + Rename {initialFilename} + + ) : ( + "Add a new file" + )} +

setFileName(e.target.value)} autoFocus /> - + {isRename ? ( + + ) : ( + + )}
diff --git a/src/components/FolderStructure.tsx b/src/components/FolderStructure.tsx index 5a6ec026..c4ed4e0c 100644 --- a/src/components/FolderStructure.tsx +++ b/src/components/FolderStructure.tsx @@ -1,7 +1,8 @@ -import styled from "styled-components"; +import styled, { css } from "styled-components"; import { ListButton } from "./shared/SelectList"; import folderIcon from "../assets/images/icon-folder.svg"; import fileIcon from "../assets/images/icon-file.svg"; +import closeIcon from "../assets/images/icon-close.svg"; interface FoldersJson { files: Array; @@ -42,6 +43,18 @@ interface DepthProp { nestDepth: number; } +const handleLongText = css` + text-align: unset; + + > p { + width: 100%; + margin: unset; + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +`; + const StyledFolder = styled.summary` display: flex; align-items: center; @@ -53,13 +66,44 @@ const StyledFolder = styled.summary` &:hover { color: var(--colorPrimary); } + + ${handleLongText} +`; + +const FileFunctions = styled.span` + display: none; + position: absolute; + top: 0; + right: 0; + padding: 0.1rem 0 0 0.5rem; + height: calc(100% + 0.2rem); + background-image: linear-gradient(90deg, transparent 0%, var(--grey100) 15%); `; const FileButton = styled(ListButton)` + position: relative; padding-left: calc(${({ nestDepth }) => nestDepth + 1} * 1.6rem); border-bottom: none; height: 3rem; user-select: none; + + &:hover ${FileFunctions} { + display: inline; + } + + ${handleLongText} +`; + +const FileFunctionButton = styled.button` + padding: 0.5rem 0.4rem 0.5rem 0.6rem; + width: 2.7rem; + border-radius: 50%; + line-height: 1; + background-color: transparent; + + &:hover { + background-color: var(--grey300); + } `; const Icon = styled.img` @@ -67,28 +111,34 @@ const Icon = styled.img` margin-right: 0.8rem; `; -interface RenderOptions { - folderStructure: FoldersJson; - onSelectFile: (folder: string) => void; +interface SharedProps { activeFile: string | null; - nestDepth?: number; + onSelectFile: (filepath: string) => void; + onRenameFile: (e, filepath: string) => void; + onDeleteFile: (e, filepath: string) => void; } +type RenderOptions = SharedProps & { + folderStructure: FoldersJson; + nestDepth?: number; +}; + function renderFolderStructure(options: RenderOptions) { - const { folderStructure, onSelectFile, activeFile, nestDepth = 0 } = options; + const { folderStructure, activeFile, nestDepth = 0, ...functions } = options; + const { onSelectFile, onRenameFile, onDeleteFile } = functions; const finalStructure = Object.entries(folderStructure.folders).map( ([folderName, contents]) => ( - + - {folderName} +

{folderName}

{renderFolderStructure({ folderStructure: contents, - onSelectFile, - activeFile, nestDepth: nestDepth + 1, + activeFile, + ...functions, })}
) @@ -101,6 +151,7 @@ function renderFolderStructure(options: RenderOptions) { finalStructure.push( onSelectFile(filePath)} isActive={isActive} @@ -108,7 +159,15 @@ function renderFolderStructure(options: RenderOptions) { aria-label="File" > - {fileName} +

{fileName}

+ + onRenameFile(e, filePath)}> + + + onDeleteFile(e, filePath)}> + + +
); }); @@ -116,20 +175,12 @@ function renderFolderStructure(options: RenderOptions) { return <>{finalStructure}; } -interface Folders { +type Folders = SharedProps & { filePaths: Array; - onSelectFile: (folder: string) => void; - activeFile: string | null; -} +}; -export function FolderStructure({ - filePaths, - onSelectFile, - activeFile, -}: Folders) { +export function FolderStructure({ filePaths, ...passProps }: Folders) { const folderStructure = structureFolders(filePaths); - return ( - <>{renderFolderStructure({ folderStructure, onSelectFile, activeFile })} - ); + return <>{renderFolderStructure({ folderStructure, ...passProps })}; } diff --git a/src/contexts/WorkplaceState.ts b/src/contexts/WorkplaceState.ts index e541ba16..ed0f5a8f 100644 --- a/src/contexts/WorkplaceState.ts +++ b/src/contexts/WorkplaceState.ts @@ -116,6 +116,22 @@ export type WorkplaceReducerAction = contents: string; }; } + | { + type: "renameFile"; + payload: { + /** path of file that should be updated. Should correspond to a property in state.files */ + path: string; + /** new name of file */ + newPath: string; + }; + } + | { + type: "deleteFile"; + payload: { + /** path of file that should be deleted. Should correspond to a property in state.files */ + path: string; + }; + } | { type: "deployWorkplace"; payload: { @@ -207,6 +223,23 @@ export const workplaceReducer = { [action.payload.path]: action.payload.contents, }, }; + case "renameFile": { + const files = { + ...state.files, + }; + const fileContents = files[action.payload.path]; + delete files[action.payload.path]; + files[action.payload.newPath] = fileContents; + return { ...state, files }; + } + case "deleteFile": { + const files = { + ...state.files, + }; + + delete files[action.payload.path]; + return { ...state, files }; + } case "deployWorkplace": { const name = action.payload.canister.name!; return { @@ -219,11 +252,8 @@ export const workplaceReducer = { }; } default: - // this should never be reached. If there is a type error here, add a 'case' - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let x: never = action; + return state; } - return state; }, }; @@ -237,7 +267,7 @@ export const WorkplaceDispatchContext = React.createContext< React.Dispatch >(() => { console.warn( - "using default WorkplaceDispathcContext. Make sure to Provide one in your component tree" + "using default WorkplaceDispatchContext. Make sure to Provide one in your component tree" ); }); export const WorkerContext = React.createContext(() => { From b74fbd0e720fba51751a172a1f08b8f83e7efab5 Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Wed, 6 Oct 2021 11:54:26 -0700 Subject: [PATCH 02/14] make TypeScript happier --- src/App.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index fccf6f49..b8937662 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -228,9 +228,10 @@ export function App() { useEffect(() => { // Show Candid UI iframe if there are canisters - const isCandidReady = + const isCandidReady = Boolean( workplaceState.selectedCanister && - workplaceState.canisters[workplaceState.selectedCanister]; + workplaceState.canisters[workplaceState.selectedCanister] + ); setShowCandidUI(isCandidReady); setCandidWidth(isCandidReady ? "30vw" : "0"); }, [workplaceState.canisters, workplaceState.selectedCanister]); From 76c9cd4c8942fb6c550c6bedfbc2e2b39dd5ffb2 Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Wed, 6 Oct 2021 11:55:00 -0700 Subject: [PATCH 03/14] delete and rename working as intended --- src/assets/images/icon-pencil.svg | 13 +++++++++++ src/components/Explorer.tsx | 12 +++++++--- src/components/FileModal.tsx | 36 ++++++++++++++++++------------ src/components/FolderStructure.tsx | 5 +++-- src/contexts/WorkplaceState.ts | 18 +++++++++++++-- 5 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 src/assets/images/icon-pencil.svg diff --git a/src/assets/images/icon-pencil.svg b/src/assets/images/icon-pencil.svg new file mode 100644 index 00000000..23e9b1b5 --- /dev/null +++ b/src/assets/images/icon-pencil.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/src/components/Explorer.tsx b/src/components/Explorer.tsx index faae714e..e348c251 100644 --- a/src/components/Explorer.tsx +++ b/src/components/Explorer.tsx @@ -47,6 +47,9 @@ const CloseButton = styled(MyButton)` margin-right: -1.1rem; margin-left: -0.3rem; `; +const ConfirmText = styled.p` + margin-bottom: 2rem; +`; interface ExplorerProps { state: WorkplaceState; @@ -98,13 +101,14 @@ export function Explorer({ state, ttl, logger }: ExplorerProps) { }, }); } - function onDeleteFile(path) { + function onDeleteFile() { dispatch({ type: "deleteFile", payload: { - path, + path: fileToModify, }, }); + setFileToModify(""); } const onCanister = async (selectedCanister: string, action: string) => { @@ -229,7 +233,9 @@ export function Explorer({ state, ttl, logger }: ExplorerProps) { setFileToModify(""); }} > - Are you sure you want to delete the file {fileToModify}? + + Are you sure you want to delete the file {fileToModify}? + Files diff --git a/src/components/FileModal.tsx b/src/components/FileModal.tsx index a3d1c7b4..69ef798a 100644 --- a/src/components/FileModal.tsx +++ b/src/components/FileModal.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { useState, useContext } from "react"; +import { useState, useContext, useEffect } from "react"; import { Modal } from "./shared/Modal"; import { Button } from "./shared/Button"; @@ -26,8 +26,21 @@ const ButtonContainer = styled.div` margin-top: 3rem; `; +const MyButton = styled(Button)` + width: 14rem; +`; + export function FileModal({ isOpen, close, filename: initialFilename = "" }) { const [fileName, setFileName] = useState(initialFilename); + + // Make sure local fileName stays in sync with whatever was passed in + useEffect(() => { + if (initialFilename !== fileName) { + setFileName(initialFilename); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialFilename]); + const worker = useContext(WorkerContext); const dispatch = useContext(WorkplaceDispatchContext); @@ -38,23 +51,18 @@ export function FileModal({ isOpen, close, filename: initialFilename = "" }) { await worker.Moc({ type: "save", file: name, content: "" }); await dispatch({ type: "saveFile", payload: { path: name, contents: "" } }); await dispatch({ type: "selectFile", payload: { path: name } }); - await close(); + close(); } async function renameFile() { if (initialFilename !== fileName) { - await worker.Moc({ - type: "renameFile", - file: initialFilename, - newName: name, - }); await dispatch({ type: "renameFile", payload: { path: initialFilename, newPath: name }, }); await dispatch({ type: "selectFile", payload: { path: name } }); } - await close(); + close(); } return ( @@ -78,21 +86,21 @@ export function FileModal({ isOpen, close, filename: initialFilename = "" }) { setFileName(e.target.value)} autoFocus /> {isRename ? ( - +
) : ( - + )} - + Cancel diff --git a/src/components/FolderStructure.tsx b/src/components/FolderStructure.tsx index c4ed4e0c..a1866a1d 100644 --- a/src/components/FolderStructure.tsx +++ b/src/components/FolderStructure.tsx @@ -2,6 +2,7 @@ import styled, { css } from "styled-components"; import { ListButton } from "./shared/SelectList"; import folderIcon from "../assets/images/icon-folder.svg"; import fileIcon from "../assets/images/icon-file.svg"; +import pencilIcon from "../assets/images/icon-pencil.svg"; import closeIcon from "../assets/images/icon-close.svg"; interface FoldersJson { @@ -162,7 +163,7 @@ function renderFolderStructure(options: RenderOptions) {

{fileName}

onRenameFile(e, filePath)}> - + onDeleteFile(e, filePath)}> @@ -182,5 +183,5 @@ type Folders = SharedProps & { export function FolderStructure({ filePaths, ...passProps }: Folders) { const folderStructure = structureFolders(filePaths); - return <>{renderFolderStructure({ folderStructure, ...passProps })}; + return renderFolderStructure({ folderStructure, ...passProps }); } diff --git a/src/contexts/WorkplaceState.ts b/src/contexts/WorkplaceState.ts index ed0f5a8f..af4297fe 100644 --- a/src/contexts/WorkplaceState.ts +++ b/src/contexts/WorkplaceState.ts @@ -230,7 +230,14 @@ export const workplaceReducer = { const fileContents = files[action.payload.path]; delete files[action.payload.path]; files[action.payload.newPath] = fileContents; - return { ...state, files }; + return { + ...state, + files, + selectedFile: + state.selectedFile === action.payload.path + ? action.payload.newPath + : state.selectedFile, + }; } case "deleteFile": { const files = { @@ -238,7 +245,14 @@ export const workplaceReducer = { }; delete files[action.payload.path]; - return { ...state, files }; + return { + ...state, + files, + selectedFile: + state.selectedFile === action.payload.path + ? selectFirstFile(state.files) + : state.selectedFile, + }; } case "deployWorkplace": { const name = action.payload.canister.name!; From 1872ac1bea0e3e2040803adfab2f87fa3cd2f1dd Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 16:09:20 -0700 Subject: [PATCH 04/14] setup github action for tests --- .github/workflows/frontend.yml | 27 ++++++++++ package-lock.json | 17 +++++++ package.json | 3 +- src/App.test.tsx | 9 ---- src/assets/fonts/CircularXX.css | 8 +-- src/components/Explorer.tsx | 1 - src/components/PackageModal.tsx | 2 +- src/components/shared/Button.test.tsx | 18 +++++++ src/components/shared/Button.tsx | 36 ++++++-------- src/components/shared/Confirm.test.tsx | 61 +++++++++++++++++++++++ src/components/shared/Modal.tsx | 6 ++- src/config/jest.config.js | 11 ---- src/contexts/WorkplaceState.test.js | 69 ++++++++++++++++++++++++++ src/contexts/WorkplaceState.ts | 10 ++-- 14 files changed, 226 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/frontend.yml delete mode 100644 src/App.test.tsx create mode 100644 src/components/shared/Button.test.tsx create mode 100644 src/components/shared/Confirm.test.tsx delete mode 100644 src/config/jest.config.js create mode 100644 src/contexts/WorkplaceState.test.js diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..29972ce3 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,27 @@ +# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Test Frontend + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run build --if-present + - run: npm test \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0d974c7d..ff80cae8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", + "@types/react-modal": "^3.13.1", "@types/styled-components": "^5.1.10", "lodash.debounce": "^4.0.8", "react": "^17.0.2", @@ -3037,6 +3038,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-modal": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz", + "integrity": "sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "0.0.8", "dev": true, @@ -22421,6 +22430,14 @@ "@types/react": "*" } }, + "@types/react-modal": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz", + "integrity": "sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==", + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "0.0.8", "dev": true, diff --git a/package.json b/package.json index ac36d5f9..0d89a4bc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", + "@types/react-modal": "^3.13.1", "@types/styled-components": "^5.1.10", "lodash.debounce": "^4.0.8", "react": "^17.0.2", @@ -35,7 +36,7 @@ "scripts": { "start": "craco start", "build": "craco build", - "test": "craco test -- --config=config/jest.config.js", + "test": "craco test", "eject": "craco eject", "deploy": "dfx deploy --argument '(null)'", "prestart": "dfx start --background --no-artificial-delay || exit 0", diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 05d3d3e5..00000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import { App } from "./App"; - -test("renders learn react link", () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/assets/fonts/CircularXX.css b/src/assets/fonts/CircularXX.css index d29ad54a..d942b43d 100644 --- a/src/assets/fonts/CircularXX.css +++ b/src/assets/fonts/CircularXX.css @@ -19,26 +19,26 @@ font-family: "CircularXX"; font-weight: 100; src: local(CircularXX), local(CircularXXWeb-Light), - url(./CircularXXWeb-Light.woff2) format("woff2"); + url(CircularXXWeb-Light.woff2) format("woff2"); } @font-face { font-family: "CircularXX"; font-weight: 400; src: local(CircularXX), local(CircularXXWeb-Book), - url(./CircularXXWeb-Book.woff2) format("woff2"); + url(CircularXXWeb-Book.woff2) format("woff2"); } @font-face { font-family: "CircularXX"; font-weight: 700; src: local(CircularXX), local(CircularXXWeb-Medium), - url(./CircularXXWeb-Medium.woff) format("woff"); + url(CircularXXWeb-Medium.woff) format("woff"); } @font-face { font-family: "CircularXX"; font-weight: 900; src: local(CircularXX), local(CircularXXWeb-Bold), - url(./CircularXXWeb-Bold.woff2) format("woff2"); + url(CircularXXWeb-Bold.woff2) format("woff2"); } diff --git a/src/components/Explorer.tsx b/src/components/Explorer.tsx index e348c251..eda4b982 100644 --- a/src/components/Explorer.tsx +++ b/src/components/Explorer.tsx @@ -217,7 +217,6 @@ export function Explorer({ state, ttl, logger }: ExplorerProps) { isOpen={showPackage} close={() => { setShowPackage(false); - setFileToModify(""); }} loadPackage={loadPackage} /> diff --git a/src/components/PackageModal.tsx b/src/components/PackageModal.tsx index 088b40f9..454ddc65 100644 --- a/src/components/PackageModal.tsx +++ b/src/components/PackageModal.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { useState, useContext } from "react"; +import { useContext } from "react"; import { Modal } from "./shared/Modal"; import { Button } from "./shared/Button"; import { ListButton, SelectList } from "./shared/SelectList"; diff --git a/src/components/shared/Button.test.tsx b/src/components/shared/Button.test.tsx new file mode 100644 index 00000000..a82cf665 --- /dev/null +++ b/src/components/shared/Button.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from "@testing-library/react"; +import { Button } from "./Button"; + +describe("Button component", () => { + it("fires onClick once when clicked", () => { + const testFn = jest.fn(); + + render(); + + const buttonElement = screen.getByText(/hello/i); + + expect(buttonElement).toBeInTheDocument(); + + buttonElement.click(); + + expect(testFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/shared/Button.tsx b/src/components/shared/Button.tsx index 2ed6e875..e81f6c68 100644 --- a/src/components/shared/Button.tsx +++ b/src/components/shared/Button.tsx @@ -13,26 +13,24 @@ export const Button = styled("button")<{ padding: 0.6rem 1.6rem; font-size: 1.4rem; font-weight: 500; - height: ${(props) => (props.small ? "3.3rem" : "4.5rem")}; - min-width: ${(props) => (props.small ? "5.6rem" : "7.8rem")}; - ${(props) => (props.width ? `width: ${props.width}` : "")}; - background-color: ${(props) => - props.variant - ? props.variant === "primary" + height: ${({ small }) => (small ? "3.3rem" : "4.5rem")}; + min-width: ${({ small }) => (small ? "5.6rem" : "7.8rem")}; + ${({ width }) => (width ? `width: ${width}` : "")}; + background-color: ${({ variant }) => + variant + ? variant === "primary" ? "var(--colorPrimary)" : "white" : "var(--grey200)"}; - color: ${(props) => - props.variant === "primary" + color: ${({ variant }) => + variant === "primary" ? "white" - : props.variant === "secondary" + : variant === "secondary" ? "var(--grey500)" : "var(--grey600)"}; - border: ${(props) => - `1px solid ${ - props.variant === "secondary" ? "var(--grey400)" : "transparent" - }`}; - border-radius: ${(props) => (props.small ? "1.7rem" : "2.3rem")}; + border: ${({ variant }) => + `1px solid ${variant === "secondary" ? "var(--grey400)" : "transparent"}`}; + border-radius: ${({ small }) => (small ? "1.7rem" : "2.3rem")}; &:not(:last-child) { margin-right: 2rem; @@ -47,12 +45,10 @@ export const Button = styled("button")<{ } &:hover { - background-color: ${(props) => - props.variant === "primary" - ? "var(--colorPrimaryDark)" - : "var(--grey100)"}; - ${(props) => - !props.variant + background-color: ${({ variant }) => + variant === "primary" ? "var(--colorPrimaryDark)" : "var(--grey100)"}; + ${({ variant }) => + !variant ? `color: var(--colorPrimary); border: 1px solid var(--grey300); diff --git a/src/components/shared/Confirm.test.tsx b/src/components/shared/Confirm.test.tsx new file mode 100644 index 00000000..7c70edbb --- /dev/null +++ b/src/components/shared/Confirm.test.tsx @@ -0,0 +1,61 @@ +import { useState } from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import { Confirm } from "./Confirm"; + +describe("Confirm component", () => { + const confirmFn = jest.fn(); + const cancelFn = jest.fn(); + + const StatefulConfirm = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + setIsOpen(false)} + onConfirm={confirmFn} + onCancel={cancelFn} + > + Hello + + + ); + }; + + it("opens and calls the confirm function", async () => { + render(); + + const buttonElement = screen.getByText(/open confirm/i); + + buttonElement.click(); + + const confirmText = screen.getByText(/hello/i); + const confirmButton = screen.getByText(/continue/i); + + await waitFor(() => expect(confirmText).toBeVisible()); + + confirmButton.click(); + + expect(confirmFn).toHaveBeenCalledTimes(1); + expect(confirmText).not.toBeVisible(); + }); + + it("opens and calls the cancel function", async () => { + render(); + const buttonElement = screen.getByText(/open confirm/i); + + buttonElement.click(); + + const confirmText = screen.getByText(/hello/i); + const cancelButton = screen.getByText(/cancel/i); + + await waitFor(() => expect(confirmText).toBeVisible()); + + cancelButton.click(); + + expect(cancelFn).toHaveBeenCalledTimes(1); + expect(confirmText).not.toBeVisible(); + }); +}); diff --git a/src/components/shared/Modal.tsx b/src/components/shared/Modal.tsx index 517845a9..d5a02be5 100644 --- a/src/components/shared/Modal.tsx +++ b/src/components/shared/Modal.tsx @@ -33,7 +33,9 @@ const StyleOverrides = createGlobalStyle` `; // Tell ReactModal to which element it should attach its Portal/overlay. -ReactModal.setAppElement("#root"); +if (process.env.NODE_ENV !== "test") { + ReactModal.setAppElement("#root"); +} // Override ReactModal's inline styles... ReactModal.defaultStyles = { ...ReactModal.defaultStyles, @@ -77,6 +79,8 @@ export function Modal({ contentLabel={label} onRequestClose={close} closeTimeoutMS={TIMING} + appElement={document.getElementById("root")!} + ariaHideApp={process.env.NODE_ENV !== "test"} {...props} > diff --git a/src/config/jest.config.js b/src/config/jest.config.js deleted file mode 100644 index 6a80b451..00000000 --- a/src/config/jest.config.js +++ /dev/null @@ -1,11 +0,0 @@ -const { aliases } = require("./generateAliases"); - -const moduleNameMapper = Object.entries(aliases).reduce( - (result, [key, value]) => { - result[`^${key}$`] = value; - return result; - }, - {} -); - -module.exports = { moduleNameMapper }; diff --git a/src/contexts/WorkplaceState.test.js b/src/contexts/WorkplaceState.test.js new file mode 100644 index 00000000..de10aae7 --- /dev/null +++ b/src/contexts/WorkplaceState.test.js @@ -0,0 +1,69 @@ +import { workplaceReducer } from "./WorkplaceState"; + +describe("WorkplaceState reducer", () => { + const defaultState = workplaceReducer.init({}); + + const newFileState = { + ...defaultState, + files: { + ...defaultState.files, + "New.mo": "Hello there!", + }, + }; + + it("initializes correctly", () => { + expect(defaultState.canisters).toEqual({}); + expect(defaultState.selectedCanister).toBe(null); + expect(defaultState.files).toEqual({ "Main.mo": "" }); + expect(defaultState.selectedFile).toBe("Main.mo"); + expect(defaultState.packages).toEqual({}); + }); + + it("adds files", () => { + const withNewFile = workplaceReducer.reduce(defaultState, { + type: "saveFile", + payload: { + path: "New.mo", + contents: "Hello there!", + }, + }); + + expect(withNewFile).toEqual(newFileState); + }); + + it("selects files", () => { + const withSelectedFile = workplaceReducer.reduce(newFileState, { + type: "selectFile", + payload: { + path: "New.mo", + }, + }); + + expect(withSelectedFile.selectedFile).toBe("New.mo"); + }); + + it("removes files", () => { + const withFileRemoved = workplaceReducer.reduce(newFileState, { + type: "deleteFile", + payload: { + path: "Main.mo", + }, + }); + + expect(withFileRemoved.files).toEqual({ "New.mo": "Hello there!" }); + expect(withFileRemoved.selectedFile).toBe("New.mo"); + }); + + it("renames files", () => { + const withRenamedFile = workplaceReducer.reduce(defaultState, { + type: "renameFile", + payload: { + path: "Main.mo", + newPath: "Renamed.mo", + }, + }); + + expect(withRenamedFile.files).toEqual({ "Renamed.mo": "" }); + expect(withRenamedFile.selectedFile).toBe("Renamed.mo"); + }); +}); diff --git a/src/contexts/WorkplaceState.ts b/src/contexts/WorkplaceState.ts index af4297fe..c07d290c 100644 --- a/src/contexts/WorkplaceState.ts +++ b/src/contexts/WorkplaceState.ts @@ -208,9 +208,11 @@ export const workplaceReducer = { }; case "deleteCanister": { const name = action.payload.name; - delete state.canisters[name]; + const canisters = { ...state.canisters }; + delete canisters[name]; return { ...state, + canisters, selectedCanister: state.selectedCanister === name ? null : state.selectedCanister, }; @@ -236,7 +238,7 @@ export const workplaceReducer = { selectedFile: state.selectedFile === action.payload.path ? action.payload.newPath - : state.selectedFile, + : state.selectedFile ?? selectFirstFile(files), }; } case "deleteFile": { @@ -250,8 +252,8 @@ export const workplaceReducer = { files, selectedFile: state.selectedFile === action.payload.path - ? selectFirstFile(state.files) - : state.selectedFile, + ? selectFirstFile(files) + : state.selectedFile ?? selectFirstFile(files), }; } case "deployWorkplace": { From 52a5eae9ed964994787ae389b7b90ac10fd5e6ea Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 16:44:38 -0700 Subject: [PATCH 05/14] install dependencies before test --- .github/workflows/frontend.yml | 56 +++++++++++++++++++++++++--------- package.json | 2 +- src/config/bootstrap-ci.sh | 27 ++++++++++++++++ src/config/jest.config.js | 12 ++++++++ src/config/vessel-install.sh | 16 ++++++++++ 5 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 src/config/bootstrap-ci.sh create mode 100644 src/config/jest.config.js create mode 100755 src/config/vessel-install.sh diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 29972ce3..0adb927b 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -1,27 +1,53 @@ # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions -name: Test Frontend +name: Frontend -on: [push, pull_request] +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the main branch +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] +# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: - + # The type of runner that the job will run on runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [14.x, 16.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - + # environment variables to set for dfx + env: + DFX_VERSION: 0.8.0 steps: - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} + - name: Setup Node.js environment uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm ci - - run: npm run build --if-present - - run: npm test \ No newline at end of file + node-version: "14" + - name: Install DFX and Vessel + run: | + echo y | DFX_VERSION=$DFX_VERSION bash -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)" + echo "/home/runner/bin" >> $GITHUB_PATH + ./src/config/vessel-install.sh + + - name: Bootstrap and Deploy + run: | + echo "vessel dependencies" + apt update && apt install -y python build-essential git + + echo "npm install from lockfile" + npm ci + + echo "/src/config/bootstrap-ci.sh" + # bootstrapping will be force-stopped if it takes too long + + timeout 1800 ./src/config/bootstrap-ci.sh + + - name: Test + run: npm run test + + - name: stop dfx + run: | + echo "dfx stop" + dfx stop diff --git a/package.json b/package.json index 0d89a4bc..cb9baae0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "scripts": { "start": "craco start", "build": "craco build", - "test": "craco test", + "test": "craco test --config=src/config/jest.config.js", "eject": "craco eject", "deploy": "dfx deploy --argument '(null)'", "prestart": "dfx start --background --no-artificial-delay || exit 0", diff --git a/src/config/bootstrap-ci.sh b/src/config/bootstrap-ci.sh new file mode 100644 index 00000000..170f078a --- /dev/null +++ b/src/config/bootstrap-ci.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e +echo "Running bootstrap script..." + +# Support bootstrapping CanCan from any folder +BASEDIR=$( + cd "$(dirname "$0")" + pwd +) +cd "$BASEDIR" + +host="localhost" +address="http://$host" +port=$(dfx config defaults.start.port) + +echo "dfx build" +npm run deploy + +URL="http://$(dfx canister id cancan_ui).$host:$port/" + +echo "Open at $URL" + +case $(uname) in +Linux) xdg-open $URL || true ;; +*) open $URL || true ;; +esac diff --git a/src/config/jest.config.js b/src/config/jest.config.js new file mode 100644 index 00000000..8f97cae1 --- /dev/null +++ b/src/config/jest.config.js @@ -0,0 +1,12 @@ +const generateAliases = require("./generateAliases"); +const aliases = generateAliases(); + +const moduleNameMapper = Object.entries(aliases).reduce( + (result, [key, value]) => { + result[`^${key}$`] = value; + return result; + }, + {} +); + +module.exports = { moduleNameMapper }; diff --git a/src/config/vessel-install.sh b/src/config/vessel-install.sh new file mode 100755 index 00000000..9c9a9244 --- /dev/null +++ b/src/config/vessel-install.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +BINDIR=$HOME/bin +mkdir -p $BINDIR +BIN=$BINDIR/vessel +OS_FILENAME=linux64 +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + OS_FILENAME=linux64 +elif [[ "$OSTYPE" == "darwin"* ]]; then + OS_FILENAME=macos +elif [[ "$OSTYPE" == "win32" ]]; then + OS_FILENAME=windows64.exe +fi + +VESSEL_VERSION=v0.6.0 +wget --output-document $BIN https://github.com/dfinity/vessel/releases/download/$VESSEL_VERSION/vessel-$OS_FILENAME && chmod +x $BIN && $BIN help From 40617640ba646b845df6cea94eb37581df64014b Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 16:46:50 -0700 Subject: [PATCH 06/14] chmod +x to bootstrap script --- src/config/bootstrap-ci.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/config/bootstrap-ci.sh diff --git a/src/config/bootstrap-ci.sh b/src/config/bootstrap-ci.sh old mode 100644 new mode 100755 From e6c83a55227439f309ce15bd61e6dd27b580c4d5 Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 16:51:03 -0700 Subject: [PATCH 07/14] add port to dfx config --- dfx.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dfx.json b/dfx.json index 55c8da9c..4413d057 100644 --- a/dfx.json +++ b/dfx.json @@ -28,6 +28,10 @@ "build": { "output": "build", "packtool": "vessel sources" + }, + "start": { + "address": "localhost", + "port": 8000 } }, "networks": { From 3089a640168e4cbb7d0207e4d37676643c0342be Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 17:11:45 -0700 Subject: [PATCH 08/14] remove bootstrap, simplify dfx steps --- .github/workflows/frontend.yml | 15 +++++++-------- src/config/actor.js | 1 + src/config/bootstrap-ci.sh | 27 --------------------------- 3 files changed, 8 insertions(+), 35 deletions(-) delete mode 100755 src/config/bootstrap-ci.sh diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 0adb927b..60b184ae 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -31,23 +31,22 @@ jobs: echo "/home/runner/bin" >> $GITHUB_PATH ./src/config/vessel-install.sh - - name: Bootstrap and Deploy + - name: DFX build steps run: | - echo "vessel dependencies" - apt update && apt install -y python build-essential git - echo "npm install from lockfile" npm ci - echo "/src/config/bootstrap-ci.sh" - # bootstrapping will be force-stopped if it takes too long + echo "start dfx" + npm run prestart - timeout 1800 ./src/config/bootstrap-ci.sh + echo "build for dfx artifacts" + dfx canister create --all + dfx build --all - name: Test run: npm run test - - name: stop dfx + - name: Stop DFX run: | echo "dfx stop" dfx stop diff --git a/src/config/actor.js b/src/config/actor.js index 721dee5f..d6cb8f07 100644 --- a/src/config/actor.js +++ b/src/config/actor.js @@ -68,6 +68,7 @@ export async function didToJs(source) { } const dataUri = "data:text/javascript;charset=utf-8," + encodeURIComponent(js[0]); + // eslint-disable-next-line no-eval const candid = await eval('import("' + dataUri + '")'); return candid; } diff --git a/src/config/bootstrap-ci.sh b/src/config/bootstrap-ci.sh deleted file mode 100755 index 170f078a..00000000 --- a/src/config/bootstrap-ci.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -e -echo "Running bootstrap script..." - -# Support bootstrapping CanCan from any folder -BASEDIR=$( - cd "$(dirname "$0")" - pwd -) -cd "$BASEDIR" - -host="localhost" -address="http://$host" -port=$(dfx config defaults.start.port) - -echo "dfx build" -npm run deploy - -URL="http://$(dfx canister id cancan_ui).$host:$port/" - -echo "Open at $URL" - -case $(uname) in -Linux) xdg-open $URL || true ;; -*) open $URL || true ;; -esac From 74c8eeeaf1f892e4f13e61389ccc371d3d449b66 Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 17:36:31 -0700 Subject: [PATCH 09/14] remove unused variable --- src/components/Editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 024f6f01..82356575 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useContext } from "react"; +import { useEffect, useContext } from "react"; import styled from "styled-components"; import MonacoEditor, { useMonaco } from "@monaco-editor/react"; import ReactMarkdown from "react-markdown"; From 2ca4ca84e5655b58b8bf06fc63fec9563fc47988 Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 17:40:48 -0700 Subject: [PATCH 10/14] add --no-wallet and skip jest.config --- .github/workflows/frontend.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 60b184ae..e4ce1d13 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -40,7 +40,7 @@ jobs: npm run prestart echo "build for dfx artifacts" - dfx canister create --all + dfx canister --no-wallet create --all dfx build --all - name: Test diff --git a/package.json b/package.json index cb9baae0..0d89a4bc 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "scripts": { "start": "craco start", "build": "craco build", - "test": "craco test --config=src/config/jest.config.js", + "test": "craco test", "eject": "craco eject", "deploy": "dfx deploy --argument '(null)'", "prestart": "dfx start --background --no-artificial-delay || exit 0", From 2815175daece581e38bb4fd507472f1739bb744c Mon Sep 17 00:00:00 2001 From: Taylor Ham Date: Mon, 18 Oct 2021 17:44:00 -0700 Subject: [PATCH 11/14] remove jest.config.js --- src/config/jest.config.js | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/config/jest.config.js diff --git a/src/config/jest.config.js b/src/config/jest.config.js deleted file mode 100644 index 8f97cae1..00000000 --- a/src/config/jest.config.js +++ /dev/null @@ -1,12 +0,0 @@ -const generateAliases = require("./generateAliases"); -const aliases = generateAliases(); - -const moduleNameMapper = Object.entries(aliases).reduce( - (result, [key, value]) => { - result[`^${key}$`] = value; - return result; - }, - {} -); - -module.exports = { moduleNameMapper }; From 9dd032567170f7281876190c2089626247e239fc Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Fri, 22 Oct 2021 13:27:26 -0700 Subject: [PATCH 12/14] add moc remove api --- src/App.tsx | 41 +++++++++++++++++++++------------- src/contexts/WorkplaceState.ts | 4 +++- src/workers/moc.ts | 9 ++++++++ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c474ab4a..e970975b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -178,22 +178,28 @@ export function App() { const deployWorkplace = (info: CanisterInfo) => { setForceUpdate(); - workplaceDispatch({ - type: "deployWorkplace", - payload: { - canister: info, + workplaceDispatch( + { + type: "deployWorkplace", + payload: { + canister: info, + }, }, - }); + worker + ); }; const importCode = useCallback( (files: Record) => { - workplaceDispatch({ - type: "loadProject", - payload: { - files, + workplaceDispatch( + { + type: "loadProject", + payload: { + files, + }, }, - }); + worker + ); }, [workplaceDispatch] ); @@ -209,13 +215,16 @@ export function App() { }; (async () => { await worker.fetchPackage(baseInfo); - await workplaceDispatch({ - type: "loadPackage", - payload: { - name: "base", - package: baseInfo, + await workplaceDispatch( + { + type: "loadPackage", + payload: { + name: "base", + package: baseInfo, + }, }, - }); + worker + ); logger.log("Base library loaded."); // fetch code after loading base library if (hasUrlParams) { diff --git a/src/contexts/WorkplaceState.ts b/src/contexts/WorkplaceState.ts index c07d290c..7cf5e389 100644 --- a/src/contexts/WorkplaceState.ts +++ b/src/contexts/WorkplaceState.ts @@ -177,7 +177,8 @@ export const workplaceReducer = { /** Return updated state based on an action */ reduce( state: WorkplaceState, - action: WorkplaceReducerAction + action: WorkplaceReducerAction, + worker ): WorkplaceState { switch (action.type) { case "loadProject": @@ -247,6 +248,7 @@ export const workplaceReducer = { }; delete files[action.payload.path]; + worker.Moc({ type: "remove", file: action.payload.path }); return { ...state, files, diff --git a/src/workers/moc.ts b/src/workers/moc.ts index fd98de27..cc11ad02 100644 --- a/src/workers/moc.ts +++ b/src/workers/moc.ts @@ -8,6 +8,9 @@ declare var Motoko: any; export type MocAction = | { type: "save"; file: string; content: string } + | { type: "remove"; file: string } + | { type: "rename"; old: string; new: string } + | { type: "ls"; path: string } | { type: "check"; file: string } | { type: "compile"; file: string } | { type: "candid"; file: string } @@ -20,6 +23,12 @@ export function Moc(action: MocAction) { switch (action.type) { case "save": return Motoko.saveFile(action.file, action.content); + case "remove": + return Motoko.removeFile(action.file); + case "rename": + return Motoko.renameFile(action.old, action.new); + case "ls": + return Motoko.readDir(action.file); case "check": return Motoko.check(action.file); case "compile": From 2d0770951ff0245cf91cd8a8baa7807968fe5c3a Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Fri, 22 Oct 2021 15:30:38 -0700 Subject: [PATCH 13/14] revert --- src/App.tsx | 41 +++++++++++++--------------------- src/contexts/WorkplaceState.ts | 4 +--- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e970975b..c474ab4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -178,28 +178,22 @@ export function App() { const deployWorkplace = (info: CanisterInfo) => { setForceUpdate(); - workplaceDispatch( - { - type: "deployWorkplace", - payload: { - canister: info, - }, + workplaceDispatch({ + type: "deployWorkplace", + payload: { + canister: info, }, - worker - ); + }); }; const importCode = useCallback( (files: Record) => { - workplaceDispatch( - { - type: "loadProject", - payload: { - files, - }, + workplaceDispatch({ + type: "loadProject", + payload: { + files, }, - worker - ); + }); }, [workplaceDispatch] ); @@ -215,16 +209,13 @@ export function App() { }; (async () => { await worker.fetchPackage(baseInfo); - await workplaceDispatch( - { - type: "loadPackage", - payload: { - name: "base", - package: baseInfo, - }, + await workplaceDispatch({ + type: "loadPackage", + payload: { + name: "base", + package: baseInfo, }, - worker - ); + }); logger.log("Base library loaded."); // fetch code after loading base library if (hasUrlParams) { diff --git a/src/contexts/WorkplaceState.ts b/src/contexts/WorkplaceState.ts index 7cf5e389..c07d290c 100644 --- a/src/contexts/WorkplaceState.ts +++ b/src/contexts/WorkplaceState.ts @@ -177,8 +177,7 @@ export const workplaceReducer = { /** Return updated state based on an action */ reduce( state: WorkplaceState, - action: WorkplaceReducerAction, - worker + action: WorkplaceReducerAction ): WorkplaceState { switch (action.type) { case "loadProject": @@ -248,7 +247,6 @@ export const workplaceReducer = { }; delete files[action.payload.path]; - worker.Moc({ type: "remove", file: action.payload.path }); return { ...state, files, From 3ac33132d6fbc419efab0f2eceb0b32f2cf4f9c1 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Fri, 22 Oct 2021 15:50:17 -0700 Subject: [PATCH 14/14] fix --- src/components/Explorer.tsx | 3 +++ src/components/FileModal.tsx | 1 + 2 files changed, 4 insertions(+) diff --git a/src/components/Explorer.tsx b/src/components/Explorer.tsx index 9bf3dc5d..391f1339 100644 --- a/src/components/Explorer.tsx +++ b/src/components/Explorer.tsx @@ -3,6 +3,7 @@ import styled from "styled-components"; import { WorkplaceState, WorkplaceDispatchContext, + WorkerContext, } from "../contexts/WorkplaceState"; import { ListButton } from "./shared/SelectList"; @@ -74,6 +75,7 @@ export function Explorer({ state, ttl, logger, deploySetter }: ExplorerProps) { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [fileToModify, setFileToModify] = useState(""); const dispatch = useContext(WorkplaceDispatchContext); + const worker = useContext(WorkerContext); function handleRenameClick(e, selectedFile: string) { e.stopPropagation(); @@ -110,6 +112,7 @@ export function Explorer({ state, ttl, logger, deploySetter }: ExplorerProps) { path: fileToModify, }, }); + worker.Moc({ type: "remove", file: fileToModify }); setFileToModify(""); } diff --git a/src/components/FileModal.tsx b/src/components/FileModal.tsx index 69ef798a..f262b072 100644 --- a/src/components/FileModal.tsx +++ b/src/components/FileModal.tsx @@ -60,6 +60,7 @@ export function FileModal({ isOpen, close, filename: initialFilename = "" }) { type: "renameFile", payload: { path: initialFilename, newPath: name }, }); + await worker.Moc({ type: "rename", old: initialFilename, new: name }); await dispatch({ type: "selectFile", payload: { path: name } }); } close();