Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shutterbug Support #2420

Merged
merged 5 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ To enable per component debugging set the "debug" localstorage key with one or m

- `bookmarks` this will show a tiny text status above the bookmark indicating which users have bookmarked this document. It will also print information about the document bookmarks each time a bookmark is toggled.
- `canvas` this will show the document key over the canvas, useful for looking up documents in Firebase
- `cms` this will print info to the console as changes are made to authored content via the CMS
- `iframe` this will print info to the console as document changes are sent from the iframe'd CLUE to the parent frame. This iframe'd CLUE is used by the CMS.
- `docList` - this will print a table of information about a list of documents
- `document` this will add the active document as `window.currentDocument`, you can use MST's hidden toJSON() like `currentDocument.toJSON()` to views its content.
- `drop` console log the dataTransfer object from drop events on the document.
Expand Down Expand Up @@ -174,11 +174,13 @@ The `firebase`, `firestore`, and `functions` params can take an `emulator` value

There is an alternative entry point for CLUE available at `/editor/`. This can be used to save and open individual documents from the local file system. Remote documents can be loaded into this editor with the `document` URL parameter. The editor requires a `unit` parameter to configure the toolbar. It can load an exported document content which is typical for section documents. It can also load a raw document content which is the same format that is stored in Firebase. It will save in the same format that was loaded.

By default the editor will save the current document to the browser's session storage. When editor is reloaded this same document will be loaded in. If you make a new tab and visit the editor this document won't be there anymore because it is in session storage. There is a "reset doc" button which clears the storage and reloads the page. You can also use the `noStorage` parameter to prevent it from loading or saving to session storage.
The `noStorage` parameter can override the default behavior. By default the editor will save the current document to the browser's session storage. When editor is reloaded this same document will be loaded in. If you make a new tab and visit the editor this document won't be there anymore because it is in session storage. There is a "reset doc" button which clears the storage and reloads the page. The `noStorage` parameter will prevent it from loading or saving to session storage.

The `document` parameter is useful if you want to work on something that requires a document in a specific state. You can just reload the page and get back to this state. You can use this locally by creating an initial document in doc-editor.html, and save the file to `src/public/[filename]`. Now you can load the document back with the parameter `document=[filename]`. This works because the document parameter will load URLs relative to the current page in the browser. This approach can also be used in Cypress tests. It would mean the test could just load in a document to test instead of having to setup the document first.
The `document` parameter is useful if you want to work on something that requires a document in a specific state. You can just reload the page and get back to this state. You can use this locally by creating an initial document in `/editor/`, and save the file to `src/public/[filename]`. Now you can load the document back with the parameter `document=[filename]`. This works because the document parameter will load URLs relative to the current page in the browser. This approach can also be used in Cypress tests. It would mean the test could just load in a document to test instead of having to setup the document first.

The Standalone Document Editor also supports a `readOnly` url param. If you specify this param the document you open will be opened in readOnly mode. This is useful for testing or debugging issues with tiles that have different displays when in readOnly model.
The `readOnly` parameter will open the document in readOnly mode. This is useful for testing or debugging issues with tiles that have different displays when in readOnly model.

The `unwrapped` parameter will open the document without any surrounding UI. This is useful for taking screenshots of documents.

### QA

Expand Down
4 changes: 2 additions & 2 deletions cms/src/cms-url-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export interface QueryParams {
curriculumBranch?: string;
// work with a local checkout of the curriculum instead of github
localCMSBackend?: boolean;
// change the location of the cms-editor.html used by iframe widget to edit
// change the location of the iframe.html used by the iframe widget to edit
// CLUE documents.
cmsEditorBase?: string;
iframeBase?: string;
}

export const processUrlParams = (): QueryParams => {
Expand Down
16 changes: 8 additions & 8 deletions cms/src/iframe-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { Map } from "immutable";
import { CmsWidgetControlProps } from "decap-cms-core";

import { urlParams } from "./cms-url-params";
import { DEBUG_CMS } from "../../src/lib/debug";
import { DEBUG_IFRAME } from "../../src/lib/debug";
import { defaultCurriculumBranch } from "./cms-constants";

import "./iframe-control.scss";

(window as any).DISABLE_FIREBASE_SYNC = true;

const cmsEditorBase = urlParams.cmsEditorBase ?? ".";
const iframeBase = urlParams.iframeBase ?? ".";
// the URL is relative to the current url of the CMS
// If the cmsEditorBase is an absolute url then the current url will be ignored
const cmsEditorBaseURL = new URL(cmsEditorBase, window.location.href);
const validOrigin = cmsEditorBaseURL.origin;
// If the iframeBase is an absolute url then the current url will be ignored
const iframeBaseURL = new URL(iframeBase, window.location.href);
const validOrigin = iframeBaseURL.origin;

interface IState {
initialValue?: string;
Expand All @@ -28,7 +28,7 @@ export class IframeControl extends React.Component<CmsWidgetControlProps, IState
}

componentDidMount = () => {
if (DEBUG_CMS) {
if (DEBUG_IFRAME) {
// eslint-disable-next-line no-console
console.log("DEBUG: CMS ClueControl initial content value is: ", this.state.initialValue);
}
Expand Down Expand Up @@ -66,13 +66,13 @@ export class IframeControl extends React.Component<CmsWidgetControlProps, IState

render() {
const curriculumBranch = urlParams.curriculumBranch ?? defaultCurriculumBranch;
const iframeBaseUrl = `${cmsEditorBase}/cms-editor.html?curriculumBranch=${curriculumBranch}`;
const iframeBaseUrl = `${iframeBase}/iframe.html?curriculumBranch=${curriculumBranch}`;
const iframeUrl = urlParams.unit
? `${iframeBaseUrl}&unit=${urlParams.unit}`
: iframeBaseUrl;
return (
<div className="iframe-control custom-widget">
<iframe id="editor" src={iframeUrl} allow="clipboard-read; clipboard-write"
<iframe id="editor" src={iframeUrl} allow="clipboard-read; clipboard-write; serial"
onLoad={this.sendInitialValueToEditor.bind(this)}>
</iframe>
</div>
Expand Down
12 changes: 6 additions & 6 deletions docs/cms.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ CLUE includes a CMS which can be accessed at `/admin.html` in a production build

- **`curriculumBranch`** By default the CMS edits the `author` branch. You can change the branch by passing a different branch name to this parameter. The CMS will not create this branch for you. You'll need to create it yourself.
- **`unit`** By default the CMS displays all of the units at the same time. It is better to work with a single unit at a time by using the `unit` param. It should be passed the unit code. This limits what is displayed in the CMS and it also configures the media library to show the images from that unit.
- **`cmsEditorBase`** By default the CMS will use an iframe pointed at `./cms-editor.html` to edit the CLUE documents. You can use this param to replace `.` with something else. This is useful for local testing (see below)
- **`iframeBase`** By default the CMS will use an iframe pointed at `./iframe.html` to edit the CLUE documents. You can use this param to replace `.` with something else. This is useful for local testing (see below)
- **`localCMSBackend`** By default the CMS works with the `github` backend. This means even when doing local CLUE development the CMS will update the `clue-curriculum` repository directly. If you add the `localCMSBackend` parameter the CMS will attempt to work with a local git proxy running at localhost:8081. You can start this proxy with:

`cd ../clue-curriculum; npx netlify-cms-proxy-server`
Expand Down Expand Up @@ -57,18 +57,18 @@ This document editor is located in `/src/cms/`

To work on the CMS locally you'll need to start both CLUE and the CMS:

- start CLUE by running `npm run start` in the top level folder
- start CLUE by running `npm start` in the top level folder
- if running the local Git proxy, start it next so it gets port 8081
- in a new terminal, open the `/cms` folder
- make sure its dependencies are installed: `npm i`
- start the CMS by running `npm run start`
- open the CMS with `http://localhost:[cms_port]/?cmsEditorBase=http://localhost:[clue_port]&unit=[clue_unit_code]&curriculumBranch=[your own branch]`
- start the CMS by running `npm start`
- open the CMS with `http://localhost:[cms_port]/?iframeBase=http://localhost:[clue_port]&unit=[clue_unit_code]&curriculumBranch=[your own branch]`
(add `&localCMSBackend` if using it). Make sure there are no extra "#" or "/" characters in the URL.

Typically CLUE will be running on portal 8080 and the CMS will be running on 8081, or CLUE on 8080, Git proxy on 8081, and CMS on 8082. In this case the url above would be:

- No proxy: `http://localhost:8081/?cmsEditorBase=http://localhost:8080&unit=[clue_unit_code]&curriculumBranch=[your own branch]`
- With proxy: `http://localhost:8082/?cmsEditorBase=http://localhost:8080&localCMSBackend&unit=[clue_unit_code]`
- No proxy: `http://localhost:8081/?iframeBase=http://localhost:8080&unit=[clue_unit_code]&curriculumBranch=[your own branch]`
- With proxy: `http://localhost:8082/?iframeBase=http://localhost:8080&localCMSBackend&unit=[clue_unit_code]`

With this approach you'll be editing the content in the `clue-curriculum` repository directly. By default this will use the `author` branch in the `clue-curriculum` repository. So you aren't making changes to the same branch as other people, you should make your own branch in that repository and pass it to the `curriculumBranch` parameter. You have to make this branch using your own git tools, the CMS cannot create branches itself.

Expand Down
18 changes: 7 additions & 11 deletions scripts/ai/document-screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const fileBatchSize = 8;

// The width of the browser window. The height is determined dynamically.
const windowWidth = 1920 / 2;

// The actual height is based on the content
const windowHeight = 540;
const publicRoot = "ai";
const rootPath = `../../src/public/${publicRoot}`;
const documentPath = `${rootPath}/${documentDirectory}`;
Expand Down Expand Up @@ -58,13 +59,13 @@ function newFileName(oldFileName: string) {

// makeSnapshot loads document content at path in a CLUE standalone document editor, takes a snapshot of it,
// then saves it in the output directory as fileName
const urlRoot = `http://localhost:8080/editor/?appMode=dev&unit=example&document=`;
const urlRoot = `http://localhost:8080/editor/?appMode=dev&unit=example&readOnly&unwrapped&document=`;
async function makeSnapshot(path: string, fileName: string) {
console.log(`* Processing snapshot`, path);
const targetFile = `${targetPath}/${fileName}`;

// View the document in the document editor
const browser = await puppeteer.launch({ headless: "new" });
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const url = `${urlRoot}${path}`;
try {
Expand All @@ -80,14 +81,9 @@ async function makeSnapshot(path: string, fileName: string) {
return;
}

// Approximate the height of the document by adding up the heights of the rows and make the viewport that tall
let pageHeight = 30;
const rowElements = await page.$$(".tile-row");
for (const rowElement of rowElements) {
const boundingBox = await rowElement.boundingBox();
pageHeight += boundingBox?.height ?? 0;
}
await page.setViewport({ width: windowWidth, height: Math.round(pageHeight) });
// The actual height is based on the content because of the `fullPage` option
// passed to `page.screenshot`
await page.setViewport({ width: windowWidth, height: windowHeight });

// Take a screenshot and save it to a file
const buffer = await page.screenshot({ fullPage: true, type: 'png' });
Expand Down
74 changes: 74 additions & 0 deletions scripts/shutterbug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Example executions:
//
// Generate image and print out the url of the image:
// npx tsx shutterbug.ts /Users/scytacki/Development/ai/dataset1720819925834-mods/documents/document-NePawLNjq3wEjk58TiW.txt
//
// Generate shutterbug.html for checking page locally:
// npx tsx shutterbug.ts /Users/scytacki/Development/ai/dataset1720819925834-mods/documents/document-NePawLNjq3wEjk58TiW.txt html

import fs from "fs";

const clueCodebase = "https://collaborative-learning.concord.org/branch/shutterbug-support";
// const clueCodebase = "http://localhost:8080";

function generateHtml(clueDocument: any) {
return `
<script>const initialValue=${JSON.stringify(clueDocument)}</script>
<!-- height will be updated when iframe sends updateHeight message -->
<iframe id='clue-frame' width='100%' height='500px' style='border:0px'
allow='serial'
src='${clueCodebase}/iframe.html?unwrapped&readOnly'
></iframe>
<script>
const clueFrame = document.getElementById('clue-frame')
function sendInitialValueToEditor() {
if (!clueFrame.contentWindow) {
console.warning("iframe doesn't have contentWindow");
}

window.addEventListener("message", (event) => {
if (event.data.type === "updateHeight") {
document.getElementById("clue-frame").height = event.data.height + "px";
}
})

clueFrame.contentWindow.postMessage(
{ initialValue: JSON.stringify(initialValue) },
"*"
);
}
clueFrame.addEventListener('load', sendInitialValueToEditor);
</script>
`;
}

export async function postToShutterbug(body: any) {
const fetchURL = "https://api.concord.org/shutterbug-production";
console.log("Fetching", fetchURL);
const response = await fetch(fetchURL,
{
method: "POST",
body: JSON.stringify(body)
}
);
const json = await response.json();
console.log(json);
}

const fileName = process.argv[2];
const outputHtml = process.argv[3];

const documentString = fs.readFileSync(fileName, "utf8");
const docObject = JSON.parse(documentString);
const html = generateHtml(docObject);

if (outputHtml) {
fs.writeFileSync("shutterbug.html", html);
} else {
postToShutterbug({content: html, height: 1500});
}
//

// Note: you can also change the `.png` to `.html` on the end of the URL returned by shutterbug.
// This will give you the actual html that shutterbug sent to its internal browser

29 changes: 21 additions & 8 deletions src/components/doc-editor/doc-editor-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
// These are defined at the module level so the initialDoc can be used in two useState
// initializers. The initialDoc should only change when the page is loaded so this is
// safe.
const {document: documentURL, readOnly, noStorage } = urlParams;
const {document: documentURL, readOnly, noStorage, unwrapped } = urlParams;
const savedDocString = noStorage ? undefined : window.sessionStorage.getItem(kDocEditorDocKey);
const initialDoc = savedDocString ? JSON.parse(savedDocString) : defaultDocumentModel;

Expand Down Expand Up @@ -195,6 +195,19 @@
});
}, [loadDocument]);

if (unwrapped) {
// Let the window do the scrolling. If the body scrolls and shows the full content
// then puppeteer's screenshot function can capture the full content easily.
window.document.body.style.overflow = "visible";
return (

Check warning on line 202 in src/components/doc-editor/doc-editor-app.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/doc-editor/doc-editor-app.tsx#L201-L202

Added lines #L201 - L202 were not covered by tests
<CanvasComponent
document={document}
context="doc-editor-read-only"
readOnly={!!readOnly}
/>
);
}

// This is wrapped in a div.primary-workspace so it can be used with cypress
// tests that are looking for stuff in a div like this
return (
Expand All @@ -213,13 +226,13 @@
</div>
</div>
<EditableDocumentContent
contained={false}
mode="1-up"
isPrimary={true}
readOnly={readOnly}
document={document}
toolbar={appConfig.authorToolbar}
/>
contained={false}
mode="1-up"
isPrimary={true}
readOnly={readOnly}
document={document}
toolbar={appConfig.authorToolbar}
/>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/doc-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ stores.unitLoadedPromise.then(() => {
<AppProvider stores={stores} modalAppElement="#app">
<DocEditorApp/>
<DialogComponent/>
</AppProvider>,
</AppProvider>
</ChakraProvider>,
document.getElementById("app")
);
Expand Down
Loading
Loading