From 6c1d4dbda56e7971b90a5a9e0dd16a24b2cb18f0 Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Fri, 23 Jul 2021 01:05:49 +0530 Subject: [PATCH 01/14] Need to be logged in --- src/components/Plugin/Plugin.jsx | 14 +++++++++++++- .../Plugin/components/PluginBody/PluginBody.jsx | 1 - 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 5fe38fdc..8e7ad36d 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -24,6 +24,7 @@ export class Plugin extends Component { const { pluginData, isFavorite } = props; this.state = { pluginData, + versions: null, loading: true, star: isFavorite || undefined, errors: [], @@ -67,7 +68,7 @@ export class Plugin extends Component { if (this.isLoggedIn()) { return this.isFavorite() ? this.unfavPlugin() : this.favPlugin(); } - return Promise.resolve(); + return this.showNotifications(new Error('You need to be logged in!')) } favPlugin = async () => { @@ -132,6 +133,17 @@ export class Plugin extends Component { } } + async fetchPluginVersions() { + + try { + const plugin = await this.client.getPlugin(); + return { ...plugin.data, url: plugin.url }; + } catch (e) { + this.showNotifications(new HttpApiCallError(e)); + return e + } + } + async fetchIsPluginStarred({ name }) { try { const response = await this.client.getPluginStars({ plugin_name: name }); diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index c69dc8f0..8539905c 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -22,7 +22,6 @@ import { sanitize } from 'dompurify'; import './PluginBody.css'; import ErrorNotification from '../../../Notification'; import HttpApiCallError from '../../../../errors/HttpApiCallError'; -// eslint-disable-next-line import/named import { GithubAPIRepoError, GithubAPIProfileError, GithubAPIReadmeError } from '../../../../errors/GithubError'; const PluginBody = ({ pluginData }) => { From 0e3285d8aa509c46c0f97e256942ce966ea6e542 Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Fri, 23 Jul 2021 01:59:02 +0530 Subject: [PATCH 02/14] Versions for install --- src/components/Plugin/Plugin.jsx | 15 +- .../components/PluginBody/PluginBody.jsx | 270 ++++++++++-------- 2 files changed, 159 insertions(+), 126 deletions(-) diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 8e7ad36d..4cd4ddfa 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -24,7 +24,6 @@ export class Plugin extends Component { const { pluginData, isFavorite } = props; this.state = { pluginData, - versions: null, loading: true, star: isFavorite || undefined, errors: [], @@ -40,9 +39,10 @@ export class Plugin extends Component { async componentDidMount() { let { pluginData } = this.state; - if (!pluginData) { + if (!pluginData) pluginData = await this.fetchPluginData(); - } + else + this.fetchPluginVersions(pluginData.name) this.setState({ pluginData, loading: false }); if (this.isLoggedIn()) { @@ -133,11 +133,12 @@ export class Plugin extends Component { } } - async fetchPluginVersions() { - + async fetchPluginVersions(name) { try { - const plugin = await this.client.getPlugin(); - return { ...plugin.data, url: plugin.url }; + const versions = await this.client.getPlugins({ limit: 10e6, name }); + return this.setState((prevState) => + ({ pluginData: { ...prevState.pluginData, versions: versions.data } }) + ); } catch (e) { this.showNotifications(new HttpApiCallError(e)); return e diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index 8539905c..72665176 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -2,7 +2,6 @@ // Required for setting README html import React, { useState, useEffect, useCallback } from 'react'; -import { Link } from 'react-router-dom'; import { Grid, GridItem, @@ -13,6 +12,7 @@ import { Popover, ClipboardCopy, Button, + ExpandableSection, } from '@patternfly/react-core'; import { DownloadIcon, UserAltIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; @@ -80,7 +80,38 @@ const PluginBody = ({ pluginData }) => { fetchRepo(); }, [fetchReadme, fetchRepoData, pluginData.public_repo, showNotifications]); - const versionString = (v) => `Version ${v}${v === pluginData.version ? ' (This)' : ''}`; + const InstallButton = () => { + if (pluginData.version) + return <> +

Version { pluginData.version }

+ + { `${process.env.REACT_APP_STORE_URL}plugins/${pluginData.id}/` } + + + + + if (pluginData.versions) + return <> +

Version { pluginData.versions[0].version }

+ + { `${process.env.REACT_APP_STORE_URL}plugins/${pluginData.versions[0].id}/` } + +
+ + { + pluginData.versions.slice(1).map((version) =>( +
+ + Version {version.version} + +
+ )) + } +
+ + + return

Loading

+ } return ( <> @@ -100,132 +131,130 @@ const PluginBody = ({ pluginData }) => {
- - Overview}> - - + + + + Overview}>
README
{ readme ?
: null } - - -
-
-

Install

-

Click to install this plugin to your ChRIS Server.

-
- Install to your ChRIS server} - bodyContent={() => ( -
-

- Copy and Paste the URL below into your ChRIS Admin Dashboard - to install this plugin. -

-
- + + + Parameters}> + + Parameters content + + + + { + pluginData.versions && + Versions}> + + + { + pluginData.versions ? ( + <> +

Versions of this plugin

{ - pluginData.url ? pluginData.url - : `${process.env.REACT_APP_STORE_URL}plugins/${pluginData.id}/` + pluginData.versions.map((version) => ( + + )) } -
-
- )} - > - -
-
- -
-

Contributors

- { - pluginData.authors.map((author) => ( - -

- - {' '} - {author} -

-
- )) - } - -
- - View all contributors - -
-
-

Plugin ID

- {pluginData.id} -
-
-

License

- { repoData ? repoData.license.name : pluginData.license } -
-
-

Content Type

- {pluginData.type} -
-
-

Date added

- {(new Date(pluginData.creation_date)).toDateString()} -
-
-
- - - - Parameters}> - - Parameters content - - - - Versions}> - - - { - pluginData.versions !== undefined ? ( - <> -

Versions of this plugin

- { - Object.keys(pluginData.versions).length > 1 - ? Object.keys(pluginData.versions).map((version) => ( -
- - {versionString(version)} - -
- )) : ( + + ) : (
- {versionString(Object.keys(pluginData.versions).toString())} +

This is the only version of this plugin.

) - } - - ) : ( + } +
+
+
+ } + + + + +
+
+

Install

+

Click to install this plugin to your ChRIS Server.

+
+ Install to your ChRIS server} + bodyContent={() => (
-

This is the only version of this plugin.

+

+ Copy and Paste the URL below into your ChRIS Admin Dashboard + to install this plugin. +

+
+
- ) - } - - - - + )} + > + +
+
+ + + { + Array.isArray(pluginData.authors) && +
+

Contributors

+ { + pluginData.authors.map((author) => ( + +

+ + {' '} + {author} +

+
+ )) + } + +
+ + View all contributors + +
+ } + + {/*
+

Plugin ID

+ {pluginData.id} +
*/} + +
+

License

+ { repoData ? repoData.license.name : pluginData.license } +
+
+

Content Type

+ {pluginData.type} +
+
+

Date added

+ {(new Date(pluginData.creation_date)).toDateString()} +
+
+
+
@@ -242,6 +271,9 @@ PluginBody.propTypes = { type: PropTypes.string, authorURL: PropTypes.string, authors: PropTypes.arrayOf(PropTypes.string), + versions: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.any), PropTypes.any + ]) }).isRequired, }; From 336bda0b04354c18b8fcf81d4e2489d043ec6731 Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Fri, 23 Jul 2021 17:36:08 +0530 Subject: [PATCH 03/14] Incorporate requested changes --- src/components/Plugin/Plugin.jsx | 18 +++++--- .../components/PluginBody/PluginBody.jsx | 42 ++++++++----------- src/components/Plugins/Plugins.jsx | 13 +----- src/utils/common.js | 16 +++++++ 4 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 4cd4ddfa..139529ca 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -66,9 +66,13 @@ export class Plugin extends Component { onStarClicked = () => { if (this.isLoggedIn()) { - return this.isFavorite() ? this.unfavPlugin() : this.favPlugin(); + if (this.isFavorite()) + this.unfavPlugin(); + else + this.favPlugin(); } - return this.showNotifications(new Error('You need to be logged in!')) + else + this.showNotifications(new Error('You need to be logged in!')) } favPlugin = async () => { @@ -136,9 +140,13 @@ export class Plugin extends Component { async fetchPluginVersions(name) { try { const versions = await this.client.getPlugins({ limit: 10e6, name }); - return this.setState((prevState) => - ({ pluginData: { ...prevState.pluginData, versions: versions.data } }) - ); + return this.setState((prevState) => ({ + pluginData: { + ...prevState.pluginData, + versions: versions.data, + url: versions.url, + } + })); } catch (e) { this.showNotifications(new HttpApiCallError(e)); return e diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index 72665176..8c87c692 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -23,6 +23,7 @@ import './PluginBody.css'; import ErrorNotification from '../../../Notification'; import HttpApiCallError from '../../../../errors/HttpApiCallError'; import { GithubAPIRepoError, GithubAPIProfileError, GithubAPIReadmeError } from '../../../../errors/GithubError'; +import { removeEmail } from '../../../../utils/common'; const PluginBody = ({ pluginData }) => { const [activeTab, setActiveTab] = useState(1); @@ -85,7 +86,7 @@ const PluginBody = ({ pluginData }) => { return <>

Version { pluginData.version }

- { `${process.env.REACT_APP_STORE_URL}plugins/${pluginData.id}/` } + {pluginData.url + pluginData.id} @@ -94,7 +95,7 @@ const PluginBody = ({ pluginData }) => { return <>

Version { pluginData.versions[0].version }

- { `${process.env.REACT_APP_STORE_URL}plugins/${pluginData.versions[0].id}/` } + {pluginData.url + pluginData.versions[0].id}
@@ -212,33 +213,26 @@ const PluginBody = ({ pluginData }) => { - { - Array.isArray(pluginData.authors) && -
-

Contributors

- { +
+

Contributors

+ { + Array.isArray(pluginData.authors) ? pluginData.authors.map((author) => ( -

- - {' '} - {author} -

+

{author}

)) - } - -
- - View all contributors - -
- } + : + +

{removeEmail(pluginData.authors)}

+
+ } - {/*
-

Plugin ID

- {pluginData.id} -
*/} +
+ + View all contributors + +

License

diff --git a/src/components/Plugins/Plugins.jsx b/src/components/Plugins/Plugins.jsx index 865ad2c1..762ae34e 100644 --- a/src/components/Plugins/Plugins.jsx +++ b/src/components/Plugins/Plugins.jsx @@ -25,23 +25,12 @@ import NotFound from '../NotFound/NotFound'; import ChrisStore from '../../store/ChrisStore'; import HttpApiCallError from '../../errors/HttpApiCallError'; import ErrorNotification from '../Notification'; +import { removeEmail } from '../../utils/common'; import './Plugins.css'; const CATEGORIES = ['FreeSurfer', 'MRI', 'Segmentation']; -// Remove email from author -export const removeEmail = (authors) => { - const emailRegex = /(<|\().+?@.{2,}?\..{2,}?(>|\))/g; - // Match '<' or '(' at the beginning and end - // Match @. inside brackets - if (!Array.isArray(authors)) - // eslint-disable-next-line no-param-reassign - authors = [ authors ] - - return authors.map((author) => author.replace(emailRegex, "").trim()); -} - /** * A page showing a list of ChRIS plugins, according to the search * specified in the URI's query string. diff --git a/src/utils/common.js b/src/utils/common.js index efc36ab2..b5dbff10 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -18,4 +18,20 @@ export const debounce = (fn, delay = 250) => { } } +/** + * Remove email from author + * @param {*} authors List or string + * @returns Just names of plugin authors + */ +export const removeEmail = (authors) => { + const emailRegex = /(<|\().+?@.{2,}?\..{2,}?(>|\))/g; + // Match '<' or '(' at the beginning and end + // Match @. inside brackets + if (!Array.isArray(authors)) + // eslint-disable-next-line no-param-reassign + authors = [ authors ] + + return authors.map((author) => author.replace(emailRegex, "").trim()); +} + export default debounce; \ No newline at end of file From d7880d30624b87bb42470fa511cb6d280c30dd2e Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Mon, 26 Jul 2021 22:56:28 +0530 Subject: [PATCH 04/14] Dont build URL from template --- .../components/PluginBody/PluginBody.jsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index 8c87c692..d8dfbc0a 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -84,9 +84,8 @@ const PluginBody = ({ pluginData }) => { const InstallButton = () => { if (pluginData.version) return <> -

Version { pluginData.version }

- {pluginData.url + pluginData.id} + {pluginData.url} @@ -155,15 +154,11 @@ const PluginBody = ({ pluginData }) => { pluginData.versions ? ( <>

Versions of this plugin

- { - pluginData.versions.map((version) => ( - - )) - } + { pluginData.versions.map((version) => ( + + Version { version.version } + + ))} ) : (
@@ -264,7 +259,9 @@ PluginBody.propTypes = { public_repo: PropTypes.string, type: PropTypes.string, authorURL: PropTypes.string, - authors: PropTypes.arrayOf(PropTypes.string), + authors: PropTypes.oneOfType([ + PropTypes.string, PropTypes.arrayOf(PropTypes.string) + ]), versions: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.any), PropTypes.any ]) From 4f622adce83b343078baf589293473f95fd0aa74 Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Tue, 27 Jul 2021 00:41:42 +0530 Subject: [PATCH 05/14] Minor fix --- .../Plugin/components/PluginBody/PluginBody.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index d8dfbc0a..456aadf3 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -97,17 +97,18 @@ const PluginBody = ({ pluginData }) => { {pluginData.url + pluginData.versions[0].id}
- - { - pluginData.versions.slice(1).map((version) =>( + { + pluginData.versions.length > 1 && + + { pluginData.versions.slice(1).map((version) =>( - )) - } - + ))} + + } return

Loading

From 976b7e2b9dfaaf8a4cd47799c2d5b15e0c2ddede Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Tue, 27 Jul 2021 02:28:54 +0530 Subject: [PATCH 06/14] Fix 2nd page plugins showing 404 --- src/components/Plugin/Plugin.jsx | 2 +- .../components/PluginBody/PluginBody.jsx | 24 +++++------ src/components/Plugins/Plugins.jsx | 40 +++++++++++-------- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 139529ca..b4691c9e 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -139,7 +139,7 @@ export class Plugin extends Component { async fetchPluginVersions(name) { try { - const versions = await this.client.getPlugins({ limit: 10e6, name }); + const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); return this.setState((prevState) => ({ pluginData: { ...prevState.pluginData, diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index 456aadf3..96b0ffac 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -116,19 +116,17 @@ const PluginBody = ({ pluginData }) => { return ( <> - { - errors.map((message, index) => ( - { - setErrors(errors.splice(index)); - }} - /> - )) - } + { errors.map((message, index) => ( + { + setErrors(errors.splice(index)); + }} + /> + ))}
diff --git a/src/components/Plugins/Plugins.jsx b/src/components/Plugins/Plugins.jsx index 762ae34e..48ecad06 100644 --- a/src/components/Plugins/Plugins.jsx +++ b/src/components/Plugins/Plugins.jsx @@ -68,15 +68,21 @@ export class Plugins extends Component { } /** - * Re-fetch the list of plugins if the input was changed - * in the NarBar's search bar. + * @todo + * Causes issues, will be reviewed later. */ - async componentDidUpdate(prevProps) { - // eslint-disable-next-line react/destructuring-assignment - if (this.props.location !== prevProps.location) { - await this.refreshPluginList(); - } - } + // /** + // * Re-fetch the list of plugins if the input was changed + // * in the NarBar's search bar. + // * @param {*} pluginId + // * @param {*} star + // */ + // async componentDidUpdate(prevProps) { + // // eslint-disable-next-line react/destructuring-assignment + // if (this.props.location !== prevProps.location) { + // await this.refreshPluginList(); + // } + // } /** * Add a star next to the plugin visually. @@ -352,21 +358,23 @@ export class Plugins extends Component { return ( - { - if (loading) - return - + { window.scrollTo(0,0); - const { name: query } = routeProps.match.params - if (pluginList.has(query)) { - const plugin = pluginList.get(query); + const { name } = routeProps.match.params + + if (loading) return + + if (pluginList.has(name)) { + const plugin = pluginList.get(name); return } + this.setState({ loading: true }) + this.refreshPluginList({ name }) return }} /> - {error && ( + { error && ( Date: Tue, 27 Jul 2021 02:35:21 +0530 Subject: [PATCH 07/14] name_exact --- src/components/Plugins/Plugins.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Plugins/Plugins.jsx b/src/components/Plugins/Plugins.jsx index 48ecad06..5e59e9f6 100644 --- a/src/components/Plugins/Plugins.jsx +++ b/src/components/Plugins/Plugins.jsx @@ -369,7 +369,7 @@ export class Plugins extends Component { return } this.setState({ loading: true }) - this.refreshPluginList({ name }) + this.refreshPluginList({ name_exact: name }) return }} /> From 32e21efb194b72ac8ddac5b2af86d1cd6dd64c6a Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Tue, 27 Jul 2021 03:03:21 +0530 Subject: [PATCH 08/14] Added documentation comments --- src/components/Plugin/Plugin.jsx | 18 ++++++++++++++++++ src/components/Plugins/Plugins.jsx | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index b4691c9e..9ae654fb 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -39,8 +39,20 @@ export class Plugin extends Component { async componentDidMount() { let { pluginData } = this.state; + /** + * When the user opens this page from `/plugins` or `/plugins/`, + * the incoming prop has a value and we use that. + * + * If the incoming prop does not have a value, we assume + * we are on `/p/` and we fetch by ID `name_exact=`. + */ if (!pluginData) pluginData = await this.fetchPluginData(); + /** + * If pluginData was fetched (by ID), it will have a version, hence continue. + * If not, we fetch all plugins with `name_exact=` + * and select plugin.versions[0] from that to show on the install button. + */ else this.fetchPluginVersions(pluginData.name) @@ -137,6 +149,12 @@ export class Plugin extends Component { } } + /** + * Fetch all versions of a plugin by name. + * + * @param {*} name Plugin name + * @returns Promise => void + */ async fetchPluginVersions(name) { try { const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); diff --git a/src/components/Plugins/Plugins.jsx b/src/components/Plugins/Plugins.jsx index 5e59e9f6..112bc858 100644 --- a/src/components/Plugins/Plugins.jsx +++ b/src/components/Plugins/Plugins.jsx @@ -34,6 +34,9 @@ const CATEGORIES = ['FreeSurfer', 'MRI', 'Segmentation']; /** * A page showing a list of ChRIS plugins, according to the search * specified in the URI's query string. + * + * When the user opens /plugins, all plugin metas are fetched, with pagination. + * so that we already have the data to immediately populate each plugin body. */ export class Plugins extends Component { constructor(props) { @@ -364,6 +367,14 @@ export class Plugins extends Component { if (loading) return + /** + * When the user opens this route from `/plugins`, the pluginList Map + * has the item and we return the ConnectedPlugin. + * + * When the user opens this route directly, the pluginList Map + * does not have the item and we we fetch by `name_exact=name`. + */ + if (pluginList.has(name)) { const plugin = pluginList.get(name); return From 89a4d3af7b885675b91dd1ccbf55d04ec83f35eb Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Tue, 27 Jul 2021 14:37:10 +0530 Subject: [PATCH 09/14] Fixed no more building plugin install URL --- .../Navbar/components/Search/Search.jsx | 2 +- src/components/Plugin/Plugin.jsx | 9 +++-- .../components/PluginBody/PluginBody.jsx | 12 +++---- src/components/Plugins/Plugins.jsx | 34 +++++++++---------- src/components/Router/Router.jsx | 5 ++- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/components/Navbar/components/Search/Search.jsx b/src/components/Navbar/components/Search/Search.jsx index 274c99d7..48c6a662 100644 --- a/src/components/Navbar/components/Search/Search.jsx +++ b/src/components/Navbar/components/Search/Search.jsx @@ -85,7 +85,7 @@ const Search = ({ onChange={onChange} autoComplete="off" onKeyDown={(e) => { - if (e.key === 'Enter' && value.length >= 3 && !autoCompleteData.length) { + if (e.key === 'Enter' && value.length >= 3) { onSearch(value, 'ENTER'); setShowAutoComplete(false); } diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 9ae654fb..221413c9 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -152,17 +152,20 @@ export class Plugin extends Component { /** * Fetch all versions of a plugin by name. * - * @param {*} name Plugin name + * @param {string} name Plugin name * @returns Promise => void */ async fetchPluginVersions(name) { try { const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); + const firstplg = await this.client.getPlugin(parseInt(versions.data[0].id, 10)); return this.setState((prevState) => ({ pluginData: { ...prevState.pluginData, - versions: versions.data, - url: versions.url, + versions: [ + { ...versions.data[0], url: firstplg.url }, + ...versions.data.slice(1) + ], } })); } catch (e) { diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index 96b0ffac..66f3df4e 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -13,6 +13,7 @@ import { ClipboardCopy, Button, ExpandableSection, + Spinner, } from '@patternfly/react-core'; import { DownloadIcon, UserAltIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; @@ -83,18 +84,13 @@ const PluginBody = ({ pluginData }) => { const InstallButton = () => { if (pluginData.version) - return <> - - {pluginData.url} - - - + return { pluginData.url } if (pluginData.versions) return <>

Version { pluginData.versions[0].version }

- {pluginData.url + pluginData.versions[0].id} + { pluginData.versions[0].url }
{ @@ -111,7 +107,7 @@ const PluginBody = ({ pluginData }) => { } - return

Loading

+ return } return ( diff --git a/src/components/Plugins/Plugins.jsx b/src/components/Plugins/Plugins.jsx index 112bc858..9bb3cc16 100644 --- a/src/components/Plugins/Plugins.jsx +++ b/src/components/Plugins/Plugins.jsx @@ -71,21 +71,17 @@ export class Plugins extends Component { } /** - * @todo - * Causes issues, will be reviewed later. + * Re-fetch the list of plugins if the input was changed + * in the NarBar's search bar. + * @param {*} pluginId + * @param {*} star */ - // /** - // * Re-fetch the list of plugins if the input was changed - // * in the NarBar's search bar. - // * @param {*} pluginId - // * @param {*} star - // */ - // async componentDidUpdate(prevProps) { - // // eslint-disable-next-line react/destructuring-assignment - // if (this.props.location !== prevProps.location) { - // await this.refreshPluginList(); - // } - // } + async componentDidUpdate(prevProps) { + // eslint-disable-next-line react/destructuring-assignment + if (this.props.location !== prevProps.location) { + await this.refreshPluginList(); + } + } /** * Add a star next to the plugin visually. @@ -141,13 +137,19 @@ export class Plugins extends Component { async refreshPluginList(search = {}) { const params = new URLSearchParams(window.location.search) const name = params.get('q'); + const { match } = this.props; + const searchParams = { limit: 20, offset: 0, - name_title_category: name, ...search }; + if (name) + searchParams.name_title_category = name; + if (match.params.name) + searchParams.name_exact = match.params.name; + let plugins; try { plugins = await this.client.getPluginMetas(searchParams); @@ -379,8 +381,6 @@ export class Plugins extends Component { const plugin = pluginList.get(name); return } - this.setState({ loading: true }) - this.refreshPluginList({ name_exact: name }) return }} /> diff --git a/src/components/Router/Router.jsx b/src/components/Router/Router.jsx index d262934d..e364946b 100644 --- a/src/components/Router/Router.jsx +++ b/src/components/Router/Router.jsx @@ -16,9 +16,12 @@ const Router = () => ( + + - + + From c81f6bf1fb03b2d166bfcf9743bffd85a1a20ad2 Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Fri, 30 Jul 2021 19:40:33 +0530 Subject: [PATCH 10/14] Got rid of nested Switch, predictable behaviour --- src/components/Plugin/Plugin.jsx | 54 +-- src/components/Plugin/Plugin.test.jsx | 369 ------------------ src/components/Plugin/PluginMeta.jsx | 272 +++++++++++++ .../components/PluginBody/PluginBody.jsx | 42 +- src/components/Plugins/Plugins.jsx | 255 ++++++------ src/components/Router/Router.jsx | 12 +- 6 files changed, 419 insertions(+), 585 deletions(-) delete mode 100644 src/components/Plugin/Plugin.test.jsx create mode 100644 src/components/Plugin/PluginMeta.jsx diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 221413c9..5d75a0f7 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -32,29 +32,11 @@ export class Plugin extends Component { const storeURL = process.env.REACT_APP_STORE_URL; const auth = { token: props.store.get('authToken') }; this.client = new Client(storeURL, auth); - - this.fetchPluginData = this.fetchPluginData.bind(this); } async componentDidMount() { - let { pluginData } = this.state; - - /** - * When the user opens this page from `/plugins` or `/plugins/`, - * the incoming prop has a value and we use that. - * - * If the incoming prop does not have a value, we assume - * we are on `/p/` and we fetch by ID `name_exact=`. - */ - if (!pluginData) - pluginData = await this.fetchPluginData(); - /** - * If pluginData was fetched (by ID), it will have a version, hence continue. - * If not, we fetch all plugins with `name_exact=` - * and select plugin.versions[0] from that to show on the install button. - */ - else - this.fetchPluginVersions(pluginData.name) + const pluginData = await this.fetchPlugin(); + this.fetchPluginVersions(pluginData.name) this.setState({ pluginData, loading: false }); if (this.isLoggedIn()) { @@ -136,7 +118,7 @@ export class Plugin extends Component { return ; } - async fetchPluginData() { + async fetchPlugin() { // eslint-disable-next-line react/destructuring-assignment const { pluginId } = this.props.match.params; @@ -258,7 +240,6 @@ export class Plugin extends Component { ); } - const { className } = this.props; return ( <> { @@ -276,7 +257,7 @@ export class Plugin extends Component { /> )) } -
+
{container}
@@ -286,36 +267,21 @@ export class Plugin extends Component { } Plugin.propTypes = { + store: PropTypes.objectOf(PropTypes.object), match: PropTypes.shape({ params: PropTypes.shape({ plugin: PropTypes.string, - }), - }), - pluginData: PropTypes.shape({ - plugin: PropTypes.string, - pluginURL: PropTypes.string, - authorURL: PropTypes.string, - title: PropTypes.string, - id: PropTypes.number, - description: PropTypes.string, - dock_image: PropTypes.string, - modification_date: PropTypes.string, - authors: PropTypes.arrayOf(PropTypes.string), - version: PropTypes.string, - }), - className: PropTypes.string, - store: PropTypes.objectOf(PropTypes.object), + }) + }) }; Plugin.defaultProps = { + store: new Map(), match: { params: { plugin: undefined, - }, - }, - pluginData: null, - className: '', - store: new Map(), + } + } }; export default ChrisStore.withStore(Plugin); diff --git a/src/components/Plugin/Plugin.test.jsx b/src/components/Plugin/Plugin.test.jsx deleted file mode 100644 index 6b715118..00000000 --- a/src/components/Plugin/Plugin.test.jsx +++ /dev/null @@ -1,369 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { Plugin } from './Plugin'; - -// define mock for @fnndsc/chrisstoreapi module -jest.mock('@fnndsc/chrisstoreapi', () => require.requireActual('../__mocks__/chrisstoreapi').default); - -describe('Plugin', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallow(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should render a plugin div', () => { - expect(wrapper.find('div.plugin')).toHaveLength(1); - }); - - it('should render plugin div based on className prop', () => { - expect(wrapper.find('div.plugin.testClass')).toHaveLength(0); - wrapper.setProps({ className: 'testClass' }); - expect(wrapper.find('div.plugin.testClass')).toHaveLength(1); - }); - - /* ============================== */ - /* =========== METHODS ========== */ - /* ============================== */ - - it('sets mounted to true on after mounting', () => { - expect(Plugin.prototype.mounted).toBeFalsy(); - Plugin.prototype.componentWillMount(); - expect(Plugin.prototype.mounted).toBeTruthy(); - }); - - it('sets mounted to false after componentWillUnmount is called', () => { - const instance = wrapper.instance(); - expect(instance.mounted).toBeTruthy(); - instance.componentWillUnmount(); - expect(instance.mounted).toBeFalsy(); - }); - - /* ============================== */ - /* ==== FETCH PLUGIN DATA FN ==== */ - /* ============================== */ - - it('should have fetchPluginData function', () => { - const { fetchPluginData } = wrapper.instance(); - expect(fetchPluginData).toBeDefined(); - }); - - it('should be able to fetch plugin data', () => { - const { fetchPluginData } = wrapper.instance(); - - wrapper.setProps({ - match: { - params: { - plugin: '1', - }, - }, - }); - - return fetchPluginData().then((pluginData) => { - expect(pluginData).toBeDefined(); - expect(pluginData.name).toEqual('testName1'); - }); - }); - - const propsData = { - modification_date: '3/14/15', - creation_date: '9/26/53', - version: '0.1', - authors: 'testAuthor (user@domain.com)', - }; - - it('should set pluginData state to pluginData props if passed', () => { - const wrapperWithoutProps = shallow(); - expect(wrapperWithoutProps.state('pluginData')).toBeNull(); - const wrapperWithProps = shallow(); - expect(wrapperWithProps.state('pluginData')).toEqual(propsData); - }); -}); - -/* ============================== */ -/* ===== WITHOUT PLUGIN DATA ==== */ -/* ============================== */ - -describe('Plugin without data', () => { - let wrapper; - beforeEach(() => { - wrapper = shallow(); - }); - - xit('should render LoadingPlugin component', () => { - expect(wrapper.find('LoadingPlugin')).toHaveLength(1); - }); -}); - -/* ============================== */ -/* ====== WITH PLUGIN DATA ====== */ -/* ============================== */ - -const sampleData = { - pluginData: { - modification_date: '3/14/15', - version: '0.1', - authors: 'testAuthor (user@domain.com)', - }, -}; - -describe('Plugin with data', () => { - let wrapper; - beforeEach(() => { - wrapper = shallow(); - wrapper.setState(sampleData); - }); - - xit('should render a plugin-name Link', () => { - expect(wrapper.find('Link.plugin-name')).toHaveLength(1); - }); - - xit('should render plugin name', () => { - wrapper.setProps({ - match: { - params: { - plugin: 'testName1', - }, - }, - }); - - expect(wrapper - .find('.plugin-name') - .childAt(0) - .text()) - .toEqual('testName1'); - }); - - xit('plugin-name Link should render correct "to" and "href" props', () => { - wrapper.setProps({ - match: { - params: { - plugin: 'testName', - }, - }, - }); - - const link = wrapper.find('Link.plugin-name'); - const whatItShould = '/plugin/testName'; - expect(link.prop('to')).toEqual(whatItShould); - expect(link.prop('href')).toEqual(whatItShould); - }); - - xit('should render plugin-modified div', () => { - expect(wrapper.find('div.plugin-modified')).toHaveLength(1); - }); - - xit('should render plugin-modified div if valid date is provided', () => { - Date.now = jest.fn(() => 1530814238992); - const changedData = { ...sampleData }; - changedData.pluginData.modification_date = '2018-06-19T15:29:11.349272Z'; - wrapper.setState(changedData); - - expect(wrapper - .find('div.plugin-modified') - .text()) - .toEqual('Last modified 16 days ago'); - }); - - xit('should not render plugin-modified if invalid date is provided', () => { - const changedData = { ...sampleData }; - changedData.pluginData.modification_date = 'invalid date'; - - wrapper.setState(changedData); - - expect(wrapper.find('div.plugin-modified')).toHaveLength(0); - }); - - xit('should render plugin-stats div', () => { - expect(wrapper.find('div.plugin-stats')).toHaveLength(1); - }); - - xit('should render plugin-version div inside plugin-stats', () => { - expect(wrapper - .find('div.plugin-stats') - .find('div.plugin-version')) - .toHaveLength(1); - }); - - xit('should render correct plugin version inside plugin-version div', () => { - const changedData = { ...sampleData }; - changedData.pluginData.version = 'testVersion'; - wrapper.setState(changedData); - - expect(wrapper - .find('div.plugin-version') - .text()) - .toEqual('testVersion'); - }); - - xit('should render plugin-created div inside plugin-stats', () => { - expect(wrapper - .find('div.plugin-stats') - .find('div.plugin-created')) - .toHaveLength(1); - }); - - xit('should render Link inside plugin-created', () => { - expect(wrapper - .find('div.plugin-created') - .find('Link.plugin-author')) - .toHaveLength(1); - }); - - xit('plugin-author Link should render correct "to" and "href" props', () => { - const changedData = { ...sampleData }; - changedData.pluginData.authors = 'testAuthor (user@domain.com)'; - wrapper.setState(changedData); - - const link = wrapper.find('Link.plugin-author'); - const whatItShould = '/author/testAuthor'; - expect(link.prop('to')).toEqual(whatItShould); - expect(link.prop('href')).toEqual(whatItShould); - }); - - xit('plugin-author Link should render correct text', () => { - const changedData = { ...sampleData }; - changedData.pluginData.authors = 'testAuthor (user@domain.com)'; - wrapper.setState(changedData); - - expect(wrapper - .find('Link.plugin-author') - .childAt(0) - .text()) - .toEqual('testAuthor'); - }); - - xit('plugin-author Link should render "to" and "href" props based on pluginData.authorURL prop', () => { - const pluginData = { - authorURL: '/test/url', - }; - wrapper.setProps({ pluginData }); - - const link = wrapper.find('Link.plugin-author'); - expect(link.prop('to')).toEqual('/test/url'); - expect(link.prop('href')).toEqual('/test/url'); - }); - - const getPluginCreatedText = () => { - const pluginCreationDiv = wrapper.find('div.plugin-created'); - const receivedText = pluginCreationDiv.text(); - // remove html elements - return receivedText.replace(/(<.*>)/g, ''); - }; - - xit('should render the value of creationDate if it is a valid date', () => { - Date.now = jest.fn(() => 1530814238992); - const changedData = { ...sampleData }; - changedData.pluginData.creation_date = '2018-06-19T15:29:11.349272Z'; - wrapper.setState(changedData); - expect(getPluginCreatedText()).toEqual(' created 16 days ago'); - }); - - xit('should not render the value of creationDate if it is not a valid date', () => { - const changedData = { ...sampleData }; - changedData.pluginData.creation_date = 'invalid date'; - wrapper.setState(changedData); - expect(getPluginCreatedText()).toEqual(''); - }); - - xit('should render PluginBody component inside plugin-container', () => { - expect(wrapper - .find('div.plugin-container') - .find('PluginBody')) - .toHaveLength(1); - }); -}); - -/* ============================== */ -/* ===== FAVORITE/UNFAVORITE ==== */ -/* ============================== */ - -describe('Plugin star', () => { - let wrapper; - const store = new Map(); - beforeEach(() => { - wrapper = shallow(); - }); - - /* ============================== */ - /* ===== FETCH STAR DATA FN ===== */ - /* ============================== */ - - it('should be able to fetch star data if user is logged in', async () => { - // define mock for @fnndsc/chrisstoreapi module - jest.mock('@fnndsc/chrisstoreapi', () => require.requireActual('../__mocks__/chrisstoreapi').default); - - store.set('authToken', 'some-token'); - - await wrapper.instance().fetchStarDataByPluginName('my-plugin'); - - expect(wrapper.state().star).not.toBeUndefined(); - }); - - it('should render star with class plugin-star-favorite when plugin is favorite', async () => { - store.set('isLoggedIn', true); - - wrapper.setState({ star: { id: 3 } }); - - expect(wrapper.find('Icon.plugin-star-favorite')).toHaveLength(1); - }); - - it('should render star with class plugin-star-disabled when user is not logged in', async () => { - store.set('isLoggedIn', false); - - wrapper.setState({ star: { id: 3 } }); - - expect(wrapper.find('Icon.plugin-star-disabled')).toHaveLength(1); - }); - - it('should render star with class plugin-star when plugin is not favorite', async () => { - store.set('isLoggedIn', true); - - wrapper.setState({ star: undefined }); - - expect(wrapper.find('Icon.plugin-star')).toHaveLength(1); - }); - - it('should mark plugin as favorited when a plugin star is clicked', () => { - // define mock for @fnndsc/chrisstoreapi module - jest.mock('@fnndsc/chrisstoreapi', () => require.requireActual('../__mocks__/chrisstoreapi').default); - - store.set('isLoggedIn', true); - - wrapper.setState({ star: undefined }); - - wrapper.find('Icon.plugin-star').simulate('click'); - - expect(wrapper.state().star).not.toBeUndefined(); - }); - - it('should mark plugin as NOT favorited when plugin star is clicked and plugin is already a favorite', () => { - // define mock for @fnndsc/chrisstoreapi module - jest.mock('@fnndsc/chrisstoreapi', () => require.requireActual('../__mocks__/chrisstoreapi').default); - - store.set('isLoggedIn', true); - - wrapper.setState({ star: { id: 3 } }); - - wrapper.find('Icon.plugin-star-favorite').simulate('click'); - - expect(wrapper.state().star).toBeUndefined(); - }); - - it('should NOT mark plugin as favorited when a plugin star is clicked and user NOT logged in', () => { - // define mock for @fnndsc/chrisstoreapi module - jest.mock('@fnndsc/chrisstoreapi', () => require.requireActual('../__mocks__/chrisstoreapi').default); - - store.set('isLoggedIn', false); - - wrapper.setState({ star: undefined }); - - wrapper.find('Icon.plugin-star-disabled').simulate('click'); - - expect(wrapper.state().star).toBeUndefined(); - }); -}); diff --git a/src/components/Plugin/PluginMeta.jsx b/src/components/Plugin/PluginMeta.jsx new file mode 100644 index 00000000..51555c19 --- /dev/null +++ b/src/components/Plugin/PluginMeta.jsx @@ -0,0 +1,272 @@ +import React, { Component } from 'react'; +import { Badge, Grid, GridItem, Split, SplitItem, Button } from '@patternfly/react-core'; +import { StarIcon } from '@patternfly/react-icons'; +import PropTypes from 'prop-types'; +import Client from '@fnndsc/chrisstoreapi'; + +import LoadingPlugin from './components/LoadingPlugin/LoadingPlugin'; +import PluginBody from './components/PluginBody/PluginBody'; +import RelativeDate from '../RelativeDate/RelativeDate'; +import ChrisStore from '../../store/ChrisStore'; +import PluginImg from '../../assets/img/brainy-pointer.png'; +import NotFound from '../NotFound/NotFound'; +import ErrorNotification from '../Notification'; +import HttpApiCallError from '../../errors/HttpApiCallError'; + +import './Plugin.css'; + +export class PluginMeta extends Component { + constructor(props) { + super(props); + + this.mounted = false; + + this.state = { + pluginData: undefined, + star: undefined, + loading: true, + errors: [], + }; + + const storeURL = process.env.REACT_APP_STORE_URL; + const auth = { token: props.store.get('authToken') }; + this.client = new Client(storeURL, auth); + } + + async componentDidMount() { + const pluginMeta = await this.fetchPluginMeta(); + this.fetchPluginVersions(pluginMeta.name) + + this.setState({ pluginData: pluginMeta, loading: false }); + if (this.isLoggedIn()) { + this.fetchIsPluginStarred(pluginMeta); + } + } + + showNotifications = (error) => { + let { errors } = this.state; + errors = [ ...errors, error.message ] + this.setState({ + errors + }) + } + + // eslint-disable-next-line react/destructuring-assignment + isFavorite = () => this.state.star !== undefined; + + // eslint-disable-next-line react/destructuring-assignment + isLoggedIn = () => this.props.store ? this.props.store.get('isLoggedIn') : false; + + onStarClicked = () => { + if (this.isLoggedIn()) { + if (this.isFavorite()) + this.unfavPlugin(); + else + this.favPlugin(); + } + else + this.showNotifications(new Error('You need to be logged in!')) + } + + favPlugin = async () => { + const { pluginData } = this.state; + + // Early state change for instant visual feedback + pluginData.stars += 1; + this.setState({ star: {}, pluginData }); + + try { + const star = await this.client.createPluginStar({ plugin_name: pluginData.name }); + this.setState({ star: star.data }); + } catch (error) { + this.showNotifications(new HttpApiCallError(error)); + pluginData.stars -= 1; + this.setState({ star: undefined, pluginData }); + } + } + + unfavPlugin = async () => { + const { pluginData, star: previousStarState } = this.state; + + // Early state change for instant visual feedback + pluginData.stars -= 1; + this.setState({ star: undefined, pluginData }); + + try { + await ( + await this.client.getPluginStar(previousStarState.id) + ).delete(); + } catch (error) { + pluginData.stars += 1; + this.setState({ star: previousStarState, pluginData }); + this.showNotifications(new HttpApiCallError(error)); + } + } + + async fetchPluginMeta() { + // eslint-disable-next-line react/destructuring-assignment + const { pluginName } = this.props.match.params; + + try { + const plugin = await this.client.getPluginMetas({ name_exact: pluginName }); + return plugin.data[0]; + } catch (e) { + this.showNotifications(new HttpApiCallError(e)); + return e + } + } + + /** + * Fetch all versions of a plugin by name. + * + * @param {string} name Plugin name + * @returns Promise => void + */ + async fetchPluginVersions(name) { + try { + const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); + const firstplg = await this.client.getPlugin(parseInt(versions.data[0].id, 10)); + return this.setState((prevState) => ({ + pluginData: { + ...prevState.pluginData, + versions: [ + { ...versions.data[0], url: firstplg.url }, + ...versions.data.slice(1) + ], + } + })); + } catch (e) { + this.showNotifications(new HttpApiCallError(e)); + return e + } + } + + async fetchIsPluginStarred({ name }) { + try { + const response = await this.client.getPluginStars({ plugin_name: name }); + if (response.data.length > 0) + this.setState({ star: response.data[0] }); + } catch(error) { + this.showNotifications(new HttpApiCallError(error)); + } + } + + render() { + const { loading, pluginData: plugin, errors } = this.state; + + if (!loading && !plugin) + return + + let container; + if (plugin) { + container = ( +
+
+ + + Plugin icon + + + + + +

{plugin.name} {plugin.category}

+

{plugin.title}

+
+ + + + + + { + !this.isFavorite() ? + + : + + } + + + + + +

{plugin.description}

+

+ { + RelativeDate.isValid(plugin.modification_date) ? + `Updated ${new RelativeDate(plugin.modification_date).format()}` + : + `Created ${new RelativeDate(plugin.creation_date).format()}` + } +

+
+
+
+
+
+ +
+ +
+
+ ); + } else { + container = ( +
+ +
+ ); + } + + return ( + <> + { + errors.map((message, index) => ( + { + errors.splice(index) + this.setState({ errors }) + }} + /> + )) + } +
+ {container} +
+ + + ); + } +} + +PluginMeta.propTypes = { + store: PropTypes.objectOf(PropTypes.object), + match: PropTypes.shape({ + params: PropTypes.shape({ + plugin: PropTypes.string, + }) + }) +}; + +PluginMeta.defaultProps = { + store: new Map(), + match: { + params: { + plugin: undefined, + } + } +}; + +export default ChrisStore.withStore(PluginMeta); diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index 66f3df4e..3455e8b7 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -110,6 +110,8 @@ const PluginBody = ({ pluginData }) => { return } + const authors = removeEmail(pluginData.authors.split(',')) + return ( <> { errors.map((message, index) => ( @@ -145,22 +147,12 @@ const PluginBody = ({ pluginData }) => { Versions}> - { - pluginData.versions ? ( - <> -

Versions of this plugin

- { pluginData.versions.map((version) => ( - - Version { version.version } - - ))} - - ) : ( -
-

This is the only version of this plugin.

-
- ) - } +

Versions of this plugin

+ { pluginData.versions.map((version) => ( + + Version { version.version } + + ))}
@@ -205,19 +197,11 @@ const PluginBody = ({ pluginData }) => {

Contributors

- { - Array.isArray(pluginData.authors) ? - pluginData.authors.map((author) => ( - -

{author}

-
- )) - : - -

{removeEmail(pluginData.authors)}

-
- } - + { authors.map((author) => ( + +

{author}

+
+ ))}
View all contributors diff --git a/src/components/Plugins/Plugins.jsx b/src/components/Plugins/Plugins.jsx index 9bb3cc16..5cfa3745 100644 --- a/src/components/Plugins/Plugins.jsx +++ b/src/components/Plugins/Plugins.jsx @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import { Switch, Route } from 'react-router-dom'; import PropTypes from 'prop-types'; import Client, { PluginMetaList } from '@fnndsc/chrisstoreapi'; import { @@ -14,13 +13,11 @@ import { } from "@patternfly/react-core"; import { CaretDownIcon } from '@patternfly/react-icons'; -import ConnectedPlugin from '../Plugin/Plugin'; import PluginItem from './components/PluginItem/PluginItem'; import LoadingPluginItem from './components/LoadingPluginItem/LoadingPluginItem'; import PluginsCategories from './components/PluginsCategories/PluginsCategories'; import LoadingContainer from '../LoadingContainer/LoadingContainer'; import LoadingContent from '../LoadingContainer/components/LoadingContent/LoadingContent'; -import NotFound from '../NotFound/NotFound'; import ChrisStore from '../../store/ChrisStore'; import HttpApiCallError from '../../errors/HttpApiCallError'; @@ -73,8 +70,6 @@ export class Plugins extends Component { /** * Re-fetch the list of plugins if the input was changed * in the NarBar's search bar. - * @param {*} pluginId - * @param {*} star */ async componentDidUpdate(prevProps) { // eslint-disable-next-line react/destructuring-assignment @@ -87,7 +82,7 @@ export class Plugins extends Component { * Add a star next to the plugin visually. * (Does not necessarily send to the backend that the plugin is a favorite). * - * @param pluginId + * @param {number} pluginId * @param star */ setPluginStar(pluginId, star) { @@ -130,13 +125,13 @@ export class Plugins extends Component { } /** - * 1. Fetch the list of plugins based on the search query. - * 2. Call setState - * 3. If user is logged in, get information about their favorite plugins. + * 1. Fetch the list of plugins based on the url path OR search query. + * 2. If user is logged in, get information about their favorite plugins. + * 3. Call setState */ async refreshPluginList(search = {}) { const params = new URLSearchParams(window.location.search) - const name = params.get('q'); + const query = params.get('q'); const { match } = this.props; const searchParams = { @@ -145,10 +140,21 @@ export class Plugins extends Component { ...search }; - if (name) - searchParams.name_title_category = name; + /** + * When the user opens this route from `/plugins`, the pluginList Map + * has the item and we return the ConnectedPlugin in `render()` below. + * + * When the user opens this route directly, the pluginList Map + * does not have the item and we we fetch by `name_exact=name`. + */ if (match.params.name) searchParams.name_exact = match.params.name; + /** + * When URL contains query-param "q=", `query` is not undefined + * and we search by `query`. + */ + else if (query) + searchParams.name_title_category = query; let plugins; try { @@ -362,134 +368,109 @@ export class Plugins extends Component { ) return ( - - { - window.scrollTo(0,0); - const { name } = routeProps.match.params - - if (loading) return - - /** - * When the user opens this route from `/plugins`, the pluginList Map - * has the item and we return the ConnectedPlugin. - * - * When the user opens this route directly, the pluginList Map - * does not have the item and we we fetch by `name_exact=name`. - */ - - if (pluginList.has(name)) { - const plugin = pluginList.get(name); - return - } - return - }} /> - - - { error && ( - this.setState({ error: null })} - /> - )} - -
-
- - -
-

ChRIS Plugins

-

- Plugins available on this ChRIS Store are listed here.
- Install these to your ChRIS instance to use them. -

-
-
- - - - - - - { - loading ? ( - - - - ) : ( - -

- {plugins.totalCount} plugins found -

- Showing {paginationOffset + 1} to {' '} - { - // eslint-disable-next-line no-nested-ternary - (paginationOffset + paginationLimit > plugins.totalCount) ? - plugins.totalCount - : - (paginationOffset > 0) ? - paginationOffset - : - paginationLimit - } -
- ) - } -
- - - - Sort by - - } - dropdownItems={[ - Name - ]} - /> - - - - -
-
- - - - - - - - - -
+
+ { error && ( + this.setState({ error: null })} + /> + )} + +
+ + +
+

ChRIS Plugins

+

+ Plugins available on this ChRIS Store are listed here.
+ Install these to your ChRIS instance to use them. +

+
+
+ + + + + + + { + loading ? ( + + + + ) : ( + +

+ {plugins.totalCount} plugins found +

+ Showing {paginationOffset + 1} to {' '} + { + // eslint-disable-next-line no-nested-ternary + (paginationOffset + paginationLimit > plugins.totalCount) ? + plugins.totalCount + : + (paginationOffset > 0) ? + paginationOffset + : + paginationLimit + } +
+ ) + } +
+ + + + Sort by + + } + dropdownItems={[ + Name + ]} + /> + + + + +
- - - + + + + + + + +
-
-
-
-
+ + + + + + +
+
); } } diff --git a/src/components/Router/Router.jsx b/src/components/Router/Router.jsx index e364946b..41321948 100644 --- a/src/components/Router/Router.jsx +++ b/src/components/Router/Router.jsx @@ -5,6 +5,7 @@ import AppLayout from '../AppLayout/AppLayout'; // pages import Welcome from '../Welcome/Welcome'; import PluginDetail from '../Plugin/Plugin'; +import PluginMetaDetail from '../Plugin/PluginMeta'; import PluginsList from '../Plugins/Plugins'; import Developers from '../Developers/Developers'; import CreatePlugin from '../CreatePlugin/CreatePlugin'; @@ -15,16 +16,15 @@ import ProtectedRoute from './ProtectedRoute'; const Router = () => ( - - + - - - - + + + + From 188bdfa0efbdfa7aca00e8736ed9a5b75bb1fb34 Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Fri, 30 Jul 2021 20:14:30 +0530 Subject: [PATCH 11/14] Remove url shortener links --- .../components/Instructions/Instructions.jsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/Developers/components/Instructions/Instructions.jsx b/src/components/Developers/components/Instructions/Instructions.jsx index 1e7de292..ab82d09d 100644 --- a/src/components/Developers/components/Instructions/Instructions.jsx +++ b/src/components/Developers/components/Instructions/Instructions.jsx @@ -8,7 +8,7 @@ const Instructions = () => (

Get Started - 4 Simple Steps {' '} - [source] + [source]

@@ -44,7 +44,7 @@ const Instructions = () => (
From 2c2c6c27e3a8a8eea696d281abf57d74eeaa854e Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Fri, 30 Jul 2021 21:44:05 +0530 Subject: [PATCH 12/14] Remove `this.mounted` dont abuse lifecycle methods --- src/components/Plugin/Plugin.jsx | 2 -- src/components/Plugin/PluginMeta.jsx | 2 -- src/components/SignIn/SignIn.jsx | 20 +++++++------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 5d75a0f7..ffb2fc86 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -19,8 +19,6 @@ export class Plugin extends Component { constructor(props) { super(props); - this.mounted = false; - const { pluginData, isFavorite } = props; this.state = { pluginData, diff --git a/src/components/Plugin/PluginMeta.jsx b/src/components/Plugin/PluginMeta.jsx index 51555c19..bd441061 100644 --- a/src/components/Plugin/PluginMeta.jsx +++ b/src/components/Plugin/PluginMeta.jsx @@ -19,8 +19,6 @@ export class PluginMeta extends Component { constructor(props) { super(props); - this.mounted = false; - this.state = { pluginData: undefined, star: undefined, diff --git a/src/components/SignIn/SignIn.jsx b/src/components/SignIn/SignIn.jsx index d3734d06..84e6dc82 100644 --- a/src/components/SignIn/SignIn.jsx +++ b/src/components/SignIn/SignIn.jsx @@ -21,7 +21,6 @@ export class SignIn extends Component { constructor(props) { super(props); - this.mounted = false; this.state = { username: '', password: '', @@ -40,7 +39,6 @@ export class SignIn extends Component { // already logged in, we will log them out. // TODO SECURITY idk if safe from CSRF // TODO SECURITY send goodbye to backend to invalidate authToken - this.mounted = true; const { store } = this.props; if (store.get('isLoggedIn')) { store.set('authToken')(''); @@ -60,19 +58,15 @@ export class SignIn extends Component { const token = await StoreClient.getAuthToken(authURL, username, password); store.set('userName')(username); store.set('authToken')(token); - if (this.mounted) { - this.setState({ loading: false }); - if (location.state && location.state.from) { - history.replace(location.state.from); - } else { - history.push('/dashboard'); - } - } + this.setState({ loading: false }); + + if (location.state && location.state.from) + history.replace(location.state.from); + else + history.push('/dashboard'); } catch (error) { this.showError('Invalid username or password'); - if (this.mounted) { - this.setState({ loading: false }); - } + this.setState({ loading: false }); } event.persist(); } From 7dd9e68f316cfb7783594d1a6c951cf65d165ff1 Mon Sep 17 00:00:00 2001 From: Alisamar Husain Date: Fri, 30 Jul 2021 23:12:32 +0530 Subject: [PATCH 13/14] Incorporate feedback --- src/components/Notification/index.jsx | 2 +- src/components/Plugin/Plugin.jsx | 89 +++++++++++------------- src/components/Plugin/PluginMeta.jsx | 99 ++++++++++++--------------- 3 files changed, 87 insertions(+), 103 deletions(-) diff --git a/src/components/Notification/index.jsx b/src/components/Notification/index.jsx index e8f555a6..86243c52 100644 --- a/src/components/Notification/index.jsx +++ b/src/components/Notification/index.jsx @@ -19,7 +19,7 @@ export const Notification = ({ position, variant, message, onClose, closeable, t Notification.propTypes = { position: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, + message: PropTypes.string, variant: PropTypes.string, closeable: PropTypes.bool, onClose: PropTypes.func, diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index ffb2fc86..fe4b0a90 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { Badge, Grid, GridItem, Split, SplitItem, Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; -import Client from '@fnndsc/chrisstoreapi'; +import Client, { Plugin } from '@fnndsc/chrisstoreapi'; import LoadingPlugin from './components/LoadingPlugin/LoadingPlugin'; import PluginBody from './components/PluginBody/PluginBody'; @@ -15,15 +15,14 @@ import HttpApiCallError from '../../errors/HttpApiCallError'; import './Plugin.css'; -export class Plugin extends Component { +export class PluginView extends Component { constructor(props) { super(props); - const { pluginData, isFavorite } = props; this.state = { - pluginData, + pluginData: undefined, + star: undefined, loading: true, - star: isFavorite || undefined, errors: [], }; @@ -33,21 +32,35 @@ export class Plugin extends Component { } async componentDidMount() { - const pluginData = await this.fetchPlugin(); - this.fetchPluginVersions(pluginData.name) - - this.setState({ pluginData, loading: false }); - if (this.isLoggedIn()) { - this.fetchIsPluginStarred(pluginData); + try { + const plugin = await this.fetchPlugin(); + const versions = await this.fetchPluginVersions(plugin.data.name); + + let star; + if (this.isLoggedIn()) + star = await this.fetchIsPluginStarred(plugin.data); + + this.setState({ + loading: false, + pluginData: { + ...plugin.data, + url: plugin.url, + versions + }, + star, + }); + } catch (error) { + this.setState((prev) => ({ + loading: false, + errors: [ ...prev.errors, error ] + })); } } showNotifications = (error) => { - let { errors } = this.state; - errors = [ ...errors, error.message ] - this.setState({ - errors - }) + this.setState((prev) => ({ + errors: [ ...prev.errors, error ] + })); } // eslint-disable-next-line react/destructuring-assignment @@ -119,14 +132,7 @@ export class Plugin extends Component { async fetchPlugin() { // eslint-disable-next-line react/destructuring-assignment const { pluginId } = this.props.match.params; - - try { - const plugin = await this.client.getPlugin(parseInt(pluginId, 10)); - return { ...plugin.data, url: plugin.url }; - } catch (e) { - this.showNotifications(new HttpApiCallError(e)); - return e - } + return this.client.getPlugin(parseInt(pluginId, 10)); } /** @@ -136,32 +142,19 @@ export class Plugin extends Component { * @returns Promise => void */ async fetchPluginVersions(name) { - try { - const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); - const firstplg = await this.client.getPlugin(parseInt(versions.data[0].id, 10)); - return this.setState((prevState) => ({ - pluginData: { - ...prevState.pluginData, - versions: [ - { ...versions.data[0], url: firstplg.url }, - ...versions.data.slice(1) - ], - } - })); - } catch (e) { - this.showNotifications(new HttpApiCallError(e)); - return e - } + const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); + const firstplg = await this.client.getPlugin(parseInt(versions.data[0].id, 10)); + return [ + { ...versions.data[0], url: firstplg.url }, + ...versions.data.slice(1) + ] } async fetchIsPluginStarred({ name }) { - try { - const response = await this.client.getPluginStars({ plugin_name: name }); - if (response.data.length > 0) - this.setState({ star: response.data[0] }); - } catch(error) { - this.showNotifications(new HttpApiCallError(error)); - } + const response = await this.client.getPluginStars({ plugin_name: name }); + if (response.data.length > 0) + return response.data[0]; + return undefined; } render() { @@ -282,4 +275,4 @@ Plugin.defaultProps = { } }; -export default ChrisStore.withStore(Plugin); +export default ChrisStore.withStore(PluginView); diff --git a/src/components/Plugin/PluginMeta.jsx b/src/components/Plugin/PluginMeta.jsx index bd441061..391acdb3 100644 --- a/src/components/Plugin/PluginMeta.jsx +++ b/src/components/Plugin/PluginMeta.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { Badge, Grid, GridItem, Split, SplitItem, Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; -import Client from '@fnndsc/chrisstoreapi'; +import Client, { PluginMeta } from '@fnndsc/chrisstoreapi'; import LoadingPlugin from './components/LoadingPlugin/LoadingPlugin'; import PluginBody from './components/PluginBody/PluginBody'; @@ -15,7 +15,7 @@ import HttpApiCallError from '../../errors/HttpApiCallError'; import './Plugin.css'; -export class PluginMeta extends Component { +export class PluginMetaView extends Component { constructor(props) { super(props); @@ -32,21 +32,34 @@ export class PluginMeta extends Component { } async componentDidMount() { - const pluginMeta = await this.fetchPluginMeta(); - this.fetchPluginVersions(pluginMeta.name) - - this.setState({ pluginData: pluginMeta, loading: false }); - if (this.isLoggedIn()) { - this.fetchIsPluginStarred(pluginMeta); + try { + const pluginMeta = await this.fetchPluginMeta(); + const versions = await this.fetchPluginVersions(pluginMeta); + + let star; + if (this.isLoggedIn()) + star = await this.fetchIsPluginStarred(pluginMeta.data); + + this.setState({ + loading: false, + star, + pluginData: { + ...pluginMeta.data, + versions + } + }); + } catch (error) { + this.setState((prev) => ({ + loading: false, + errors: [ ...prev.errors, error ] + })); } } - + showNotifications = (error) => { - let { errors } = this.state; - errors = [ ...errors, error.message ] - this.setState({ - errors - }) + this.setState((prev) => ({ + errors: [ ...prev.errors, error ] + })); } // eslint-disable-next-line react/destructuring-assignment @@ -104,49 +117,26 @@ export class PluginMeta extends Component { async fetchPluginMeta() { // eslint-disable-next-line react/destructuring-assignment const { pluginName } = this.props.match.params; - - try { - const plugin = await this.client.getPluginMetas({ name_exact: pluginName }); - return plugin.data[0]; - } catch (e) { - this.showNotifications(new HttpApiCallError(e)); - return e - } + const metas = await this.client.getPluginMetas({ name_exact: pluginName, limit: 1 }); + return metas.getItems().shift(); } /** - * Fetch all versions of a plugin by name. - * - * @param {string} name Plugin name - * @returns Promise => void + * Fetch all versions of a plugin. + * @param {PluginMeta} pluginMeta + * @returns {Promise} Versions of this plugin. */ - async fetchPluginVersions(name) { - try { - const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); - const firstplg = await this.client.getPlugin(parseInt(versions.data[0].id, 10)); - return this.setState((prevState) => ({ - pluginData: { - ...prevState.pluginData, - versions: [ - { ...versions.data[0], url: firstplg.url }, - ...versions.data.slice(1) - ], - } - })); - } catch (e) { - this.showNotifications(new HttpApiCallError(e)); - return e - } + // eslint-disable-next-line class-methods-use-this + async fetchPluginVersions(pluginMeta) { + const versions = (await pluginMeta.getPlugins()).getItems(); + return versions.map(({ data, url }) => ({ ...data, url })); } async fetchIsPluginStarred({ name }) { - try { - const response = await this.client.getPluginStars({ plugin_name: name }); - if (response.data.length > 0) - this.setState({ star: response.data[0] }); - } catch(error) { - this.showNotifications(new HttpApiCallError(error)); - } + const response = await this.client.getPluginStars({ plugin_name: name }); + if (response.data.length > 0) + return response.data[0]; + return undefined; } render() { @@ -226,10 +216,11 @@ export class PluginMeta extends Component { return ( <> { - errors.map((message, index) => ( + errors.map((error, index) => ( Date: Fri, 6 Aug 2021 14:56:29 +0530 Subject: [PATCH 14/14] Added documentation comments --- src/components/Plugin/Plugin.jsx | 23 +++++++++++++++++----- src/components/Plugin/PluginMeta.jsx | 29 ++++++++++++++++++++++------ src/components/Plugins/Plugins.jsx | 15 +++++++------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index fe4b0a90..ee692a93 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -15,6 +15,9 @@ import HttpApiCallError from '../../errors/HttpApiCallError'; import './Plugin.css'; +/** + * View a plugin by plugin ID. + */ export class PluginView extends Component { constructor(props) { super(props); @@ -31,9 +34,16 @@ export class PluginView extends Component { this.client = new Client(storeURL, auth); } + /** + * Fetch a plugin by ID, from URL params. + * Then fetch other plugins which have the same name as versions. + * Set stars if user is logged in. + */ async componentDidMount() { + // eslint-disable-next-line react/destructuring-assignment + const { pluginId } = this.props.match.params; try { - const plugin = await this.fetchPlugin(); + const plugin = await this.fetchPlugin(pluginId); const versions = await this.fetchPluginVersions(plugin.data.name); let star; @@ -77,7 +87,7 @@ export class PluginView extends Component { this.favPlugin(); } else - this.showNotifications(new Error('You need to be logged in!')) + this.showNotifications(new Error('Login required to favorite this plugin.')) } favPlugin = async () => { @@ -129,15 +139,18 @@ export class PluginView extends Component { return ; } - async fetchPlugin() { + /** + * Fetch a plugin by ID + * @param {string} pluginId + * @returns {Promise} Plugin + */ + async fetchPlugin(pluginId) { // eslint-disable-next-line react/destructuring-assignment - const { pluginId } = this.props.match.params; return this.client.getPlugin(parseInt(pluginId, 10)); } /** * Fetch all versions of a plugin by name. - * * @param {string} name Plugin name * @returns Promise => void */ diff --git a/src/components/Plugin/PluginMeta.jsx b/src/components/Plugin/PluginMeta.jsx index 391acdb3..ef3248f0 100644 --- a/src/components/Plugin/PluginMeta.jsx +++ b/src/components/Plugin/PluginMeta.jsx @@ -15,6 +15,13 @@ import HttpApiCallError from '../../errors/HttpApiCallError'; import './Plugin.css'; +/** + * View a plugin meta by plugin name. + * + * @todo + * Make this view visually different + * from the plugin view by ID. + */ export class PluginMetaView extends Component { constructor(props) { super(props); @@ -31,9 +38,16 @@ export class PluginMetaView extends Component { this.client = new Client(storeURL, auth); } + /** + * Fetch a plugin meta by name, from URL params. + * Then fetch all versions of that plugin. + * Set stars if user is logged in. + */ async componentDidMount() { + // eslint-disable-next-line react/destructuring-assignment + const { pluginName } = this.props.match.params; try { - const pluginMeta = await this.fetchPluginMeta(); + const pluginMeta = await this.fetchPluginMeta(pluginName); const versions = await this.fetchPluginVersions(pluginMeta); let star; @@ -76,7 +90,7 @@ export class PluginMetaView extends Component { this.favPlugin(); } else - this.showNotifications(new Error('You need to be logged in!')) + this.showNotifications(new Error('Login required to favorite this plugin.')) } favPlugin = async () => { @@ -114,9 +128,12 @@ export class PluginMetaView extends Component { } } - async fetchPluginMeta() { - // eslint-disable-next-line react/destructuring-assignment - const { pluginName } = this.props.match.params; + /** + * Fetch a plugin meta by plugin name. + * @param {string} pluginName + * @returns {Promise} PluginMeta + */ + async fetchPluginMeta(pluginName) { const metas = await this.client.getPluginMetas({ name_exact: pluginName, limit: 1 }); return metas.getItems().shift(); } @@ -124,7 +141,7 @@ export class PluginMetaView extends Component { /** * Fetch all versions of a plugin. * @param {PluginMeta} pluginMeta - * @returns {Promise} Versions of this plugin. + * @returns {Promise} Versions of the plugin */ // eslint-disable-next-line class-methods-use-this async fetchPluginVersions(pluginMeta) { diff --git a/src/components/Plugins/Plugins.jsx b/src/components/Plugins/Plugins.jsx index 5cfa3745..d6d071b6 100644 --- a/src/components/Plugins/Plugins.jsx +++ b/src/components/Plugins/Plugins.jsx @@ -29,11 +29,9 @@ import './Plugins.css'; const CATEGORIES = ['FreeSurfer', 'MRI', 'Segmentation']; /** - * A page showing a list of ChRIS plugins, according to the search - * specified in the URI's query string. - * - * When the user opens /plugins, all plugin metas are fetched, with pagination. - * so that we already have the data to immediately populate each plugin body. + * A page showing a list of ChRIS plugins. + * If search is specified in the URI's query string, plugins which + * match the query in the name, title or category are fetched. */ export class Plugins extends Component { constructor(props) { @@ -60,7 +58,10 @@ export class Plugins extends Component { } /** - * Search for plugin data from the backend. + * Fetch list of plugin metas, if search is specified then + * use query to filter results. + * Fetch all plugins to build a list of categories. This can be + * disabled to just have a list of pre-set hardcoded categories. */ async componentDidMount() { await this.refreshPluginList(); @@ -300,7 +301,7 @@ export class Plugins extends Component { } } else { - this.showNotifications(new Error('You need to be logged in!')); + this.showNotifications(new Error('Login required to favorite this plugin.')); } }