From 46fdcb85a92f37094c13c1dab478e736c2e23b5f Mon Sep 17 00:00:00 2001 From: James Li Date: Wed, 22 May 2019 16:59:04 +1000 Subject: [PATCH 1/6] Broke off code block into separate component --- src/snippets/SnippetItem.jsx | 152 ++----------------- src/snippets/SnippetItemCodeBlock.jsx | 201 ++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 140 deletions(-) create mode 100644 src/snippets/SnippetItemCodeBlock.jsx diff --git a/src/snippets/SnippetItem.jsx b/src/snippets/SnippetItem.jsx index e83db54..e28e3fd 100644 --- a/src/snippets/SnippetItem.jsx +++ b/src/snippets/SnippetItem.jsx @@ -2,67 +2,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { - faChevronDown, faChevronRight, faCopy, faDownload, + faChevronDown, faChevronRight, faDownload, } from '@fortawesome/free-solid-svg-icons'; +import SnippetItemCodeBlock from "./SnippetItemCodeBlock"; + /** * Array of supported languages for snippets */ const snippetLanguages = ['Python', 'R', 'Bash', 'Web Access']; -/** - * Generates code snippet text for a given data URL - * - * @param {string} language Target language to generate snippet for - * @param {string} url URL of source data for snippet - * @param {string} filename Filename for downloaded file in snippet - * @param {string} [publisher] Publisher of source data - * @param {string} [contact] Contact point (e.g. email address) for source data - * @param {string} [license] License governing use of source data - * @param {string} [landingPage] URL to landing page related to source data - */ -function generateSnippetText(language, url, filename, publisher, contact, license, landingPage) { - const commentBlockContents = [ - publisher && `Publisher: ${publisher}`, - contact && `Contact point: ${contact}`, - license && `License: ${license}`, - landingPage && `Full page: ${landingPage}`, - ].filter(line => line); // Removes lines which we don't have any info for - - /** - * @param {string} linePrefix - */ - function getCommentBlock(linePrefix) { - return `${commentBlockContents.map(line => `${linePrefix} ${line}`).join('\n')} \n`; - } - - switch (language) { - case 'Python': - return `${getCommentBlock('#')} -import urllib.request -url = '${url.replace(/'/g, '\\\'')}' -filename = '${filename}' -urllib.request.urlretrieve(url, filename)`; - - case 'R': - return `${getCommentBlock('#')} -url <- "${url.replace(/"/g, '\\"')}" -filename <- "${filename}" -download.file(url, destfile=filename)`; - - case 'Bash': - return `${getCommentBlock('#')} -curl -LO ${url}`; - - case 'Web Access': - return `${getCommentBlock('#')} -${url}`; - - default: - throw new Error(`Language "${language}" not supported`); - } -} - /** * Generates suggested filename for snippet * @@ -77,69 +26,6 @@ function generateSuggestedFilename(url, distId) { return url.split(/[?#]/)[0].replace(/^.*[\\/]/, '') || distId; } -/** - * Selects all text within target element - * - * @param {Element} element Target element to select text within - */ -function selectElementText(element) { - // Go over the selection range - const range = document.createRange(); - range.selectNodeContents(element); - - // Apply selection to the window - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - return selection; -} - -/** - * Generates Promise around handler for copying text to the clipboard - * - * @param {string} text Text to copy to clipboard (not guaranteed; see - * `copyTextToClipboard()`) - * - * @returns {Promise} - */ -function generateClipboardCopyPromise(text) { - // Detect whether we can use the latest Clipboard API methods - if (navigator.clipboard && navigator.clipboard.writeText) { - return navigator.clipboard.writeText(text); - } - - // Use old `execCommand` based API - // NOTE: Requires active selection in the window - return new Promise((resolve, reject) => { - const success = document.execCommand('copy'); - if (success) { - resolve(); - } else { - reject(); - } - }); -} - -/** - * Copies text to clipboard - * - * Note that this will attempt to first use the Clipboard API with given text, - * otherwise will fire a copy event which will only copy the last selected - * text within the document. - * - * @param {string} text Text to copy to clipboard (not guaranteed; see notes) - */ -function copyTextToClipboard(text) { - const textCopyPromise = generateClipboardCopyPromise(text); - - return textCopyPromise - .catch(() => { - // Alert when copy failed - alert('Text was not copied; please copy manually'); - }); -} - export class SnippetItem extends React.Component { static propTypes = { distribution: PropTypes.objectOf(PropTypes.any).isRequired, @@ -224,31 +110,17 @@ export class SnippetItem extends React.Component { { /* TODO: this should be a sub component */ url ? snippetLanguages.map((language) => { - // Creating a reference so that the actual element may be - // referred to for copying text - const snippetTextElementRef = React.createRef(); const filename = generateSuggestedFilename(url, dist.identifier); - const snippetText = generateSnippetText( - language, - url, - filename, - publisher, - contactPoint, - license, - landingPage, - ); - return ( -
-
- {language} - -
- -
- {snippetText} -
-
+ ); }) : ( diff --git a/src/snippets/SnippetItemCodeBlock.jsx b/src/snippets/SnippetItemCodeBlock.jsx new file mode 100644 index 0000000..65937d3 --- /dev/null +++ b/src/snippets/SnippetItemCodeBlock.jsx @@ -0,0 +1,201 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCopy } from '@fortawesome/free-solid-svg-icons'; + +/** + * Generates code snippet text for a given data URL + * + * @param {string} language Target language to generate snippet for + * @param {string} url URL of source data for snippet + * @param {string} filename Filename for downloaded file in snippet + * @param {string} [publisher] Publisher of source data + * @param {string} [contact] Contact point (e.g. email address) for source data + * @param {string} [license] License governing use of source data + * @param {string} [landingPage] URL to landing page related to source data + */ +function generateSnippetText( + language, + url, + filename, + publisher, + contact, + license, + landingPage, +) { + const commentBlockContents = [ + publisher && `Publisher: ${publisher}`, + contact && `Contact point: ${contact}`, + license && `License: ${license}`, + landingPage && `Full page: ${landingPage}`, + ].filter(line => line); // Removes lines which we don't have any info for + + /** + * @param {string} linePrefix + */ + function getCommentBlock(linePrefix) { + return `${commentBlockContents + .map(line => `${linePrefix} ${line}`) + .join('\n')} \n`; + } + + switch (language) { + case 'Python': + return `${getCommentBlock('#')} +import urllib.request +url = '${url.replace(/'/g, "\\'")}' +filename = '${filename}' +urllib.request.urlretrieve(url, filename)`; + + case 'R': + return `${getCommentBlock('#')} +url <- "${url.replace(/"/g, '\\"')}" +filename <- "${filename}" +download.file(url, destfile=filename)`; + + case 'Bash': + return `${getCommentBlock('#')} +curl -LO ${url}`; + + case 'Web Access': + return `${getCommentBlock('#')} +${url}`; + + default: + throw new Error(`Language "${language}" not supported`); + } +} + +/** + * Selects all text within target element + * + * @param {Element} element Target element to select text within + */ +function selectElementText(element) { + // Go over the selection range + const range = document.createRange(); + range.selectNodeContents(element); + + // Apply selection to the window + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + return selection; +} + +/** + * Generates Promise around handler for copying text to the clipboard + * + * @param {string} text Text to copy to clipboard (not guaranteed; see + * `copyTextToClipboard()`) + * + * @returns {Promise} + */ +function generateClipboardCopyPromise(text) { + // Detect whether we can use the latest Clipboard API methods + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + + // Use old `execCommand` based API + // NOTE: Requires active selection in the window + return new Promise((resolve, reject) => { + const success = document.execCommand('copy'); + if (success) { + resolve(); + } else { + reject(); + } + }); +} + +/** + * Copies text to clipboard + * + * Note that this will attempt to first use the Clipboard API with given text, + * otherwise will fire a copy event which will only copy the last selected + * text within the document. + * + * @param {string} text Text to copy to clipboard (not guaranteed; see notes) + */ +function copyTextToClipboard(text) { + const textCopyPromise = generateClipboardCopyPromise(text); + + return textCopyPromise.catch(() => { + // Alert when copy failed + alert('Text was not copied; please copy manually'); + }); +} + +export class SnippetItemCodeBlock extends React.Component { + static propTypes = { + url: PropTypes.string.isRequired, + language: PropTypes.string.isRequired, + filename: PropTypes.string.isRequired, + + publisher: PropTypes.string, + contactPoint: PropTypes.string, + landingPage: PropTypes.string, + license: PropTypes.string, + }; + + static defaultProps = { + publisher: undefined, + contactPoint: undefined, + landingPage: undefined, + license: undefined, + }; + + render() { + const { + url, + language, + filename, + publisher, + contactPoint, + landingPage, + license, + } = this.props; + + const snippetText = generateSnippetText( + language, + url, + filename, + publisher, + contactPoint, + license, + landingPage, + ); + + // Creating a reference so that the actual element may be + // referred to for copying text + const snippetTextElementRef = React.createRef(); + + return ( +
+
+ {language} + +
+ +
+ {snippetText} +
+
+ ); + } +} + +export default SnippetItemCodeBlock; From 851a1a61f30529257c103b84a984241d18baf170 Mon Sep 17 00:00:00 2001 From: James Li Date: Wed, 22 May 2019 17:04:12 +1000 Subject: [PATCH 2/6] Placeholder for format-specific snippets --- src/snippets/SnippetItem.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/snippets/SnippetItem.jsx b/src/snippets/SnippetItem.jsx index e28e3fd..9c43afc 100644 --- a/src/snippets/SnippetItem.jsx +++ b/src/snippets/SnippetItem.jsx @@ -64,7 +64,8 @@ export class SnippetItem extends React.Component { const isCollapsed = this.props.collapsed; /** @type {string | undefined} */ const license = dist.license && dist.license.name; - + /** @type {string | undefined} */ + const format = dist.format; // If collapsed, render only the collapsed portion if (isCollapsed) { @@ -84,6 +85,13 @@ export class SnippetItem extends React.Component { ); } + // TODO: Format specific snippets + switch (format) { + default: { + // TODO: + } + } + return (
  • Date: Wed, 22 May 2019 17:04:46 +1000 Subject: [PATCH 3/6] Changed variable name for clarity --- src/snippets/SnippetItem.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snippets/SnippetItem.jsx b/src/snippets/SnippetItem.jsx index 9c43afc..b2866f4 100644 --- a/src/snippets/SnippetItem.jsx +++ b/src/snippets/SnippetItem.jsx @@ -65,7 +65,7 @@ export class SnippetItem extends React.Component { /** @type {string | undefined} */ const license = dist.license && dist.license.name; /** @type {string | undefined} */ - const format = dist.format; + const fileType = dist.format; // If collapsed, render only the collapsed portion if (isCollapsed) { @@ -86,7 +86,7 @@ export class SnippetItem extends React.Component { } // TODO: Format specific snippets - switch (format) { + switch (fileType) { default: { // TODO: } From 94aeb2ccda7794d9a73374b29f9667b43b72ddb2 Mon Sep 17 00:00:00 2001 From: James Li Date: Mon, 10 Jun 2019 14:41:24 +1000 Subject: [PATCH 4/6] Update base config file --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index de9c351..1ebd3fe 100644 --- a/config/config.json +++ b/config/config.json @@ -18,6 +18,6 @@ "tracking_id": null }, "explorer": { - "esServerUrl": "https://v2-1.knowledgenet.co/api/v0/es-query/datasets" + "esServerUrl": "https://knowledgenet.co/api/v0/es-query/datasets" } } From 7018e48595c35897f2c7acec23ade930a517432a Mon Sep 17 00:00:00 2001 From: James Li Date: Mon, 10 Jun 2019 15:39:02 +1000 Subject: [PATCH 5/6] Whitelisting snippet generation --- src/snippets/SnippetItem.jsx | 91 +++++++----------- src/snippets/SnippetItemCodeSection.jsx | 121 ++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 59 deletions(-) create mode 100644 src/snippets/SnippetItemCodeSection.jsx diff --git a/src/snippets/SnippetItem.jsx b/src/snippets/SnippetItem.jsx index b2866f4..8ae60ea 100644 --- a/src/snippets/SnippetItem.jsx +++ b/src/snippets/SnippetItem.jsx @@ -2,29 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { - faChevronDown, faChevronRight, faDownload, + faChevronDown, + faChevronRight, + faDownload, } from '@fortawesome/free-solid-svg-icons'; -import SnippetItemCodeBlock from "./SnippetItemCodeBlock"; - -/** - * Array of supported languages for snippets - */ -const snippetLanguages = ['Python', 'R', 'Bash', 'Web Access']; - -/** - * Generates suggested filename for snippet - * - * @param {string} url URL of source data for snippet - * @param {string} distId Distribution ID - */ -function generateSuggestedFilename(url, distId) { - // Get the "filename" from the URL where possible, after removal of query - // string or anchor - // - // If the string is blank, then we return the distribution ID - return url.split(/[?#]/)[0].replace(/^.*[\\/]/, '') || distId; -} +import SnippetItemCodeSection from './SnippetItemCodeSection'; export class SnippetItem extends React.Component { static propTypes = { @@ -34,19 +17,19 @@ export class SnippetItem extends React.Component { landingPage: PropTypes.string, collapsed: PropTypes.bool, toggleCollapsed: PropTypes.func.isRequired, - } + }; static defaultProps = { publisher: undefined, contactPoint: undefined, landingPage: undefined, collapsed: false, - } + }; toggleCollapsed = (e) => { this.props.toggleCollapsed(this.props.distribution.identifier); e.preventDefault(); - } + }; render() { const { @@ -78,20 +61,14 @@ export class SnippetItem extends React.Component { onClick={this.toggleCollapsed} onKeyPress={this.toggleCollapsed} > -   + +   {dist.title}
  • ); } - // TODO: Format specific snippets - switch (fileType) { - default: { - // TODO: - } - } - return (
  • - { /* + {/* Store in Workspace   -   */ } - {url && ( Download file )} +   */} + {url && ( + + Download file + + )}

    {dist.description}

    - { /* TODO: this should be a sub component */ - url - ? snippetLanguages.map((language) => { - const filename = generateSuggestedFilename(url, dist.identifier); - return ( - - ); - }) - : ( -
    -
    - No URL available for this resource -
    -
    - ) - } +
  • ); diff --git a/src/snippets/SnippetItemCodeSection.jsx b/src/snippets/SnippetItemCodeSection.jsx new file mode 100644 index 0000000..3d2d3c5 --- /dev/null +++ b/src/snippets/SnippetItemCodeSection.jsx @@ -0,0 +1,121 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SnippetItemCodeBlock from './SnippetItemCodeBlock'; + +/** + * Array of supported languages for snippets + */ +const snippetLanguages = ['Python', 'R', 'Bash', 'Web Access']; + +/** + * Array of supported file types for snippets + */ +const supportedSnippetFileTypes = [ + 'PDF', + 'ZIP', + 'TIFF', + 'XLSX', + 'CSV', + 'JPEG', + 'DOCX', + 'PLAIN', + 'XML', +]; + +/** + * Generates suggested filename for snippet + * + * @param {string} url URL of source data for snippet + * @param {string} distId Distribution ID + */ +function generateSuggestedFilename(url, distId) { + // Get the "filename" from the URL where possible, after removal of query + // string or anchor + // + // If the string is blank, then we return the distribution ID + return url.split(/[?#]/)[0].replace(/^.*[\\/]/, '') || distId; +} + +/** + * Code block content component for `SnippetItem` + */ +class SnippetItemCodeSection extends React.PureComponent { + static propTypes = { + url: PropTypes.string, + distId: PropTypes.string.isRequired, + fileType: PropTypes.string, + publisher: PropTypes.string, + license: PropTypes.string, + landingPage: PropTypes.string, + contactPoint: PropTypes.string, + }; + + static defaultProps = { + url: undefined, + fileType: undefined, + publisher: undefined, + license: undefined, + landingPage: undefined, + contactPoint: undefined, + }; + + render() { + const { + url, + distId, + fileType, + publisher, + license, + landingPage, + contactPoint, + } = this.props; + + if (!url) { + return ( +
    +
    + No URL available for this resource +
    +
    + ); + } + + // We shall only produce a code block for whitelisted file types + if (fileType && supportedSnippetFileTypes.indexOf(fileType) !== -1) { + return ( + <> + {snippetLanguages.map((language) => { + const filename = generateSuggestedFilename(url, distId); + return ( + + ); + })} + + ); + } + + return ( +
    +
    + + # The file type of this resource is not supported; please download + by visiting the below URL. +
    +
    + {url} +
    +
    +
    + ); + } +} + +export default SnippetItemCodeSection; From cfefe1c54ed7ec742772f3f00e4b275ab6a58920 Mon Sep 17 00:00:00 2001 From: James Li Date: Mon, 10 Jun 2019 15:41:11 +1000 Subject: [PATCH 6/6] Removing redundant
    --- src/snippets/SnippetItemCodeSection.jsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/snippets/SnippetItemCodeSection.jsx b/src/snippets/SnippetItemCodeSection.jsx index 3d2d3c5..f0e1e8f 100644 --- a/src/snippets/SnippetItemCodeSection.jsx +++ b/src/snippets/SnippetItemCodeSection.jsx @@ -73,9 +73,7 @@ class SnippetItemCodeSection extends React.PureComponent { if (!url) { return (
    -
    - No URL available for this resource -
    + No URL available for this resource
    ); } @@ -104,15 +102,13 @@ class SnippetItemCodeSection extends React.PureComponent { return (
    -
    - - # The file type of this resource is not supported; please download - by visiting the below URL. -
    -
    - {url} -
    -
    + + # The file type of this resource is not supported; please download by + visiting the below URL. +
    +
    + {url} +
    ); }