From 9b6c9f6be7d958af184ff94951a9c98f12fddbd4 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Sat, 29 Jun 2024 14:52:16 -0400 Subject: [PATCH 1/3] save project as gist --- gui/src/app/pages/HomePage/ExportWindow.tsx | 73 ------ gui/src/app/pages/HomePage/LeftPanel.tsx | 44 ++-- ...ImportWindow.tsx => LoadProjectWindow.tsx} | 14 +- .../app/pages/HomePage/SaveProjectWindow.tsx | 208 ++++++++++++++++++ .../app/pages/HomePage/saveAsGitHubGist.ts | 74 +++++++ 5 files changed, 312 insertions(+), 101 deletions(-) delete mode 100644 gui/src/app/pages/HomePage/ExportWindow.tsx rename gui/src/app/pages/HomePage/{ImportWindow.tsx => LoadProjectWindow.tsx} (94%) create mode 100644 gui/src/app/pages/HomePage/SaveProjectWindow.tsx create mode 100644 gui/src/app/pages/HomePage/saveAsGitHubGist.ts diff --git a/gui/src/app/pages/HomePage/ExportWindow.tsx b/gui/src/app/pages/HomePage/ExportWindow.tsx deleted file mode 100644 index 6d50e7d7..00000000 --- a/gui/src/app/pages/HomePage/ExportWindow.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { FunctionComponent, useContext } from "react"; - -import { mapModelToFileManifest } from "../../Project/FileMapping"; -import { ProjectContext } from "../../Project/ProjectContextProvider"; -import { serializeAsZip } from "../../Project/ProjectSerialization"; -import { triggerDownload } from "../../util/triggerDownload"; - -type ExportWindowProps = { - onClose: () => void; -}; - -const ExportWindow: FunctionComponent = ({ onClose }) => { - const { data, update } = useContext(ProjectContext); - const fileManifest = mapModelToFileManifest(data); - - return ( -
-

Export this project

- - - - - - - {Object.entries(fileManifest).map(([name, content], i) => ( - - - - - ))} - -
Title - - update({ type: "retitle", title: newTitle }) - } - /> -
{name}{content.length} bytes
-
- -
-
- ); -}; - -type EditTitleComponentProps = { - value: string; - onChange: (value: string) => void; -}; - -const EditTitleComponent: FunctionComponent = ({ - value, - onChange, -}) => { - return ( - onChange(e.target.value)} - /> - ); -}; - -export default ExportWindow; diff --git a/gui/src/app/pages/HomePage/LeftPanel.tsx b/gui/src/app/pages/HomePage/LeftPanel.tsx index ec3adc99..4ad7c1b4 100644 --- a/gui/src/app/pages/HomePage/LeftPanel.tsx +++ b/gui/src/app/pages/HomePage/LeftPanel.tsx @@ -3,8 +3,8 @@ import ModalWindow, { useModalWindow } from "@fi-sci/modal-window"; import { FunctionComponent, useCallback, useContext } from "react"; import examplesStanies, { Stanie } from "../../exampleStanies/exampleStanies"; import { ProjectContext } from "../../Project/ProjectContextProvider"; -import ExportWindow from "./ExportWindow"; -import ImportWindow from "./ImportWindow"; +import SaveProjectWindow from "./SaveProjectWindow"; +import LoadProjectWindow from "./LoadProjectWindow"; import { ChevronLeft, ChevronRight } from "@mui/icons-material"; type LeftPanelProps = { @@ -33,14 +33,14 @@ const LeftPanel: FunctionComponent = ({ ); const { - visible: exportVisible, - handleOpen: exportOpen, - handleClose: exportClose, + visible: saveProjectVisible, + handleOpen: saveProjectOpen, + handleClose: saveProjectClose, } = useModalWindow(); const { - visible: importVisible, - handleOpen: importOpen, - handleClose: importClose, + visible: loadProjectVisible, + handleOpen: loadProjectOpen, + handleClose: loadProjectClose, } = useModalWindow(); if (collapsed) { @@ -82,6 +82,16 @@ const LeftPanel: FunctionComponent = ({ ))}
+
+ +   + +
+
 
{/* This will probably be removed or replaced in the future. It's just for convenience during development. */}
-
 
-
- -   - -
- - + + - - + + ); diff --git a/gui/src/app/pages/HomePage/ImportWindow.tsx b/gui/src/app/pages/HomePage/LoadProjectWindow.tsx similarity index 94% rename from gui/src/app/pages/HomePage/ImportWindow.tsx rename to gui/src/app/pages/HomePage/LoadProjectWindow.tsx index ed7d808e..fc31ec11 100644 --- a/gui/src/app/pages/HomePage/ImportWindow.tsx +++ b/gui/src/app/pages/HomePage/LoadProjectWindow.tsx @@ -18,11 +18,13 @@ import { } from "../../Project/ProjectSerialization"; import UploadFilesArea from "./UploadFilesArea"; -type ImportWindowProps = { +type LoadProjectWindowProps = { onClose: () => void; }; -const ImportWindow: FunctionComponent = ({ onClose }) => { +const LoadProjectWindow: FunctionComponent = ({ + onClose, +}) => { const { update } = useContext(ProjectContext); const [errorText, setErrorText] = useState(null); const [filesUploaded, setFilesUploaded] = useState< @@ -105,7 +107,7 @@ const ImportWindow: FunctionComponent = ({ onClose }) => { return (
-

Import project

+

Load project

You can upload:
    @@ -132,13 +134,13 @@ const ImportWindow: FunctionComponent = ({ onClose }) => { {showReplaceProjectOptions && (
     
    )} @@ -146,4 +148,4 @@ const ImportWindow: FunctionComponent = ({ onClose }) => { ); }; -export default ImportWindow; +export default LoadProjectWindow; diff --git a/gui/src/app/pages/HomePage/SaveProjectWindow.tsx b/gui/src/app/pages/HomePage/SaveProjectWindow.tsx new file mode 100644 index 00000000..2719a9c1 --- /dev/null +++ b/gui/src/app/pages/HomePage/SaveProjectWindow.tsx @@ -0,0 +1,208 @@ +import { FunctionComponent, useCallback, useContext, useState } from "react"; + +import { + FileRegistry, + mapModelToFileManifest, +} from "../../Project/FileMapping"; +import { ProjectContext } from "../../Project/ProjectContextProvider"; +import { serializeAsZip } from "../../Project/ProjectSerialization"; +import { triggerDownload } from "../../util/triggerDownload"; +import saveAsGitHubGist from "./saveAsGitHubGist"; + +type SaveProjectWindowProps = { + onClose: () => void; +}; + +const SaveProjectWindow: FunctionComponent = ({ + onClose, +}) => { + const { data, update } = useContext(ProjectContext); + const fileManifest = mapModelToFileManifest(data); + + const [exportingToGist, setExportingToGist] = useState(false); + + return ( +
    +

    Save this project

    + + + + + + + {Object.entries(fileManifest).map(([name, content], i) => ( + + + + + ))} + +
    Title + + update({ type: "retitle", title: newTitle }) + } + /> +
    {name}{content.length} bytes
    +
     
    + {!exportingToGist && ( +
    + +   + +
    + )} + {exportingToGist && ( + + )} +
    + ); +}; + +type EditTitleComponentProps = { + value: string; + onChange: (value: string) => void; +}; + +const EditTitleComponent: FunctionComponent = ({ + value, + onChange, +}) => { + return ( + onChange(e.target.value)} + /> + ); +}; + +type GistExportViewProps = { + fileManifest: Partial; + title: string; + onClose: () => void; +}; + +const GistExportView: FunctionComponent = ({ + fileManifest, + title, + onClose, +}) => { + const [gitHubPersonalAccessToken, setGitHubPersonalAccessToken] = + useState(""); + const [gistUrl, setGistUrl] = useState(null); + + const handleExport = useCallback(async () => { + try { + const gistUrl = await saveAsGitHubGist(fileManifest, { + defaultDescription: title, + personalAccessToken: gitHubPersonalAccessToken, + }); + setGistUrl(gistUrl); + } catch (err: any) { + alert(`Error saving to GitHub Gist: ${err.message}`); + } + }, [gitHubPersonalAccessToken, fileManifest, title]); + + return ( +
    +

    Save to GitHub Gist

    +

    + In order to save this project as a GitHub Gist, you will need to provide + a GitHub Personal Access Token.  This token will be used to + authenticate with GitHub and create a new Gist with the files in this + project.  You can create a new Personal Access Token by visiting + your{" "} + + GitHub settings + + .  Go to Developer settings and Tokens (classic) + .  Generate a new classic token and be sure to only grant gist + scope with an expiration date.  Copy the token and paste it into + the field below. +

    +

    + For security reasons, your token will not be saved in this + application,  so you may want to store it securely in a text file + for future use. +

    + + + + + + + +
    GitHub Personal Access Token + setGitHubPersonalAccessToken(e.target.value)} + /> +
    +
     
    + {!gistUrl && ( +
    + +   + +
    + )} + {gistUrl && ( +
    +

    + Successfully saved to GitHub Gist:  + + {gistUrl} + +

    +

    + You can now share the following link to this Stan Playground + project:  +
    +
    + + {makeSPShareableLinkFromGistUrl(gistUrl)} + +
    +

    + +
    + )} +
    + ); +}; + +const makeSPShareableLinkFromGistUrl = (gistUrl: string) => { + const protocol = window.location.protocol; + const host = window.location.host; + const url = `${protocol}//${host}?project=${gistUrl}`; + return url; +}; + +export default SaveProjectWindow; diff --git a/gui/src/app/pages/HomePage/saveAsGitHubGist.ts b/gui/src/app/pages/HomePage/saveAsGitHubGist.ts new file mode 100644 index 00000000..ec672db0 --- /dev/null +++ b/gui/src/app/pages/HomePage/saveAsGitHubGist.ts @@ -0,0 +1,74 @@ +import { Octokit } from "@octokit/core"; + +const saveAsGitHubGist = async ( + files: { [key: string]: string }, + o: { defaultDescription: string; personalAccessToken: string }, +) => { + const { defaultDescription, personalAccessToken } = o; + const description = prompt( + "SAVING AS PUBLIC GIST: Enter a description:", + defaultDescription, + ); + if (!description) { + throw Error("No description provided"); + } + const octokit = new Octokit({ + auth: personalAccessToken, + }); + const files2: { [key: string]: { content: string } } = {}; + for (const key in files) { + // gists do not support empty files or whitespace-only files + if (files[key].trim() === "") { + console.warn(`File ${key} is empty or whitespace-only. Not saving.`); + } else { + files2[key] = { content: files[key] }; + } + } + const r = await octokit.request("POST /gists", { + description, + public: true, + files: files2, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + // const gistId = r.data.id; + const gistUrl = r.data.html_url; + if (!gistUrl) { + throw new Error("Problem creating gist. r.data.html_url is null."); + } + return gistUrl; +}; + +export const updateGitHubGist = async ( + gistUri: string, + patch: { [path: string]: string | null }, + o: { personalAccessToken: string }, +) => { + const octokit = new Octokit({ + auth: o.personalAccessToken, + }); + const gistId = gistUri.split("/").pop(); + if (!gistId) { + throw new Error("Invalid gist URI"); + } + // patch + const files: { [key: string]: { content?: string } } = {}; + for (const path in patch) { + const content = patch[path]; + if (content === null || content.trim() === "") { + files[path] = {}; + } else { + files[path] = { content }; + } + } + await octokit.request(`PATCH /gists/${gistId}`, { + gist_id: gistId, + files, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + }); +}; + +export default saveAsGitHubGist; From 11f0c60474eb28a2d79c2c7456ace143e3083276 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Tue, 2 Jul 2024 13:33:30 -0400 Subject: [PATCH 2/3] improve saving to gists --- gui/src/app/Project/ProjectQueryLoading.ts | 2 +- .../{Project => gists}/loadFilesFromGist.ts | 0 .../HomePage => gists}/saveAsGitHubGist.ts | 0 .../app/pages/HomePage/SaveProjectWindow.tsx | 21 ++++++++++++------- 4 files changed, 14 insertions(+), 9 deletions(-) rename gui/src/app/{Project => gists}/loadFilesFromGist.ts (100%) rename gui/src/app/{pages/HomePage => gists}/saveAsGitHubGist.ts (100%) diff --git a/gui/src/app/Project/ProjectQueryLoading.ts b/gui/src/app/Project/ProjectQueryLoading.ts index f937b07d..998eb737 100644 --- a/gui/src/app/Project/ProjectQueryLoading.ts +++ b/gui/src/app/Project/ProjectQueryLoading.ts @@ -6,7 +6,7 @@ import { persistStateToEphemera, } from "./ProjectDataModel"; import { loadFromProjectFiles } from "./ProjectSerialization"; -import loadFilesFromGist from "./loadFilesFromGist"; +import loadFilesFromGist from "../gists/loadFilesFromGist"; enum QueryParamKeys { Project = "project", diff --git a/gui/src/app/Project/loadFilesFromGist.ts b/gui/src/app/gists/loadFilesFromGist.ts similarity index 100% rename from gui/src/app/Project/loadFilesFromGist.ts rename to gui/src/app/gists/loadFilesFromGist.ts diff --git a/gui/src/app/pages/HomePage/saveAsGitHubGist.ts b/gui/src/app/gists/saveAsGitHubGist.ts similarity index 100% rename from gui/src/app/pages/HomePage/saveAsGitHubGist.ts rename to gui/src/app/gists/saveAsGitHubGist.ts diff --git a/gui/src/app/pages/HomePage/SaveProjectWindow.tsx b/gui/src/app/pages/HomePage/SaveProjectWindow.tsx index 2719a9c1..dd05401d 100644 --- a/gui/src/app/pages/HomePage/SaveProjectWindow.tsx +++ b/gui/src/app/pages/HomePage/SaveProjectWindow.tsx @@ -7,7 +7,7 @@ import { import { ProjectContext } from "../../Project/ProjectContextProvider"; import { serializeAsZip } from "../../Project/ProjectSerialization"; import { triggerDownload } from "../../util/triggerDownload"; -import saveAsGitHubGist from "./saveAsGitHubGist"; +import saveAsGitHubGist from "../../gists/saveAsGitHubGist"; type SaveProjectWindowProps = { onClose: () => void; @@ -132,18 +132,23 @@ const GistExportView: FunctionComponent = ({ authenticate with GitHub and create a new Gist with the files in this project.  You can create a new Personal Access Token by visiting your{" "} - + GitHub settings - .  Go to Developer settings and Tokens (classic) - .  Generate a new classic token and be sure to only grant gist - scope with an expiration date.  Copy the token and paste it into - the field below. + .  Go to Fine-grained tokens and generate a new fine-grained + token. Be sure to only grant Gist read/write permission by using Gists + item in the Account Permissions section. You should also specify + an expiration date.  Copy the token and paste it into the field + below.

    For security reasons, your token will not be saved in this - application,  so you may want to store it securely in a text file - for future use. + application,  so you should store it securely in a text file for + future use.

    From 62f875cbc5798ad25a029d74afa4130884c55d4e Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Wed, 3 Jul 2024 11:52:38 -0400 Subject: [PATCH 3/3] tweaks for js review --- gui/src/app/gists/saveAsGitHubGist.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/gui/src/app/gists/saveAsGitHubGist.ts b/gui/src/app/gists/saveAsGitHubGist.ts index ec672db0..cae24c35 100644 --- a/gui/src/app/gists/saveAsGitHubGist.ts +++ b/gui/src/app/gists/saveAsGitHubGist.ts @@ -1,8 +1,13 @@ import { Octokit } from "@octokit/core"; +type SaveAsGitHubGistOpts = { + defaultDescription: string; + personalAccessToken: string; +}; + const saveAsGitHubGist = async ( files: { [key: string]: string }, - o: { defaultDescription: string; personalAccessToken: string }, + o: SaveAsGitHubGistOpts, ) => { const { defaultDescription, personalAccessToken } = o; const description = prompt( @@ -15,19 +20,19 @@ const saveAsGitHubGist = async ( const octokit = new Octokit({ auth: personalAccessToken, }); - const files2: { [key: string]: { content: string } } = {}; + const filesForGistExport: { [key: string]: { content: string } } = {}; for (const key in files) { // gists do not support empty files or whitespace-only files if (files[key].trim() === "") { console.warn(`File ${key} is empty or whitespace-only. Not saving.`); } else { - files2[key] = { content: files[key] }; + filesForGistExport[key] = { content: files[key] }; } } const r = await octokit.request("POST /gists", { description, public: true, - files: files2, + files: filesForGistExport, headers: { "X-GitHub-Api-Version": "2022-11-28", },