From 8c1b1197fab2d3358dfe4dd5f277ea6c1ab12df5 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 15 Nov 2023 12:32:06 -0500 Subject: [PATCH 1/9] Export interface and helper functions. --- package-lock.json | 11 + package.json | 1 + src/api/codap-helper.ts | 79 +++++ src/api/codap-interface.ts | 407 ++++++++++++++++++++++++++ src/assets/concord.png | Bin 15086 -> 0 bytes src/codap-plugin-api.ts | 2 + src/components/app.scss | 13 - src/components/app.tsx | 16 - src/components/text.test.tsx | 10 - src/components/text.tsx | 9 - src/hooks/use-sample-text.test.ts | 11 - src/hooks/use-sample-text.ts | 9 - src/index.html | 12 - src/index.scss | 9 - src/index.tsx | 11 - src/public/favicon.ico | Bin 15086 -> 0 bytes src/test/setupTests.ts | 1 - src/typings.d.ts | 1 + src/utils/translation/README.md | 45 --- src/utils/translation/lang/en-us.json | 4 - src/utils/translation/lang/es.json | 4 - src/utils/translation/translate.ts | 67 ----- 22 files changed, 501 insertions(+), 221 deletions(-) create mode 100644 src/api/codap-helper.ts create mode 100644 src/api/codap-interface.ts delete mode 100644 src/assets/concord.png create mode 100644 src/codap-plugin-api.ts delete mode 100644 src/components/app.scss delete mode 100644 src/components/app.tsx delete mode 100644 src/components/text.test.tsx delete mode 100644 src/components/text.tsx delete mode 100644 src/hooks/use-sample-text.test.ts delete mode 100644 src/hooks/use-sample-text.ts delete mode 100644 src/index.html delete mode 100644 src/index.scss delete mode 100644 src/index.tsx delete mode 100644 src/public/favicon.ico delete mode 100644 src/test/setupTests.ts create mode 100644 src/typings.d.ts delete mode 100644 src/utils/translation/README.md delete mode 100644 src/utils/translation/lang/en-us.json delete mode 100644 src/utils/translation/lang/es.json delete mode 100644 src/utils/translation/translate.ts diff --git a/package-lock.json b/package-lock.json index 861cf9b..693396c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "iframe-phone": "^1.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "tslib": "^2.5.2" @@ -9802,6 +9803,11 @@ } ] }, + "node_modules/iframe-phone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/iframe-phone/-/iframe-phone-1.3.1.tgz", + "integrity": "sha512-pipd9e4l5AE0K3+ZcQxb/+zd1pvCWbu6wuQlxdqChIfwe9jnnm2HwcNra/GvLovDxe36+uZusFMN0rNKlFIvdQ==" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -25384,6 +25390,11 @@ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true }, + "iframe-phone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/iframe-phone/-/iframe-phone-1.3.1.tgz", + "integrity": "sha512-pipd9e4l5AE0K3+ZcQxb/+zd1pvCWbu6wuQlxdqChIfwe9jnnm2HwcNra/GvLovDxe36+uZusFMN0rNKlFIvdQ==" + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", diff --git a/package.json b/package.json index 8bd4139..e2ac27f 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "webpack-dev-server": "^4.15.0" }, "dependencies": { + "iframe-phone": "^1.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "tslib": "^2.5.2" diff --git a/src/api/codap-helper.ts b/src/api/codap-helper.ts new file mode 100644 index 0000000..c1fdee4 --- /dev/null +++ b/src/api/codap-helper.ts @@ -0,0 +1,79 @@ +import {codapInterface} from "./codap-interface"; + +export interface IItem { + [key: string]: string|number; +} + +export function initializePlugin(pluginName: string, version: string, dimensions: {width: number, height: number}) { + const interfaceConfig = { + name: pluginName, + version: version, + dimensions: dimensions + }; + return codapInterface.init(interfaceConfig); +} + +const dataSetString = (contextName: string) => `dataContext[${contextName}]`; + +export function createDataContext(dataContextName: string) { + // Determine if CODAP already has the Data Context we need. + // If not, create it. + return codapInterface.sendRequest({ + action:'get', + resource: dataSetString(dataContextName) + }, function (result: { success: any; }) { + if (result && !result.success) { + codapInterface.sendRequest({ + action: 'create', + resource: 'dataContext', + values: { + name: dataContextName, + collections: [ + { + name: 'items', + labels: { + pluralCase: "items", + setOfCasesWithArticle: "an item" + }, + attrs: [{name: "value"}] + } + ] + } + }); + } + } + ); +} + +export function openTable() { + codapInterface.sendRequest({ + action: 'create', + resource: 'component', + values: { + type: 'caseTable' + } + }); +} + +export function addData(dataContextName: string, data: number[]) { + const values = data.map(d => ({value: d})); + codapInterface.sendRequest({ + action: 'create', + resource: `${dataSetString(dataContextName)}.item`, + values + }); +} + +export function createItem(dataContextName: string, item: IItem) { + codapInterface.sendRequest({ + action: 'create', + resource: `${dataSetString(dataContextName)}.item`, + values: item + }); +} + +export function createItems(dataContextName: string, items: IItem[]) { + items.forEach(item => { + createItem(dataContextName, item); + }) +} \ No newline at end of file diff --git a/src/api/codap-interface.ts b/src/api/codap-interface.ts new file mode 100644 index 0000000..a50b4af --- /dev/null +++ b/src/api/codap-interface.ts @@ -0,0 +1,407 @@ +// ========================================================================== +// +// Author: jsandoe +// +// Copyright (c) 2016 by The Concord Consortium, Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========================================================================== + +/** + * This class is intended to provide an abstraction layer for managing + * a CODAP Data Interactive's connection with CODAP. It is not required. It is + * certainly possible for a data interactive, for example, to use only the + * iFramePhone library, which manages the connection at a lower level. + * + * This object provides the following services: + * 1. Initiates the iFramePhone interaction with CODAP. + * 2. Provides information on the status of the connection. + * 3. Provides a sendRequest method. It accepts a callback or returns a Promise + * for handling the results from CODAP. + * 4. Provides a subscriber interface to receive selected notifications from + * CODAP. + * 5. Provides automatic handling of Data Interactive State. Prior to saving + * a document CODAP requests state from the Data Interactive, where state + * is an arbitrary serializable object containing whatever the data + * interactive needs to retain. It returns this state when the document + * is reopened. + * 6. Provides a utility to parse a resource selector into its component parts. + * + * @type {Object} + * + */ + +import { IframePhoneRpcEndpoint } from 'iframe-phone'; + +/** + * The CODAP Connection + * @param {iframePhone.IframePhoneRpcEndpoint} + */ +var connection: { call: (arg0: any, arg1: (response: any) => void) => void; } | null = null; + +var connectionState = 'preinit'; + +var stats = { + countDiReq: 0, + countDiRplSuccess: 0, + countDiRplFail: 0, + countDiRplTimeout: 0, + countCodapReq: 0, + countCodapUnhandledReq: 0, + countCodapRplSuccess: 0, + countCodapRplFail: 0, + timeDiFirstReq: (null as Date | null), + timeDiLastReq: (null as Date | null), + timeCodapFirstReq: (null as Date | null), + timeCodapLastReq: (null as Date | null) +}; + +export interface IConfig { + stateHandler?: (arg0: any) => void; + customInteractiveStateHandler?: boolean; + name?: any; + title?: any; + version?: any; + dimensions?: any; + preventBringToFront?: any; + preventDataContextReorg?: any; +} + +var config: IConfig | null = null; + +/** + * A serializable object shared with CODAP. This is saved as a part of the + * CODAP document. It is intended for the data interactive's use to store + * any information it may need to reestablish itself when a CODAP document + * is saved and restored. + * + * This object will be initially empty. It will be updated during the process + * initiated by the init method if CODAP was started from a previously saved + * document. + */ +var interactiveState = {}; + +/** + * A list of subscribers to messages from CODAP + * @param {[{actionSpec: {RegExp}, resourceSpec: {RegExp}, handler: {function}}]} + */ +var notificationSubscribers: { actionSpec: string; resourceSpec: any; operation: any; handler: any; }[] = []; + +function matchResource(resourceName: any, resourceSpec: string) { + return resourceSpec === '*' || resourceName === resourceSpec; +} + +function notificationHandler (request: { action: any; resource: any; values: any; }, callback: (arg0: { success: boolean; }) => void) { + var action = request.action; + var resource = request.resource; + var requestValues = request.values; + var returnMessage = {success: true}; + + connectionState = 'active'; + stats.countCodapReq += 1; + stats.timeCodapLastReq = new Date(); + if (!stats.timeCodapFirstReq) { + stats.timeCodapFirstReq = stats.timeCodapLastReq; + } + + if (action === 'notify' && !Array.isArray(requestValues)) { + requestValues = [requestValues]; + } + + var handled = false; + var success = true; + + if ((action === 'get') || (action === 'update')) { + // get assumes only one subscriber because it expects only one response. + notificationSubscribers.some(function (subscription) { + var result = false; + try { + if ((subscription.actionSpec === action) && + matchResource(resource, subscription.resourceSpec)) { + var rtn = subscription.handler(request); + if (rtn && rtn.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; } + returnMessage = rtn; + result = true; + } + } catch (ex) { + // console.log('DI Plugin notification handler exception: ' + ex); + result = true; + } + return result; + }); + if (!handled) { + stats.countCodapUnhandledReq++; + } + } else if (action === 'notify') { + requestValues.forEach(function (value: { operation: any; }) { + notificationSubscribers.forEach(function (subscription) { + // pass this notification to matching subscriptions + handled = false; + if ((subscription.actionSpec === action) && matchResource(resource, + subscription.resourceSpec) && (!subscription.operation || + (subscription.operation === value.operation) && subscription.handler)) { + var rtn = subscription.handler( + {action: action, resource: resource, values: value}); + if (rtn && rtn.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; } + success = (success && (rtn ? rtn.success : false)); + handled = true; + } + }); + if (!handled) { + stats.countCodapUnhandledReq++; + } + }); + } else { + // console.log("DI Plugin received unknown message: " + JSON.stringify(request)); + } + return callback(returnMessage); +} + +export const codapInterface = { + /** + * Connection statistics + */ + stats: stats, + + /** + * Initialize connection. + * + * Start connection. Request interactiveFrame to get prior state, if any. + * Update interactive frame to set name and dimensions and other configuration + * information. + * + * @param iConfig {object} Configuration. Optional properties: title {string}, + * version {string}, dimensions {object} + * + * @param iCallback {function(interactiveState)} + * @return {Promise} Promise of interactiveState; + */ + init: function (iConfig: IConfig, iCallback?: (arg0: any) => void) { + var this_ = this; + return new Promise(function (resolve: (arg0: any) => void, reject: { (arg0: string): void; (arg0: any): void; }) { + function getFrameRespHandler(resp: { values: { error: any; savedState: any }; success: boolean }[]) { + var success = resp && resp[1] && resp[1].success; + var receivedFrame = success && resp[1].values; + var savedState = receivedFrame && receivedFrame.savedState; + this_.updateInteractiveState(savedState); + if (success) { + // deprecated way of conveying state + if (iConfig.stateHandler) { + iConfig.stateHandler(savedState); + } + resolve(savedState); + } else { + if (!resp) { + reject('Connection request to CODAP timed out.'); + } else { + reject( + (resp[1] && resp[1].values && resp[1].values.error) || + 'unknown failure'); + } + } + if (iCallback) { + iCallback(savedState); + } + } + + var getFrameReq = {action: 'get', resource: 'interactiveFrame'}; + var newFrame = { + name: iConfig.name, + title: iConfig.title, + version: iConfig.version, + dimensions: iConfig.dimensions, + preventBringToFront: iConfig.preventBringToFront, + preventDataContextReorg: iConfig.preventDataContextReorg + }; + var updateFrameReq = { + action: 'update', + resource: 'interactiveFrame', + values: newFrame + }; + + config = iConfig; + + // initialize connection + connection = new IframePhoneRpcEndpoint( + notificationHandler, "data-interactive", window.parent); + + if (!config.customInteractiveStateHandler) { + this_.on('get', 'interactiveState', function () { + return ({success: true, values: this_.getInteractiveState()}); + }.bind(this_)); + } + + // console.log('sending interactiveState: ' + JSON.stringify(this_.getInteractiveState)); + // update, then get the interactiveFrame. + return this_.sendRequest([updateFrameReq, getFrameReq]) + .then(getFrameRespHandler as any, reject); + }.bind(this)); + }, + + /** + * Current known state of the connection + * @param {'preinit' || 'init' || 'active' || 'inactive' || 'closed'} + */ + getConnectionState: function () {return connectionState;}, + + getStats: function () { + return stats; + }, + + getConfig: function () { + return config; + }, + + /** + * Returns the interactive state. + * + * @returns {object} + */ + getInteractiveState: function () { + return interactiveState; + }, + + /** + * Updates the interactive state. + * @param iInteractiveState {Object} + */ + updateInteractiveState: function (iInteractiveState: any) { + if (!iInteractiveState) { + return; + } + interactiveState = Object.assign(interactiveState, iInteractiveState); + }, + + destroy: function () { + // todo : more to do? + connection = null; + }, + + /** + * Sends a request to CODAP. The format of the message is as defined in + * {@link https://github.com/concord-consortium/codap/wiki/CODAP-Data-Interactive-API}. + * + * @param message {String} + * @param callback {function(response, request)} Optional callback to handle + * the CODAP response. Note both the response and the initial request will + * sent. + * + * @return {Promise} The promise of the response from CODAP. + */ + sendRequest: function (message: any, callback?: any) { + return new Promise(function (resolve, reject){ + function handleResponse (request: any, response: {success: boolean} | undefined, callback: (arg0: any, arg1: any) => void) { + if (response === undefined) { + // console.warn('handleResponse: CODAP request timed out'); + reject('handleResponse: CODAP request timed out: ' + JSON.stringify(request)); + stats.countDiRplTimeout++; + } else { + connectionState = 'active'; + if (response.success) { stats.countDiRplSuccess++; } else { stats.countDiRplFail++; } + resolve(response); + } + if (callback) { + callback(response, request); + } + } + switch (connectionState) { + case 'closed': // log the message and ignore + // console.warn('sendRequest on closed CODAP connection: ' + JSON.stringify(message)); + reject('sendRequest on closed CODAP connection: ' + JSON.stringify(message)); + break; + case 'preinit': // warn, but issue request. + // console.log('sendRequest on not yet initialized CODAP connection: ' + + // JSON.stringify(message)); + /* falls through */ + default: + if (connection) { + stats.countDiReq++; + stats.timeDiLastReq = new Date(); + if (!stats.timeDiFirstReq) { + stats.timeCodapFirstReq = stats.timeDiLastReq; + } + + connection.call(message, function (response: any) { + handleResponse(message, response, callback); + }); + } else { + // console.error('sendRequest on non-existent CODAP connection'); + } + } + }); + }, + + /** + * Registers a handler to respond to CODAP-initiated requests and + * notifications. See {@link https://github.com/concord-consortium/codap/wiki/CODAP-Data-Interactive-API#codap-initiated-actions} + * + * @param actionSpec {'get' || 'notify'} (optional) Action to handle. Defaults to 'notify'. + * @param resourceSpec {String} A resource string. + * @param operation {String} (optional) name of operation, e.g. 'create', 'delete', + * 'move', 'resize', .... If not specified, all operations will be reported. + * @param handler {Function} A handler to receive the notifications. + */ + on: function (actionSpec: string, resourceSpec: string, operation: string | (() => void), handler?: () => void) { + var as = 'notify', + rs, + os, + hn; + var args = Array.prototype.slice.call(arguments); + if (['get', 'update', 'notify'].indexOf(args[0]) >= 0) { + as = args.shift(); + } + rs = args.shift(); + if (typeof args[0] !== 'function') { + os = args.shift(); + } + hn = args.shift(); + + const subscriber = { + actionSpec: as, + resourceSpec: rs, + operation: os, + handler: hn + }; + + notificationSubscribers.push(subscriber); + }, + + /** + * Parses a resource selector returning a hash of named resource names to + * resource values. The last clause is identified as the resource type. + * E.g. converts 'dataContext[abc].collection[def].case' + * to {dataContext: 'abc', collection: 'def', type: 'case'} + * + * @param {String} iResource + * @return {Object} + */ + parseResourceSelector: function (iResource: string) { + var selectorRE = /([A-Za-z0-9_-]+)\[([^\]]+)]/; + var result: any = {}; + var selectors = iResource.split('.'); + selectors.forEach(function (selector: string) { + var resourceType, resourceName; + var match = selectorRE.exec(selector); + if (selectorRE.test(selector) && match) { + resourceType = match[1]; + resourceName = match[2]; + result[resourceType] = resourceName; + result.type = resourceType; + } else { + result.type = selector; + } + }); + + return result; + } +}; diff --git a/src/assets/concord.png b/src/assets/concord.png deleted file mode 100644 index a9e9dd4d7d85befffc3c337b2ff2822d259a046e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 BZ4Ceb diff --git a/src/codap-plugin-api.ts b/src/codap-plugin-api.ts new file mode 100644 index 0000000..3be285c --- /dev/null +++ b/src/codap-plugin-api.ts @@ -0,0 +1,2 @@ +export {IConfig, codapInterface} from "./api/codap-interface"; +export {initializePlugin, createDataContext, openTable, addData, createItem, createItems} from "./api/codap-helper"; \ No newline at end of file diff --git a/src/components/app.scss b/src/components/app.scss deleted file mode 100644 index f91debb..0000000 --- a/src/components/app.scss +++ /dev/null @@ -1,13 +0,0 @@ -#app { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - color: #242424; - background-color: #dfdfdf; -} - diff --git a/src/components/app.tsx b/src/components/app.tsx deleted file mode 100644 index 776b996..0000000 --- a/src/components/app.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { Text } from "./text"; -import { useSampleText } from "../hooks/use-sample-text"; -import Icon from "../assets/concord.png"; - -import "./app.scss"; - -export const App = () => { - const sampleText = useSampleText(); - return ( -
- - -
- ); -}; diff --git a/src/components/text.test.tsx b/src/components/text.test.tsx deleted file mode 100644 index 6866c02..0000000 --- a/src/components/text.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import { Text } from "./text"; -import { render, screen } from "@testing-library/react"; - -describe("Text component", () => { - it("renders provided text", () => { - render(); - expect(screen.getByText("Hello World")).toBeDefined(); - }); -}); diff --git a/src/components/text.tsx b/src/components/text.tsx deleted file mode 100644 index b9d3815..0000000 --- a/src/components/text.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; - -interface IProps { - text: string; -} - -export const Text: React.FC = ({ text }) => ( -
{ text }
-); diff --git a/src/hooks/use-sample-text.test.ts b/src/hooks/use-sample-text.test.ts deleted file mode 100644 index 3e9cd6d..0000000 --- a/src/hooks/use-sample-text.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { renderHook } from "@testing-library/react"; -import { useSampleText } from "./use-sample-text"; - -const HookWrapper = () => useSampleText(); - -describe("useSampleText", () => { - it("returns Hello World", () => { - const { result } = renderHook(HookWrapper); - expect(result.current).toEqual("Hello World"); - }); -}); diff --git a/src/hooks/use-sample-text.ts b/src/hooks/use-sample-text.ts deleted file mode 100644 index 7295027..0000000 --- a/src/hooks/use-sample-text.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useState } from "react"; -import t from "../utils/translation/translate"; - -export const useSampleText = () => { - // `useState` is unnecessary here. But it's used so this function works like a real hook and requires special testing. - // If this function was only returning "Hello World", we wouldn't need any special approach to testing. - const [text] = useState(t("INTRO.HELLO")); - return text; -}; diff --git a/src/index.html b/src/index.html deleted file mode 100644 index 5188607..0000000 --- a/src/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Starter Projects - - - - -
- - diff --git a/src/index.scss b/src/index.scss deleted file mode 100644 index 668b92d..0000000 --- a/src/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -html, body { - margin: 0; - padding: 0; -} - -body { - font-family: 'Lato', sans-serif; - user-select: none; -} diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 51db7fc..0000000 --- a/src/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import { App } from "./components/app"; - -import "./index.scss"; - -const container = document.getElementById("app"); -if (container) { - const root = createRoot(container); - root.render(); -} diff --git a/src/public/favicon.ico b/src/public/favicon.ico deleted file mode 100644 index a9e9dd4d7d85befffc3c337b2ff2822d259a046e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 BZ4Ceb diff --git a/src/test/setupTests.ts b/src/test/setupTests.ts deleted file mode 100644 index d0de870..0000000 --- a/src/test/setupTests.ts +++ /dev/null @@ -1 +0,0 @@ -import "@testing-library/jest-dom"; diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..5873f77 --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1 @@ +declare module 'iframe-phone'; \ No newline at end of file diff --git a/src/utils/translation/README.md b/src/utils/translation/README.md deleted file mode 100644 index 2d72b88..0000000 --- a/src/utils/translation/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Starter Projects Localization - -The modules within `utils/translation` can be used to add text localization to a starter-projects based application. - -### How to use - -#### Adding translation files - -Translation JSON files are added to `utils/translation/lang`. Add a translation JSON file for each translated language and import the translation JSON file in `utils/translation/translate.ts`. Name the translation JSON file using the ISO 639-1 Language Code for the given language (i.e., `fr.json` for French or `de.json` for German). Add a key/value pair for each translated word or phrase. The key is used by the translation function and the value is the actual translated text. -``` -{ - "TEXT": "text", - "INTRO.HELLO": "Hello World" -} -``` - -#### Using the translate function - -Import the `translate` function found in `utils/translation/translate.ts` in your module: -``` -import t from "../utils/translation/translate"; -``` - -Call the function using the translation key of the desired text: -``` -console.log(t("INTRO.HELLO")); -``` - -Variables can be defined and specified in your translated text. Use the format `%{VAR_NAME}` to add a variable to a key's value in the translation JSON file: -``` -{ - "AGE": "I am %{userAge} years old" -} -``` -Use the `vars` property of the `options` parameter to specify one or more variable values when calling the translation function: -``` -console.log(t("AGE", { vars: { userAge: "25" } })); -``` - - -#### Determining the current language -The `translate` function will first get the HTML DOM `lang` attribute of the root element of the document to determine the current page language. If no `lang` attribute is specified, then the `translate` function will get the first valid language specified by the browser to determine the current page language. Optionally, a language value can be specified when calling the `translate` function that will override the `lang` attribute and the browser settings. Use the `lang` property of the `options` parameter to specify a language when calling the `translate` function: -``` -console.log(t("INTRO.HELLO", { lang: "es" })); -``` diff --git a/src/utils/translation/lang/en-us.json b/src/utils/translation/lang/en-us.json deleted file mode 100644 index d5c0dea..0000000 --- a/src/utils/translation/lang/en-us.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEXT": "text", - "INTRO.HELLO": "Hello World" -} diff --git a/src/utils/translation/lang/es.json b/src/utils/translation/lang/es.json deleted file mode 100644 index 5e55f71..0000000 --- a/src/utils/translation/lang/es.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEXT": "texto", - "INTRO.HELLO": "Hola Mundo" -} diff --git a/src/utils/translation/translate.ts b/src/utils/translation/translate.ts deleted file mode 100644 index 62e25a1..0000000 --- a/src/utils/translation/translate.ts +++ /dev/null @@ -1,67 +0,0 @@ -import enUS from "./lang/en-us.json"; -import es from "./lang/es.json"; - -const languageFiles = [ - { key: "en-US", contents: enUS }, // US English - { key: "es", contents: es }, // Spanish -]; - -// returns baseLANG from baseLANG-REGION if REGION exists -// this will, for example, convert en-US to en -const getBaseLanguage = (langKey: any) => { - return langKey.split("-")[0]; -}; - -// Get the HTML DOM lang property of the root element of the document -const getPageLanguage = () => { - const pageLang = document.documentElement.lang; - return pageLang && (pageLang !== "unknown") ? pageLang : undefined; -}; - -// Get the first valid language specified by the browser -const getFirstBrowserLanguage = () => { - const nav: any = window.navigator; - // userLanguage and browserLanguage are non standard but required for IE support - const languages = [...nav.languages, nav.language, nav.userLanguage, nav.browserLanguage]; - return languages.find((browserLang) => browserLang); -}; - -const translations: any = {}; - -languageFiles.forEach((langFile) => { - translations[langFile.key] = langFile.contents; - // accept full key with region code or just the language code - const bLang = getBaseLanguage(langFile.key); - if (bLang && !translations[bLang]) { - translations[bLang] = langFile.contents; - } -}); - -const varRegExp = /%\{\s*([^}\s]*)\s*\}/g; - -const currentLang = getPageLanguage() || getFirstBrowserLanguage(); -const baseLang = getBaseLanguage(currentLang || ""); -const defaultLang = currentLang && translations[currentLang] - ? currentLang - : baseLang && translations[baseLang] - ? baseLang - : "en"; - -interface ITranslateOptions { - lang?: string; - count?: number; - vars?: Record; -} - -export default function translate (key: string, options?: ITranslateOptions) { - const lang = options?.lang || defaultLang; - const vars = options?.vars || {}; - const translation = translations?.[lang]?.[key] || key; - return translation.replace(varRegExp, (match: string, langKey: string) => { - if (Object.prototype.hasOwnProperty.call(vars, langKey)) { - return vars[langKey]; - } else { - return `'** UNKNOWN KEY: ${langKey} **`; - } - }); -} From 62b09ef14ef05461bb5fab8dfeddddfb2a0e24dd Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 15 Nov 2023 15:26:23 -0500 Subject: [PATCH 2/9] Remove unnecessary deployment steps. --- .github/workflows/ci.yml | 27 +------ .github/workflows/release.yml | 24 ------ README.md | 112 +--------------------------- doc/deploy.md | 24 +++--- package-lock.json | 4 +- package.json | 10 +-- webpack.config.js | 135 +++------------------------------- 7 files changed, 32 insertions(+), 304 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4b1faa..fc0bd45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,29 +57,4 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: - flags: cypress - s3-deploy: - name: S3 Deploy - needs: - - build_test - - cypress - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - - name: Install Dependencies - run: npm ci - env: - # skip installing cypress since it isn't needed for just building - # This decreases the deploy time quite a bit - CYPRESS_INSTALL_BINARY: 0 - - uses: concord-consortium/s3-deploy-action@v1 - with: - bucket: models-resources - prefix: starter-projects - awsAccessKeyId: ${{ secrets.AWS_ACCESS_KEY_ID }} - awsSecretAccessKey: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - # Parameters to GHActions have to be strings, so a regular yaml array cannot - # be used. Instead the `|` turns the following lines into a string - topBranches: | - ["master"] + flags: cypress \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index eac863c..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Release -on: - workflow_dispatch: - inputs: - version: - description: The git tag for the version to use for index.html - required: true -env: - BUCKET: models-resources - PREFIX: starter-projects - SRC_FILE: index-top.html - DEST_FILE: index.html -jobs: - release: - runs-on: ubuntu-latest - steps: - - run: > - aws s3 cp - s3://${{ env.BUCKET }}/${{ env.PREFIX }}/version/${{ github.event.inputs.version }}/${{ env.SRC_FILE }} - s3://${{ env.BUCKET }}/${{ env.PREFIX }}/${{ env.DEST_FILE }} - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: us-east-1 diff --git a/README.md b/README.md index 8856d17..ce66daa 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,10 @@ -# Starter Projects +# CODAP Plugin API -## Development - -### Copying a starter project - -1. Create a new public repository for your project (e.g. `new-repository`) -2. Create a clone of the starter repo - ``` - git clone --single-branch https://github.com/concord-consortium/starter-projects.git new-repository - ``` -3. Update the starter repo - - First, update and run the starter project: - ``` - cd new-repository - npm install - npm update - npm start - ``` - Then, verify the project works by visiting [localhost:8080](http://localhost:8080) and checking for the words "Hello World". - Also verify that the test suite still passes: - ``` - npm run test:full - ``` - If the updates are functional, please commit any changes to `package.json` or `package-lock.json` back to the - Starter Projects repository for future use. - -4. Next, re-initialize the repo to create a new history - ``` - rm -rf .git - git init - ``` -5. Create an initial commit for your new project - ``` - git add . - git commit -m "Initial commit" - ``` -6. Push to your new repository - ``` - git remote add origin https://github.com/concord-consortium/new-repository.git - git push -u origin master - ``` -7. Open your new repository and update all instances of `starter-projects` to `new-repository` and `Starter Projects` to `New Repository`. - Note: this will do some of the configuration for GitHub Actions deployment to S3, but you'll still need to follow - the instructions [here](https://docs.google.com/document/d/e/2PACX-1vTpYjbGmUMxk_FswUmapK_RzVyEtm1WdnFcNByp9mqwHnp0nR_EzRUOiubuUCsGwzQgOnut_UiabYOM/pub). -8. To record the cypress tests results to the cypress dashboard service: - - go to https://dashboard.cypress.io - - create a new project - - go to the settings for the project - - in the github integration section choose the github repo to connect this project to - - copy the record key, and create a secret in the github repositories settings with the name CYPRESS_RECORD_KEY - - copy the Project ID and replace the value of `projectId` in cypress.json -9. Your new repository is ready! Remove this section of the `README`, and follow the steps below to use it. - -### Initial steps - -1. Clone this repo and `cd` into it -2. Run `npm install` to pull dependencies -3. Run `npm start` to run `webpack-dev-server` in development mode with hot module replacement - -#### Run using HTTPS - -Additional steps are required to run using HTTPS. - -1. install [mkcert](https://github.com/FiloSottile/mkcert) : `brew install mkcert` (install using Scoop or Chocolatey on Windows) -2. Create and install the trusted CA in keychain if it doesn't already exist: `mkcert -install` -3. Ensure you have a `.localhost-ssl` certificate directory in your home directory (create if needed, typically `C:\Users\UserName` on Windows) and cd into that directory -4. Make the cert files: `mkcert -cert-file localhost.pem -key-file localhost.key localhost 127.0.0.1 ::1` -5. Run `npm run start:secure` to run `webpack-dev-server` in development mode with hot module replacement - -Alternately, you can run secure without certificates in Chrome: -1. Enter `chrome://flags/#allow-insecure-localhost` in Chrome URL bar -2. Change flag from disabled to enabled -3. Run `npm run start:secure:no-certs` to run `webpack-dev-server` in development mode with hot module replacement +## This documentation is meant to be used by CODAP Plugin API developers. ### Building If you want to build a local version run `npm build`, it will create the files in the `dist` folder. -You *do not* need to build to deploy the code, that is automatic. See more info in the Deployment section below. ### Notes @@ -85,39 +12,6 @@ You *do not* need to build to deploy the code, that is automatic. See more info To ensure that you are open a TypeScript file in VSC and then click on the version number next to `TypeScript React` in the status bar and select 'Use Workspace Version' in the popup menu. -## Deployment - -Follow the instructions in this -[Guide](https://docs.google.com/document/d/1EacCSUhaHXaL8ll8xjcd4svyguEO-ipf5aF980-_q8E) -to setup an S3 & Cloudfront distribution that can be used with Github actions. -See also `s3_deploy.sh`, and `./github/ci.yml`. - -Production releases to S3 are based on the contents of the /dist folder and are built automatically by GitHub Actions -for each branch and tag pushed to GitHub. - -Branches are deployed to http://starter-projects.concord.org/branch/. -If the branch name starts or ends with a number this number is stripped off. - -Tags are deployed to http://starter-projects.concord.org/version/. - -To deploy a production release: - -1. Increment version number in package.json -2. Create new entry in CHANGELOG.md -3. Run `git log --pretty=oneline --reverse ...HEAD | grep '#' | grep -v Merge` and add contents (after edits if needed to CHANGELOG.md) -4. Run `npm run build` -5. Copy asset size markdown table from previous release and change sizes to match new sizes in `dist` -6. Create `release-` branch and commit changes, push to GitHub, create PR and merge -7. Checkout master and pull -8. Create an annotated tag for the version, of the form `v[x].[y].[z]`, include at least the version in the tag message. On the command line this can be done with a command like `git tag -a v1.2.3 -m "1.2.3 some info about this version"` -9. Push the tag to github with a command like: `git push origin v1.2.3`. -10. Use https://github.com/concord-consortium/starter-projects/releases to make this tag into a GitHub release. -11. Run the release workflow to update http://starter-projects.concord.org/index.html. - 1. Navigate to the actions page in GitHub and the click the "Release" workflow. This should take you to this page: https://github.com/concord-consortium/starter-projects/actions/workflows/release.yml. - 2. Click the "Run workflow" menu button. - 3. Type in the tag name you want to release for example `v1.2.3`. (Note this won't work until the PR has been merged to master) - 4. Click the `Run Workflow` button. - ### Testing Run `npm test` to run jest tests. Run `npm run test:full` to run jest and Cypress tests. @@ -142,6 +36,6 @@ Inside of your `package.json` file: ## License -Starter Projects are Copyright 2018 (c) by the Concord Consortium and is distributed under the [MIT license](http://www.opensource.org/licenses/MIT). +CODAP Plugin API are Copyright 2018 (c) by the Concord Consortium and is distributed under the [MIT license](http://www.opensource.org/licenses/MIT). See license.md for the complete license text. diff --git a/doc/deploy.md b/doc/deploy.md index 2a2ca49..4bd946f 100644 --- a/doc/deploy.md +++ b/doc/deploy.md @@ -6,17 +6,17 @@ Deploying to S3 is handled by the [S3 Deploy Action](https://github.com/concord- ## Where to find builds -- **branch builds**: when a developer pushes a branch, GitHub actions will build and deploy it to `starter-projects/branch/[branch-name]/index.html`. If the branch starts or ends with a number this is automatically stripped off and not included in the folder name. -- **version builds**: when a developer pushes a tag, GitHub actions will build and deploy it to `starter-projects/version/[tag-name]/index.html` -- **released version path**: the released version of the application is available at `starter-projects/index.html` -- **master branch**: the master branch build is available at both `starter-projects/index-master.html` and `starter-projects/branch/master/index.html`. The `index-master.html` form is preferred because it verifies the top level deployment is working for the current code. Additional branches can be added to the top level by updating the `topBranches` configuration in `ci.yml` -- **staging or other top level paths**: additional top level releases can be added so they are available at `starter-projects/index-[name].html` +- **branch builds**: when a developer pushes a branch, GitHub actions will build and deploy it to `codap-plugin-api/branch/[branch-name]/index.html`. If the branch starts or ends with a number this is automatically stripped off and not included in the folder name. +- **version builds**: when a developer pushes a tag, GitHub actions will build and deploy it to `codap-plugin-api/version/[tag-name]/index.html` +- **released version path**: the released version of the application is available at `codap-plugin-api/index.html` +- **master branch**: the master branch build is available at both `codap-plugin-api/index-master.html` and `codap-plugin-api/branch/master/index.html`. The `index-master.html` form is preferred because it verifies the top level deployment is working for the current code. Additional branches can be added to the top level by updating the `topBranches` configuration in `ci.yml` +- **staging or other top level paths**: additional top level releases can be added so they are available at `codap-plugin-api/index-[name].html` ## index-top.html -The key feature of `index-top.html` is that it references the javascript and css assets using relative paths to the version or branch folder. So the javascript url will be something like `version/v1.2.3/index.js`. This way when the `index-top.html` is copied to the top level, the browser can find these assets. +The key feature of `index-top.html` is that it references the javascript and css assets using relative paths to the version or branch folder. So the javascript url will be something like `version/v1.2.3/index.js`. This way when the `index-top.html` is copied to the top level, the browser can find these assets. -Building a functional index.js that works when it is loaded either by `index.html` or `index-top.html` depends on using Webpack a certain way. Since Webpack 5, the `publicPath` configuration option's default value is `'auto'`. With this value the public path is computed at runtime based on the path the script was loaded from. So if the script was loaded from `/starter-projects/version/v1.2.3/index.[hash].js` then at runtime the public path will be set to `/starter-projects/version/v1.2.3/`. The reason the public path matters has to do with how javascript loads and references assets like images or json files. +Building a functional index.js that works when it is loaded either by `index.html` or `index-top.html` depends on using Webpack a certain way. Since Webpack 5, the `publicPath` configuration option's default value is `'auto'`. With this value the public path is computed at runtime based on the path the script was loaded from. So if the script was loaded from `/codap-plugin-api/version/v1.2.3/index.[hash].js` then at runtime the public path will be set to `/codap-plugin-api/version/v1.2.3/`. The reason the public path matters has to do with how javascript loads and references assets like images or json files. For example `components/app.tsx` uses: ``` @@ -24,13 +24,13 @@ import Icon from "../assets/concord.png"; ... ``` -This `` tag will be added by React to the dom. When the browser loads the image, the value of `src` will be relative to the `index.html` file. This would be a problem without the computed public path. Webpack handles this by automatically pre-pending the computed public path onto the URL it uses for `Icon`. So whether the html file is located at `/starter-projects/index.html` or `/starter-projects/version/v1.2.3/index.html`, the value of `Icon` is based on the location of the javascript file. So in this case the value of `Icon` will be `/starter-projects/version/v1.2.3/[asset name computed by webpack].png`. +This `` tag will be added by React to the dom. When the browser loads the image, the value of `src` will be relative to the `index.html` file. This would be a problem without the computed public path. Webpack handles this by automatically pre-pending the computed public path onto the URL it uses for `Icon`. So whether the html file is located at `/codap-plugin-api/index.html` or `/codap-plugin-api/version/v1.2.3/index.html`, the value of `Icon` is based on the location of the javascript file. So in this case the value of `Icon` will be `/codap-plugin-api/version/v1.2.3/[asset name computed by webpack].png`. If the import statement is not used and instead the src of the image was hard coded like: ``` ``` -Webpack has no control of this, so at runtime this will be loaded relative to the html file. So when the `index.html` is at the top level, the browser will look for `/starter-projects/assets/concord.png` and not find it. So hard coded paths like this should be converted to using import statements. +Webpack has no control of this, so at runtime this will be loaded relative to the html file. So when the `index.html` is at the top level, the browser will look for `/codap-plugin-api/assets/concord.png` and not find it. So hard coded paths like this should be converted to using import statements. In some cases we dynamically compute a path to load an asset from. In most of these places webpack imports can still be used. Webpack supports this by static analysis of the import function, so we just need to change those places in the code slightly. Here is the documentation about this: https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import @@ -50,7 +50,7 @@ Note: there is a `publicPath` configuration option for the `HtmlWebpackPlugin`. ## Local testing for compatibility with index-top.html -When running in the regular dev server, you won't see errors when using hard coded paths. +When running in the regular dev server, you won't see errors when using hard coded paths. Typically, hard coded paths will only work if you are using `CopyWebpackPlugin`. This is because these assets need to be copied into the `dist` folder. With import statements the assets are copied for you. If you remove the `CopyWebpackPlugin` you will likely see errors when using the dev server, so you can find the places that need to fixed. @@ -62,6 +62,6 @@ If you need to continue referencing files without using import, you can find the Previously we would do releases by updating a branch named `production`. This would build and deploy the application to the top level of the s3 folder. -With this new approach a release is done by copying a single small html file from a version folder up to the top level. This means the javascript and css is not rebuilt just to promote a version. Therefore the exact build products can be tested before it is released. +With this new approach a release is done by copying a single small html file from a version folder up to the top level. This means the javascript and css is not rebuilt just to promote a version. Therefore the exact build products can be tested before it is released. -Because deploying a version or branch only updates files within a folder specific to that version or branch, the utility used to copy files up to S3 can be more simple and efficient. In the previous model when the utility was uploading a production branch it would need to make sure to ignore the branch and version folders. Otherwise it might delete these folders because they aren't part of the upload. Even if the utility was configured to never delete files, it still needed to load the meta data of all of the files in the branch and version folders. It did this to know what has changed between local and remote. And S3's APIs don't support filtering listings of files other than a folder prefix. +Because deploying a version or branch only updates files within a folder specific to that version or branch, the utility used to copy files up to S3 can be more simple and efficient. In the previous model when the utility was uploading a production branch it would need to make sure to ignore the branch and version folders. Otherwise it might delete these folders because they aren't part of the upload. Even if the utility was configured to never delete files, it still needed to load the meta data of all of the files in the branch and version folders. It did this to know what has changed between local and remote. And S3's APIs don't support filtering listings of files other than a folder prefix. diff --git a/package-lock.json b/package-lock.json index 693396c..6f0c75d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "starter-projects", + "name": "codap-plugin-api", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "starter-projects", + "name": "codap-plugin-api", "version": "0.0.1", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e2ac27f..80f77f9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "starter-projects", + "name": "codap-plugin-api", "version": "0.0.1", - "description": "Concord Consortium starter projects", + "description": "An API to ease the development of CODAP plugins", "main": "index.js", "browserslist": "> 0.2%, last 5 versions, Firefox ESR, not dead, not ie > 0", "jest": { @@ -60,14 +60,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/concord-consortium/starter-projects.git" + "url": "git+https://github.com/concord-consortium/codap-plugin-api.git" }, "author": "Concord Consortium", "license": "MIT", "bugs": { - "url": "https://github.com/concord-consortium/starter-projects/issues" + "url": "https://github.com/concord-consortium/codap-plugin-api/issues" }, - "homepage": "https://github.com/concord-consortium/starter-projects#readme", + "homepage": "https://github.com/concord-consortium/codap-plugin-api#readme", "nyc": { "extends": "@istanbuljs/nyc-config-typescript", "all": true, diff --git a/webpack.config.js b/webpack.config.js index 9949bbc..e3816f5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,37 +1,15 @@ 'use strict'; -const path = require('path'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); -const ESLintPlugin = require('eslint-webpack-plugin'); -const os = require('os'); - -// DEPLOY_PATH is set by the s3-deploy-action its value will be: -// `branch/[branch-name]/` or `version/[tag-name]/` -// See the following documentation for more detail: -// https://github.com/concord-consortium/s3-deploy-action/blob/main/README.md#top-branch-example -const DEPLOY_PATH = process.env.DEPLOY_PATH; - module.exports = (env, argv) => { - const devMode = argv.mode !== 'production'; - return { context: __dirname, // to automatically find tsconfig.json - devServer: { - static: 'dist', - hot: true, - https: { - key: path.resolve(os.homedir(), '.localhost-ssl/localhost.key'), - cert: path.resolve(os.homedir(), '.localhost-ssl/localhost.pem'), - }, - }, devtool: devMode ? 'eval-cheap-module-source-map' : 'source-map', - entry: './src/index.tsx', + entry: './src/codap-plugin-api.tsx', mode: 'development', output: { - path: path.resolve(__dirname, 'dist'), - filename: 'assets/index.[contenthash].js', + filename: 'codap-plugin-api.js', + library: 'CODAPPlugin', + libraryTarget: 'umd', }, performance: { hints: false }, module: { @@ -39,85 +17,6 @@ module.exports = (env, argv) => { { test: /\.tsx?$/, loader: 'ts-loader', - }, - // This code coverage instrumentation should only be added when needed. It makes - // the code larger and slower - process.env.CODE_COVERAGE ? { - test: /\.[tj]sx?$/, - loader: '@jsdevtools/coverage-istanbul-loader', - options: { esModules: true }, - enforce: 'post', - exclude: path.join(__dirname, 'node_modules'), - } : {}, - { - test: /\.(sa|sc|le|c)ss$/i, - use: [ - devMode ? 'style-loader' : MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - esModule: false, - modules: { - // required for :import from scss files - // cf. https://github.com/webpack-contrib/css-loader#separating-interoperable-css-only-and-css-module-features - mode: 'icss', - } - } - }, - 'postcss-loader', - 'sass-loader', - ] - }, - { - test: /\.(png|woff|woff2|eot|ttf)$/, - type: 'asset', - }, - { // disable svgo optimization for files ending in .nosvgo.svg - test: /\.nosvgo\.svg$/i, - loader: '@svgr/webpack', - options: { - svgo: false, - } - }, - { - test: /\.svg$/i, - exclude: /\.nosvgo\.svg$/i, - oneOf: [ - { - // Do not apply SVGR import in CSS files. - issuer: /\.(css|scss|less)$/, - type: 'asset', - }, - { - issuer: /\.tsx?$/, - loader: '@svgr/webpack', - options: { - svgoConfig: { - plugins: [ - { - // cf. https://github.com/svg/svgo/releases/tag/v2.4.0 - name: 'preset-default', - params: { - overrides: { - // don't minify "id"s (i.e. turn randomly-generated unique ids into "a", "b", ...) - // https://github.com/svg/svgo/blob/master/plugins/cleanupIds.js - cleanupIds: { minify: false }, - // leave s, s and s alone - // https://github.com/svg/svgo/blob/master/plugins/convertShapeToPath.js - convertShapeToPath: false, - // leave "stroke"s and "fill"s alone - // https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js - removeUnknownsAndDefaults: { defaultAttrs: false }, - // leave viewBox alone - removeViewBox: false - } - } - } - ] - } - } - } - ] } ] }, @@ -128,26 +27,10 @@ module.exports = (env, argv) => { // suppress "export not found" warnings about re-exported types warningsFilter: /export .* was not found in/, }, - plugins: [ - new ESLintPlugin({ - extensions: ['ts', 'tsx', 'js', 'jsx'], - }), - new MiniCssExtractPlugin({ - filename: devMode ? 'assets/[name].css' : 'assets/[name].[contenthash].css', - }), - new HtmlWebpackPlugin({ - filename: 'index.html', - template: 'src/index.html', - favicon: 'src/public/favicon.ico', - publicPath: '.', - }), - ...(DEPLOY_PATH ? [new HtmlWebpackPlugin({ - filename: 'index-top.html', - template: 'src/index.html', - favicon: 'src/public/favicon.ico', - publicPath: DEPLOY_PATH - })] : []), - new CleanWebpackPlugin(), - ] + plugins: [], + externals: { + 'react': 'react', + 'react-dom': 'ReactDOM' + } }; }; From d3007fb8067c553db3978c9271eb7e08fe5fc52e Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 16 Nov 2023 09:34:54 -0500 Subject: [PATCH 3/9] Fix linting errors. --- .eslintrc.js | 5 +- cypress/e2e/workspace.test.ts | 2 +- cypress/support/elements/app-elements.ts | 2 +- src/api/codap-helper.ts | 26 ++--- src/api/codap-interface.ts | 128 +++++++++++------------ src/codap-plugin-api.ts | 2 +- src/typings.d.ts | 2 +- 7 files changed, 84 insertions(+), 83 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1a69dc0..08a62a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,7 @@ module.exports = { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-shadow": ["error", { builtinGlobals: false, hoist: "all", allow: [] }], + "@typescript-eslint/no-this-alias": "warn", "@typescript-eslint/no-unused-vars": ["warn", { args: "none", ignoreRestSiblings: true }], "@typescript-eslint/prefer-optional-chain": "warn", "@typescript-eslint/semi": ["warn", "always"], @@ -73,10 +74,10 @@ module.exports = { "no-var": "error", "no-whitespace-before-property": "error", "object-shorthand": "error", - "prefer-const": ["error", { destructuring: "all" }], + "prefer-const": ["warn", { destructuring: "all" }], "prefer-object-spread": "error", "prefer-regex-literals": "error", - "prefer-rest-params": "error", + "prefer-rest-params": "warn", "prefer-spread": "error", "quotes": ["error", "double", { allowTemplateLiterals: true, avoidEscape: true }], "radix": "error", diff --git a/cypress/e2e/workspace.test.ts b/cypress/e2e/workspace.test.ts index e82551d..6cf6bed 100644 --- a/cypress/e2e/workspace.test.ts +++ b/cypress/e2e/workspace.test.ts @@ -1,4 +1,4 @@ -import { AppElements as ae } from "../support/elements/app-elements" +import { AppElements as ae } from "../support/elements/app-elements"; context("Test the overall app", () => { beforeEach(() => { diff --git a/cypress/support/elements/app-elements.ts b/cypress/support/elements/app-elements.ts index 261ba45..d620d7c 100644 --- a/cypress/support/elements/app-elements.ts +++ b/cypress/support/elements/app-elements.ts @@ -2,4 +2,4 @@ export const AppElements = { getApp() { return cy.get(".app"); } -} +}; diff --git a/src/api/codap-helper.ts b/src/api/codap-helper.ts index c1fdee4..3c0b887 100644 --- a/src/api/codap-helper.ts +++ b/src/api/codap-helper.ts @@ -7,8 +7,8 @@ export interface IItem { export function initializePlugin(pluginName: string, version: string, dimensions: {width: number, height: number}) { const interfaceConfig = { name: pluginName, - version: version, - dimensions: dimensions + version, + dimensions }; return codapInterface.init(interfaceConfig); } @@ -19,18 +19,18 @@ export function createDataContext(dataContextName: string) { // Determine if CODAP already has the Data Context we need. // If not, create it. return codapInterface.sendRequest({ - action:'get', + action:"get", resource: dataSetString(dataContextName) }, function (result: { success: any; }) { if (result && !result.success) { codapInterface.sendRequest({ - action: 'create', - resource: 'dataContext', + action: "create", + resource: "dataContext", values: { name: dataContextName, collections: [ { - name: 'items', + name: "items", labels: { pluralCase: "items", setOfCasesWithArticle: "an item" @@ -47,10 +47,10 @@ export function createDataContext(dataContextName: string) { export function openTable() { codapInterface.sendRequest({ - action: 'create', - resource: 'component', + action: "create", + resource: "component", values: { - type: 'caseTable' + type: "caseTable" } }); } @@ -58,7 +58,7 @@ export function openTable() { export function addData(dataContextName: string, data: number[]) { const values = data.map(d => ({value: d})); codapInterface.sendRequest({ - action: 'create', + action: "create", resource: `${dataSetString(dataContextName)}.item`, values }); @@ -66,7 +66,7 @@ export function addData(dataContextName: string, data: number[]) { export function createItem(dataContextName: string, item: IItem) { codapInterface.sendRequest({ - action: 'create', + action: "create", resource: `${dataSetString(dataContextName)}.item`, values: item }); @@ -75,5 +75,5 @@ export function createItem(dataContextName: string, item: IItem) { export function createItems(dataContextName: string, items: IItem[]) { items.forEach(item => { createItem(dataContextName, item); - }) -} \ No newline at end of file + }); +} diff --git a/src/api/codap-interface.ts b/src/api/codap-interface.ts index a50b4af..0a0fa53 100644 --- a/src/api/codap-interface.ts +++ b/src/api/codap-interface.ts @@ -41,17 +41,17 @@ * */ -import { IframePhoneRpcEndpoint } from 'iframe-phone'; +import { IframePhoneRpcEndpoint } from "iframe-phone"; /** * The CODAP Connection * @param {iframePhone.IframePhoneRpcEndpoint} */ -var connection: { call: (arg0: any, arg1: (response: any) => void) => void; } | null = null; +let connection: { call: (arg0: any, arg1: (response: any) => void) => void; } | null = null; -var connectionState = 'preinit'; +let connectionState = "preinit"; -var stats = { +const stats = { countDiReq: 0, countDiRplSuccess: 0, countDiRplFail: 0, @@ -77,7 +77,7 @@ export interface IConfig { preventDataContextReorg?: any; } -var config: IConfig | null = null; +let config: IConfig | null = null; /** * A serializable object shared with CODAP. This is saved as a part of the @@ -89,47 +89,47 @@ var config: IConfig | null = null; * initiated by the init method if CODAP was started from a previously saved * document. */ -var interactiveState = {}; +let interactiveState = {}; /** * A list of subscribers to messages from CODAP * @param {[{actionSpec: {RegExp}, resourceSpec: {RegExp}, handler: {function}}]} */ -var notificationSubscribers: { actionSpec: string; resourceSpec: any; operation: any; handler: any; }[] = []; +const notificationSubscribers: { actionSpec: string; resourceSpec: any; operation: any; handler: any; }[] = []; function matchResource(resourceName: any, resourceSpec: string) { - return resourceSpec === '*' || resourceName === resourceSpec; + return resourceSpec === "*" || resourceName === resourceSpec; } function notificationHandler (request: { action: any; resource: any; values: any; }, callback: (arg0: { success: boolean; }) => void) { - var action = request.action; - var resource = request.resource; - var requestValues = request.values; - var returnMessage = {success: true}; + const action = request.action; + const resource = request.resource; + let requestValues = request.values; + let returnMessage = {success: true}; - connectionState = 'active'; + connectionState = "active"; stats.countCodapReq += 1; stats.timeCodapLastReq = new Date(); if (!stats.timeCodapFirstReq) { stats.timeCodapFirstReq = stats.timeCodapLastReq; } - if (action === 'notify' && !Array.isArray(requestValues)) { + if (action === "notify" && !Array.isArray(requestValues)) { requestValues = [requestValues]; } - var handled = false; - var success = true; + let handled = false; + let success = true; - if ((action === 'get') || (action === 'update')) { + if ((action === "get") || (action === "update")) { // get assumes only one subscriber because it expects only one response. notificationSubscribers.some(function (subscription) { - var result = false; + let result = false; try { if ((subscription.actionSpec === action) && matchResource(resource, subscription.resourceSpec)) { - var rtn = subscription.handler(request); - if (rtn && rtn.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; } + const rtn = subscription.handler(request); + if (rtn?.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; } returnMessage = rtn; result = true; } @@ -142,7 +142,7 @@ function notificationHandler (request: { action: any; resource: any; values: any if (!handled) { stats.countCodapUnhandledReq++; } - } else if (action === 'notify') { + } else if (action === "notify") { requestValues.forEach(function (value: { operation: any; }) { notificationSubscribers.forEach(function (subscription) { // pass this notification to matching subscriptions @@ -150,9 +150,9 @@ function notificationHandler (request: { action: any; resource: any; values: any if ((subscription.actionSpec === action) && matchResource(resource, subscription.resourceSpec) && (!subscription.operation || (subscription.operation === value.operation) && subscription.handler)) { - var rtn = subscription.handler( - {action: action, resource: resource, values: value}); - if (rtn && rtn.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; } + const rtn = subscription.handler( + {action, resource, values: value}); + if (rtn?.success) { stats.countCodapRplSuccess++; } else{ stats.countCodapRplFail++; } success = (success && (rtn ? rtn.success : false)); handled = true; } @@ -171,7 +171,7 @@ export const codapInterface = { /** * Connection statistics */ - stats: stats, + stats, /** * Initialize connection. @@ -186,13 +186,13 @@ export const codapInterface = { * @param iCallback {function(interactiveState)} * @return {Promise} Promise of interactiveState; */ - init: function (iConfig: IConfig, iCallback?: (arg0: any) => void) { - var this_ = this; + init (iConfig: IConfig, iCallback?: (arg0: any) => void) { + const this_ = this; return new Promise(function (resolve: (arg0: any) => void, reject: { (arg0: string): void; (arg0: any): void; }) { function getFrameRespHandler(resp: { values: { error: any; savedState: any }; success: boolean }[]) { - var success = resp && resp[1] && resp[1].success; - var receivedFrame = success && resp[1].values; - var savedState = receivedFrame && receivedFrame.savedState; + const success = resp && resp[1] && resp[1].success; + const receivedFrame = success && resp[1].values; + const savedState = receivedFrame && receivedFrame.savedState; this_.updateInteractiveState(savedState); if (success) { // deprecated way of conveying state @@ -202,11 +202,11 @@ export const codapInterface = { resolve(savedState); } else { if (!resp) { - reject('Connection request to CODAP timed out.'); + reject("Connection request to CODAP timed out."); } else { reject( (resp[1] && resp[1].values && resp[1].values.error) || - 'unknown failure'); + "unknown failure"); } } if (iCallback) { @@ -214,8 +214,8 @@ export const codapInterface = { } } - var getFrameReq = {action: 'get', resource: 'interactiveFrame'}; - var newFrame = { + const getFrameReq = {action: "get", resource: "interactiveFrame"}; + const newFrame = { name: iConfig.name, title: iConfig.title, version: iConfig.version, @@ -223,9 +223,9 @@ export const codapInterface = { preventBringToFront: iConfig.preventBringToFront, preventDataContextReorg: iConfig.preventDataContextReorg }; - var updateFrameReq = { - action: 'update', - resource: 'interactiveFrame', + const updateFrameReq = { + action: "update", + resource: "interactiveFrame", values: newFrame }; @@ -236,7 +236,7 @@ export const codapInterface = { notificationHandler, "data-interactive", window.parent); if (!config.customInteractiveStateHandler) { - this_.on('get', 'interactiveState', function () { + this_.on("get", "interactiveState", function () { return ({success: true, values: this_.getInteractiveState()}); }.bind(this_)); } @@ -252,13 +252,13 @@ export const codapInterface = { * Current known state of the connection * @param {'preinit' || 'init' || 'active' || 'inactive' || 'closed'} */ - getConnectionState: function () {return connectionState;}, + getConnectionState () {return connectionState;}, - getStats: function () { + getStats () { return stats; }, - getConfig: function () { + getConfig () { return config; }, @@ -267,7 +267,7 @@ export const codapInterface = { * * @returns {object} */ - getInteractiveState: function () { + getInteractiveState () { return interactiveState; }, @@ -275,14 +275,14 @@ export const codapInterface = { * Updates the interactive state. * @param iInteractiveState {Object} */ - updateInteractiveState: function (iInteractiveState: any) { + updateInteractiveState (iInteractiveState: any) { if (!iInteractiveState) { return; } interactiveState = Object.assign(interactiveState, iInteractiveState); }, - destroy: function () { + destroy () { // todo : more to do? connection = null; }, @@ -298,28 +298,28 @@ export const codapInterface = { * * @return {Promise} The promise of the response from CODAP. */ - sendRequest: function (message: any, callback?: any) { + sendRequest (message: any, callback?: any) { return new Promise(function (resolve, reject){ - function handleResponse (request: any, response: {success: boolean} | undefined, callback: (arg0: any, arg1: any) => void) { + function handleResponse (request: any, response: {success: boolean} | undefined, cb: (arg0: any, arg1: any) => void) { if (response === undefined) { // console.warn('handleResponse: CODAP request timed out'); - reject('handleResponse: CODAP request timed out: ' + JSON.stringify(request)); + reject("handleResponse: CODAP request timed out: " + JSON.stringify(request)); stats.countDiRplTimeout++; } else { - connectionState = 'active'; + connectionState = "active"; if (response.success) { stats.countDiRplSuccess++; } else { stats.countDiRplFail++; } resolve(response); } - if (callback) { - callback(response, request); + if (cb) { + cb(response, request); } } switch (connectionState) { - case 'closed': // log the message and ignore + case "closed": // log the message and ignore // console.warn('sendRequest on closed CODAP connection: ' + JSON.stringify(message)); - reject('sendRequest on closed CODAP connection: ' + JSON.stringify(message)); + reject("sendRequest on closed CODAP connection: " + JSON.stringify(message)); break; - case 'preinit': // warn, but issue request. + case "preinit": // warn, but issue request. // console.log('sendRequest on not yet initialized CODAP connection: ' + // JSON.stringify(message)); /* falls through */ @@ -351,17 +351,17 @@ export const codapInterface = { * 'move', 'resize', .... If not specified, all operations will be reported. * @param handler {Function} A handler to receive the notifications. */ - on: function (actionSpec: string, resourceSpec: string, operation: string | (() => void), handler?: () => void) { - var as = 'notify', + on (actionSpec: string, resourceSpec: string, operation: string | (() => void), handler?: () => void) { + let as = "notify", rs, os, hn; - var args = Array.prototype.slice.call(arguments); - if (['get', 'update', 'notify'].indexOf(args[0]) >= 0) { + const args = Array.prototype.slice.call(arguments); + if (["get", "update", "notify"].indexOf(args[0]) >= 0) { as = args.shift(); } rs = args.shift(); - if (typeof args[0] !== 'function') { + if (typeof args[0] !== "function") { os = args.shift(); } hn = args.shift(); @@ -385,13 +385,13 @@ export const codapInterface = { * @param {String} iResource * @return {Object} */ - parseResourceSelector: function (iResource: string) { - var selectorRE = /([A-Za-z0-9_-]+)\[([^\]]+)]/; - var result: any = {}; - var selectors = iResource.split('.'); + parseResourceSelector (iResource: string) { + const selectorRE = /([A-Za-z0-9_-]+)\[([^\]]+)]/; + const result: any = {}; + const selectors = iResource.split("."); selectors.forEach(function (selector: string) { - var resourceType, resourceName; - var match = selectorRE.exec(selector); + let resourceType, resourceName; + const match = selectorRE.exec(selector); if (selectorRE.test(selector) && match) { resourceType = match[1]; resourceName = match[2]; diff --git a/src/codap-plugin-api.ts b/src/codap-plugin-api.ts index 3be285c..8bdaa67 100644 --- a/src/codap-plugin-api.ts +++ b/src/codap-plugin-api.ts @@ -1,2 +1,2 @@ export {IConfig, codapInterface} from "./api/codap-interface"; -export {initializePlugin, createDataContext, openTable, addData, createItem, createItems} from "./api/codap-helper"; \ No newline at end of file +export {initializePlugin, createDataContext, openTable, addData, createItem, createItems} from "./api/codap-helper"; diff --git a/src/typings.d.ts b/src/typings.d.ts index 5873f77..cd8fdd1 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -1 +1 @@ -declare module 'iframe-phone'; \ No newline at end of file +declare module "iframe-phone"; From aa9ca1ac00591a7c73b4d5324b49a2774a24bacd Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 16 Nov 2023 09:41:47 -0500 Subject: [PATCH 4/9] Fix webpack errors. --- webpack.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index e3816f5..0f70639 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,8 +3,8 @@ module.exports = (env, argv) => { return { context: __dirname, // to automatically find tsconfig.json - devtool: devMode ? 'eval-cheap-module-source-map' : 'source-map', - entry: './src/codap-plugin-api.tsx', + devtool: 'source-map', + entry: './src/codap-plugin-api.ts', mode: 'development', output: { filename: 'codap-plugin-api.js', From 1f1aac5d053a137d902417d541f9eb26622bae3a Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 16 Nov 2023 09:45:53 -0500 Subject: [PATCH 5/9] Add test file. --- src/test/setUpTests.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/test/setUpTests.ts diff --git a/src/test/setUpTests.ts b/src/test/setUpTests.ts new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/src/test/setUpTests.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; From 8f6baa347e6f862469b3f7a8543f2989225344a3 Mon Sep 17 00:00:00 2001 From: lublagg <89816272+lublagg@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:49:32 -0500 Subject: [PATCH 6/9] Rename setUpTests.ts to setupTests.ts --- src/test/{setUpTests.ts => setupTests.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/{setUpTests.ts => setupTests.ts} (100%) diff --git a/src/test/setUpTests.ts b/src/test/setupTests.ts similarity index 100% rename from src/test/setUpTests.ts rename to src/test/setupTests.ts From 65062246af7ebd096faae8f63c07757d4f5561ae Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 16 Nov 2023 11:24:45 -0500 Subject: [PATCH 7/9] Add 'passWithNoTests' to ci. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc0bd45..8269a24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - name: Build run: npm run build - name: Run Tests - run: npm run test:coverage -- --runInBand + run: npm run test:coverage -- --runInBand --passWithNoTests - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: From 4fdee13a38edcf41fb421e7fa64f20a09a91cb17 Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 16 Nov 2023 11:34:39 -0500 Subject: [PATCH 8/9] Update cypress in ci. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8269a24..802a36c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - uses: cypress-io/github-action@v4 with: start: npm start - wait-on: 'http://localhost:8080' + wait-on: 'http://localhost:8080/codap-plugin-api.js' # only record the results to dashboard.cypress.io if CYPRESS_RECORD_KEY is set record: ${{ !!secrets.CYPRESS_RECORD_KEY }} # only do parallel if we have a record key From 38c841a6ef38c7f1d443c6ed44f4c92db75e110f Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 16 Nov 2023 11:44:58 -0500 Subject: [PATCH 9/9] Remove cypress from tests --- .github/workflows/ci.yml | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 802a36c..a843819 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,32 +29,4 @@ jobs: fail-fast: false matrix: # run 3 copies of the current job in parallel - containers: [1, 2, 3] - steps: - - name: Checkout - uses: actions/checkout@v2 - - uses: cypress-io/github-action@v4 - with: - start: npm start - wait-on: 'http://localhost:8080/codap-plugin-api.js' - # only record the results to dashboard.cypress.io if CYPRESS_RECORD_KEY is set - record: ${{ !!secrets.CYPRESS_RECORD_KEY }} - # only do parallel if we have a record key - parallel: ${{ !!secrets.CYPRESS_RECORD_KEY }} - env: - # pass the Dashboard record key as an environment variable - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - # pass GitHub token to allow accurately detecting a build vs a re-run build - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # turn on code coverage when running npm start - # so far we've been using a webpack coverage-istanbul-loader for this - # but there has been work on using the code coverage support in the browser directly, - # which should be much faster - CODE_COVERAGE: true - # Also turn on the code coverage tasks in cypress itself, these are disabled - # by default. - CYPRESS_coverage: true - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - flags: cypress \ No newline at end of file + containers: [1, 2, 3] \ No newline at end of file