diff --git a/src/components/Developers/components/DeveloperSignup/DeveloperSignup.jsx b/src/components/Developers/components/DeveloperSignup/DeveloperSignup.jsx index 1c4dba96..c4d13bcc 100644 --- a/src/components/Developers/components/DeveloperSignup/DeveloperSignup.jsx +++ b/src/components/Developers/components/DeveloperSignup/DeveloperSignup.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import { Redirect } from 'react-router-dom'; import PropTypes from 'prop-types'; import _ from 'lodash'; @@ -12,14 +12,11 @@ import ChrisStore from '../../../../store/ChrisStore'; import FormInput from '../../../FormInput'; import isTouchDevice from './isTouchDevice'; -export class DeveloperSignup extends Component { - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleChange = this.handleChange.bind(this); +const DeveloperSignup = (props) => { const queryParams = new URLSearchParams(window.location.search); const emailVal = queryParams.get('email'); - this.state = { + + const [state, setState] = useState({ loading: false, error: { message: String(), @@ -29,20 +26,72 @@ export class DeveloperSignup extends Component { email: emailVal, password: String(), passwordConfirm: String(), - }; + }); + + const { username, email, password, passwordConfirm, error, loading, toDashboard, } = state; + const { store } = props; + +const handleChange = (value, name) => { + setState({ [name]: value }); } - handleChange(value, name) { - this.setState({ [name]: value }); + const handleStoreLogin = async () => { + const storeURL = process.env.REACT_APP_STORE_URL; + const usersURL = `${storeURL}users/`; + const authURL = `${storeURL}auth-token/`; + let authToken; + + try { + await StoreClient.createUser(usersURL, username, password, email); + } catch (e) { + if (_.has(e, 'response')) { + if (_.has(e, 'response.data.username')) { + setState({ + loading: false, + error: { + message: 'This username is already registered.', + controls: ['username'], + }, + }); + } else { + setState({ + loading: false, + error: { + message: 'This email is already registered.', + controls: ['email'], + }, + }); + } + } else { + setState({ + loading: false, + }); + } + return e; + } + + try { + authToken = await StoreClient.getAuthToken(authURL, username, password); + } catch (e) { + return Promise.reject(e); + } + + return setState( + { + toDashboard: true, + }, + () => { + store.set('authToken')(authToken); + return authToken; + } + ); } - handleSubmit(event) { +const handleSubmit = (event) => { event.persist(); - const { username, email, password, passwordConfirm } = this.state; - const { store } = this.props; if (!username) { - return this.setState({ + return setState({ error: { message: 'Username is required', controls: ['username'], @@ -51,7 +100,7 @@ export class DeveloperSignup extends Component { } if (!email || !validate(email)) { - return this.setState({ + return setState({ error: { message: 'A valid Email is required', controls: ['email'], @@ -60,7 +109,7 @@ export class DeveloperSignup extends Component { } if (!password) { - return this.setState({ + return setState({ error: { message: 'Password is required', controls: ['password'], @@ -69,7 +118,7 @@ export class DeveloperSignup extends Component { } if (!passwordConfirm) { - return this.setState({ + return setState({ error: { message: 'Confirmation is required', controls: ['confirmation'], @@ -78,7 +127,7 @@ export class DeveloperSignup extends Component { } if (password !== passwordConfirm) { - return this.setState({ + return setState({ error: { message: 'Password and confirmation do not match', controls: ['password', 'confirmation'], @@ -86,7 +135,7 @@ export class DeveloperSignup extends Component { }); } - this.setState( + setState( { loading: true, error: { @@ -97,80 +146,14 @@ export class DeveloperSignup extends Component { () => store.set('userName')(username) ); - return this.handleStoreLogin(); + return handleStoreLogin(); } - async handleStoreLogin() { - const { username, email, password } = this.state; - const { store } = this.props; - const storeURL = process.env.REACT_APP_STORE_URL; - const usersURL = `${storeURL}users/`; - const authURL = `${storeURL}auth-token/`; - let authToken; - - try { - await StoreClient.createUser(usersURL, username, password, email); - } catch (e) { - if (_.has(e, 'response')) { - if (_.has(e, 'response.data.username')) { - this.setState({ - loading: false, - error: { - message: 'This username is already registered.', - controls: ['username'], - }, - }); - } else { - this.setState({ - loading: false, - error: { - message: 'This email is already registered.', - controls: ['email'], - }, - }); - } - } else { - this.setState({ - loading: false, - }); - } - return e; - } - - try { - authToken = await StoreClient.getAuthToken(authURL, username, password); - } catch (e) { - return Promise.reject(e); - } - - return this.setState( - { - toDashboard: true, - }, - () => { - store.set('authToken')(authToken); - return authToken; - } - ); - } - - render() { - const { - error, - loading, - toDashboard, - - username, - email, - password, - passwordConfirm, - } = this.state; - if (toDashboard) return ; const disableControls = loading; return ( -
+ this.handleChange(val, 'username')} + onChange={(val) => handleChange(val, 'username')} disableControls={disableControls} error={error} /> @@ -194,7 +177,7 @@ export class DeveloperSignup extends Component { id="email" fieldName="email" value={email} - onChange={(val) => this.handleChange(val, 'email')} + onChange={(val) => handleChange(val, 'email')} disableControls={disableControls} error={error} /> @@ -207,7 +190,7 @@ export class DeveloperSignup extends Component { id="password" fieldName="password" value={password} - onChange={(val) => this.handleChange(val, 'password')} + onChange={(val) => handleChange(val, 'password')} disableControls={disableControls} error={error} /> @@ -220,7 +203,7 @@ export class DeveloperSignup extends Component { id="password-confirm" fieldName="passwordConfirm" value={passwordConfirm} - onChange={(val) => this.handleChange(val, 'passwordConfirm')} + onChange={(val) => handleChange(val, 'passwordConfirm')} disableControls={disableControls} error={error} /> @@ -237,7 +220,7 @@ export class DeveloperSignup extends Component { ); } -} + export default ChrisStore.withStore(DeveloperSignup); diff --git a/src/components/Plugin/Plugin.jsx b/src/components/Plugin/Plugin.jsx index 0a4ddafe..4791298b 100644 --- a/src/components/Plugin/Plugin.jsx +++ b/src/components/Plugin/Plugin.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useEffect, useState } from 'react'; import { Badge, Grid, GridItem, Split, SplitItem, Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; @@ -18,161 +18,153 @@ import './Plugin.css'; /** * View a plugin by plugin ID. */ -export class PluginView extends Component { - constructor(props) { - super(props); +const PluginView = (props) => { +const [state, setState] = useState({ + pluginData: undefined, + star: undefined, + loading: true, + errors: [], +}) - this.state = { - pluginData: undefined, - star: undefined, - loading: true, - errors: [], - }; + const { match, store } = props + const { loading, pluginData: plugin, errors, star } = state; const storeURL = process.env.REACT_APP_STORE_URL; - const auth = { token: props.store.get('authToken') }; - 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(pluginId); - 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] - })); - } - } + const auth = { token: store.get('authToken') }; + const client = new Client(storeURL, auth); - showNotifications = (error) => { - this.setState((prev) => ({ +const showNotifications = (error) => { + setState((prev) => ({ errors: [...prev.errors, error] })); } - // eslint-disable-next-line react/destructuring-assignment - isFavorite = () => this.state.star !== undefined; +const isFavorite = () => 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('Login required to favorite this plugin.')) - } - - favPlugin = async () => { - const { pluginData } = this.state; +const isLoggedIn = () => store ? store.get('isLoggedIn') : false; +const favPlugin = async () => { + const { pluginData } = state; // Early state change for instant visual feedback pluginData.stars += 1; - this.setState({ star: {}, pluginData }); + setState({ star: {}, pluginData }); try { - const star = await this.client.createPluginStar({ plugin_name: pluginData.name }); - this.setState({ star: star.data }); + const createdStar = await client.createPluginStar({ plugin_name: pluginData.name }); + setState({ star: createdStar.data }); } catch (error) { - this.showNotifications(new HttpApiCallError(error)); + showNotifications(new HttpApiCallError(error)); pluginData.stars -= 1; - this.setState({ star: undefined, pluginData }); + setState({ star: undefined, pluginData }); } } - unfavPlugin = async () => { - const { pluginData, star: previousStarState } = this.state; +const unfavPlugin = async () => { + const { pluginData, star: previousStarState } = state; // Early state change for instant visual feedback pluginData.stars -= 1; - this.setState({ star: undefined, pluginData }); + setState({ star: undefined, pluginData }); try { await ( - await this.client.getPluginStar(previousStarState.id) + await client.getPluginStar(previousStarState.id) ).delete(); } catch (error) { pluginData.stars += 1; - this.setState({ star: previousStarState, pluginData }); - this.showNotifications(new HttpApiCallError(error)); + setState({ star: previousStarState, pluginData }); + showNotifications(new HttpApiCallError(error)); } } - renderStar = () => { - let name; - let className; - - if (this.isLoggedIn()) { - className = this.isFavorite() ? 'plugin-star-favorite' : 'plugin-star'; - name = this.isFavorite() ? 'star' : 'star-o'; - } else { - className = 'plugin-star-disabled'; - name = 'star-o'; + const onStarClicked = () => { + if (isLoggedIn()) { + if (isFavorite()) + unfavPlugin(); + else + favPlugin(); } - return ; + else + showNotifications(new Error('Login required to favorite this plugin.')) } +// const renderStar = () => { +// let name; +// let className; + +// if (isLoggedIn()) { +// className = isFavorite() ? 'plugin-star-favorite' : 'plugin-star'; +// name = isFavorite() ? 'star' : 'star-o'; +// } else { +// className = 'plugin-star-disabled'; +// name = 'star-o'; +// } +// return onStarClicked()} />; +// } + /** * Fetch a plugin by ID * @param {string} pluginId * @returns {Promise} Plugin */ - async fetchPlugin(pluginId) { - // eslint-disable-next-line react/destructuring-assignment - return this.client.getPlugin(parseInt(pluginId, 10)); - } +const fetchPlugin = async (pluginId) => client.getPlugin(parseInt(pluginId, 10)); /** * Fetch all versions of a plugin by name. * @param {string} name Plugin name * @returns Promise => void */ - async fetchPluginVersions(name) { - const versions = await this.client.getPlugins({ limit: 10e6, name_exact: name }); - const firstplg = await this.client.getPlugin(parseInt(versions.data[0].id, 10)); +const fetchPluginVersions = async (name) => { + const versions = await client.getPlugins({ limit: 10e6, name_exact: name }); + const firstplg = await client.getPlugin(parseInt(versions.data[0].id, 10)); return [ { ...versions.data[0], url: firstplg.url }, ...versions.data.slice(1) ] } - async fetchIsPluginStarred({ name }) { - const response = await this.client.getPluginStars({ plugin_name: name }); +const fetchIsPluginStarred = async ({ name }) => { + const response = await client.getPluginStars({ plugin_name: name }); if (response.data.length > 0) return response.data[0]; return undefined; } - render() { - const { loading, pluginData: plugin, errors } = this.state; - + /** + * 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. + */ +useEffect(() => { + const { pluginId } = match.params; + const fetchData = async() => { + try { + const fetchedPlugin = await fetchPlugin(pluginId); + const versions = await fetchPluginVersions(fetchedPlugin.data.name); + + // eslint-disable-next-line no-shadow + let star; + if (isLoggedIn()) + star = await fetchIsPluginStarred(plugin.data); + + setState({ + loading: false, + pluginData: { + ...plugin.data, + url: plugin.url, + versions + }, + star, + }); + } catch (error) { + setState((prev) => ({ + loading: false, + errors: [...prev.errors, error] + })); + } +} +fetchData() +}) if (!loading && !plugin) return @@ -202,12 +194,12 @@ export class PluginView extends Component { { - !this.isFavorite() ? - : - } @@ -256,7 +248,7 @@ export class PluginView extends Component { closeable onClose={() => { errors.splice(index) - this.setState({ errors }) + setState({ errors }) }} /> )) @@ -268,7 +260,7 @@ export class PluginView extends Component { ); } -} + Plugin.propTypes = { store: PropTypes.objectOf(PropTypes.object), diff --git a/src/components/Plugin/PluginMeta.jsx b/src/components/Plugin/PluginMeta.jsx index 1d88da57..df904f28 100644 --- a/src/components/Plugin/PluginMeta.jsx +++ b/src/components/Plugin/PluginMeta.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useEffect, useState } from 'react'; import { Badge, Grid, GridItem, Split, SplitItem, Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; @@ -22,120 +22,83 @@ import './Plugin.css'; * Make this view visually different * from the plugin view by ID. */ -export class PluginMetaView extends Component { - constructor(props) { - super(props); - - this.state = { +const PluginMetaView = (props) => { + const [state, setState] = useState({ 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); - } + const { match, store } = props + const { star, pluginData, loading, pluginData: plugin, errors } = state - /** - * 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(pluginName); - const versions = await this.fetchPluginVersions(pluginMeta); - const collaborators = await this.fetchPluginCollaborators(pluginMeta); - let star; - if (this.isLoggedIn()) - star = await this.fetchIsPluginStarred(pluginMeta.data); - - this.setState({ - loading: false, - star, - pluginData: { - ...pluginMeta.data, - versions, - collaborators, - } - }); - } catch (error) { - this.setState((prev) => ({ - loading: false, - errors: [...prev.errors, error] - })); - } - } + const storeURL = process.env.REACT_APP_STORE_URL; + const auth = { token: store.get('authToken') }; + const client = new Client(storeURL, auth); - showNotifications = (error) => { - this.setState((prev) => ({ +const showNotifications = (error) => { + setState((prev) => ({ errors: [...prev.errors, error] })); } - // eslint-disable-next-line react/destructuring-assignment - isFavorite = () => this.state.star !== undefined; +const isFavorite = () => star !== undefined; - // eslint-disable-next-line react/destructuring-assignment - isLoggedIn = () => this.props.store ? this.props.store.get('isLoggedIn') : false; +const isLoggedIn = () => store ? store.get('isLoggedIn') : false; - onStarClicked = () => { - if (this.isLoggedIn()) { - if (this.isFavorite()) - this.unfavPlugin(); - else - this.favPlugin(); - } - else - this.showNotifications(new Error('Login required to favorite this plugin.')) - } - - favPlugin = async () => { - const { pluginData } = this.state; +const favPlugin = async () => { // Early state change for instant visual feedback pluginData.stars += 1; - this.setState({ star: {}, pluginData }); + setState({ star: {}, pluginData }); try { - const star = await this.client.createPluginStar({ plugin_name: pluginData.name }); - this.setState({ star: star.data }); + const createStar = await client.createPluginStar({ plugin_name: pluginData.name }); + setState({ star: createStar.data }); } catch (error) { - this.showNotifications(new HttpApiCallError(error)); + showNotifications(new HttpApiCallError(error)); pluginData.stars -= 1; - this.setState({ star: undefined, pluginData }); + setState({ star: undefined, pluginData }); } } - unfavPlugin = async () => { - const { pluginData, star: previousStarState } = this.state; +const unfavPlugin = async () => { + const { star: previousStarState } = state; // Early state change for instant visual feedback pluginData.stars -= 1; - this.setState({ star: undefined, pluginData }); + setState({ star: undefined, pluginData }); try { await ( - await this.client.getPluginStar(previousStarState.id) + await client.getPluginStar(previousStarState.id) ).delete(); } catch (error) { pluginData.stars += 1; - this.setState({ star: previousStarState, pluginData }); - this.showNotifications(new HttpApiCallError(error)); + setState({ star: previousStarState, pluginData }); + showNotifications(new HttpApiCallError(error)); } } + const onStarClicked = () => { + if (isLoggedIn()) { + if (isFavorite()) + unfavPlugin(); + else + favPlugin(); + } + else + showNotifications(new Error('Login required to favorite this plugin.')) + } + /** * 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 }); +const fetchPluginMeta = async (pluginName) => { + const metas = await client.getPluginMetas({ name_exact: pluginName, limit: 1 }); return metas.getItems().shift(); } @@ -144,8 +107,7 @@ export class PluginMetaView extends Component { * @param {PluginMeta} pluginMeta * @returns {Promise} Versions of the plugin */ - // eslint-disable-next-line class-methods-use-this - async fetchPluginVersions(pluginMeta) { + const fetchPluginVersions = async (pluginMeta) => { const versions = (await pluginMeta.getPlugins()).getItems(); return versions.map(({ data, url }) => ({ ...data, url })); } @@ -155,22 +117,53 @@ export class PluginMetaView extends Component { * @param {PluginMeta} pluginMeta * @returns {Promise} Collaborators of the plugin */ - // eslint-disable-next-line class-methods-use-this - async fetchPluginCollaborators(pluginMeta) { + const fetchPluginCollaborators = async (pluginMeta) => { const collaborators = (await pluginMeta.getCollaborators()).getItems(); return collaborators.map((collaborator, index) => collaborators[index].data); } - async fetchIsPluginStarred({ name }) { - const response = await this.client.getPluginStars({ plugin_name: name }); +const fetchIsPluginStarred = async({ name }) => { + const response = await client.getPluginStars({ plugin_name: name }); if (response.data.length > 0) return response.data[0]; return undefined; } - render() { - const { loading, pluginData: plugin, errors } = this.state; + /** + * Fetch a plugin meta by name, from URL params. + * Then fetch all versions of that plugin. + * Set stars if user is logged in. + */ +useEffect(() => { + const fetchData = async () => { + const { pluginName } = match.params; + try { + const pluginMeta = await fetchPluginMeta(pluginName); + const versions = await fetchPluginVersions(pluginMeta); + const collaborators = await fetchPluginCollaborators(pluginMeta); + let fetchedStar; + if (isLoggedIn()) + fetchedStar = await fetchIsPluginStarred(pluginMeta.data); + + setState({ + loading: false, + star: fetchedStar, + pluginData: { + ...pluginMeta.data, + versions, + collaborators, + } + }); + } catch (error) { + setState((prev) => ({ + loading: false, + errors: [...prev.errors, error] + })); + } + } + fetchData() + }) if (!loading && !plugin) return @@ -201,12 +194,12 @@ export class PluginMetaView extends Component { { - !this.isFavorite() ? - : - } @@ -245,7 +238,7 @@ export class PluginMetaView extends Component { return ( <> - { + { errors && errors.map((error, index) => ( { errors.splice(index) - this.setState({ errors }) + setState({ errors }) }} /> )) @@ -267,7 +260,6 @@ export class PluginMetaView extends Component { ); - } } PluginMeta.propTypes = { diff --git a/src/components/Plugin/components/PluginBody/PluginBody.jsx b/src/components/Plugin/components/PluginBody/PluginBody.jsx index d770904d..0f6ccacc 100644 --- a/src/components/Plugin/components/PluginBody/PluginBody.jsx +++ b/src/components/Plugin/components/PluginBody/PluginBody.jsx @@ -118,6 +118,8 @@ const PluginBody = ({ pluginData }) => { <> { errors.map((message, index) => ( { + + const { store, match} = props const storeURL = process.env.REACT_APP_STORE_URL; - const auth = { token: props.store.get("authToken") }; - this.client = new Client(storeURL, auth); - - const categories = new Map(); - CATEGORIES.forEach((name) => categories.set(name, 0)); - - this.state = { - loading: true, - isSortOpen: false, - error: null, - plugins: new PluginMetaList(), - paginationLimit: 0, - paginationOffset: 0, - categories, - selectedCategory: null, - starsByPlugin: {}, - }; + const auth = { token: store.get("authToken") }; + const client = new Client(storeURL, auth); + + const [plugins, setPlugins] = useState(new PluginMetaList()) + const [categories, setCategories] = useState(new Map()) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [isSortOpen, setIsSortOpen] = useState(false) + const [paginationLimit, setPaginationLimit] = useState(0) + const [paginationOffset, setPaginationOffset] = useState(0) + const [selectedCategory, setSelectedCategory] = useState(null) + const [starsByPlugin, setStarsByPlugin] = useState({}) + + const isLoggedIn = () => store ? store.get("isLoggedIn") : false; + + const isFavorite = ({ id }) => starsByPlugin[id] !== undefined; + + /** + * Show a notification for some network error. + * @param error error message + */ + const showNotifications = (err) => { + setError(err.message) } /** - * 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. + * 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 + */ +const refreshPluginList = async (search = {}) => { + const params = new URLSearchParams(window.location.search) + const query = params.get('q'); + + const searchParams = { + limit: 20, + offset: 0, + ...search + }; + + /** + * 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`. */ - async componentDidMount() { - await this.refreshPluginList(); - await this.fetchAllCategories(); + 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 pluginsMeta; + try { + pluginsMeta = await client.getPluginMetas(searchParams); + } catch (err) { + showNotifications(new HttpApiCallError(err)); + return; + } + +// plugin list and category list are available always, even if not logged in +const nextState = { + loading: false, + paginationOffset: searchParams.offset, + paginationLimit: searchParams.limit, + // plugins: pluginsMeta +}; + + if (isLoggedIn()) { + try { + const stars = await client.getPluginStars(); + stars.data.forEach((star) => { + const pluginId = star.meta_id; + starsByPlugin[pluginId] = star; + }); + nextState.starsByPlugin = starsByPlugin; + } catch (err) { + showNotifications(new HttpApiCallError(err)); + } } + // finally update the state once with pluginList, categories, and maybe starsByPlugin + setLoading(false) + setPaginationOffset(searchParams.offset) + setPaginationLimit(searchParams.limit) + setPlugins(pluginsMeta) +} + /** - * Re-fetch the list of plugins if the input was changed - * in the NarBar's search bar. + * Fetch and accumulate all existing categories from the backend. + * Temporary, until there is a backend function for this. + * @returns void */ - async componentDidUpdate(prevProps) { - // eslint-disable-next-line react/destructuring-assignment - if (this.props.location !== prevProps.location) { - await this.refreshPluginList(); + const fetchAllCategories = async () => { + const CATEGORIES = ['FreeSurfer', 'MRI', 'Segmentation']; + + const categoriesMap = new Map() + CATEGORIES.forEach((name) => categoriesMap.set(name, 0)); + let pluginCategories; + try { + pluginCategories = await client.getPlugins({ + limit: 1e6, + offset: 0, + name_title_category: null, + }); + } catch (err) { + showNotifications(new HttpApiCallError(err)); + return; } + // count the frequency of pluginCategories which belong to categories + // eslint-disable-next-line no-restricted-syntax + for (const { category } of pluginCategories.data) + if (category) + categoriesMap.set( + category, + categoriesMap.has(category) ? + categoriesMap.get(category) + 1 : 1); + setCategories(categoriesMap) } + /** + * 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. + */ +useEffect(() => { + const fetchData = async() => { + await refreshPluginList() + await fetchAllCategories(); + } + return fetchData() + }) + /** * Add a star next to the plugin visually. * (Does not necessarily send to the backend that the plugin is a favorite). @@ -86,8 +181,8 @@ export class Plugins extends Component { * @param {number} pluginId * @param star */ - setPluginStar(pluginId, star) { - this.setState((prevState) => ({ +const setPluginStar = (pluginId, star) => { + setStarsByPlugin((prevState) => ({ starsByPlugin: { ...prevState.starsByPlugin, [pluginId]: star, @@ -95,131 +190,29 @@ export class Plugins extends Component { })); } - // eslint-disable-next-line react/destructuring-assignment - isLoggedIn = () => this.props.store ? this.props.store.get("isLoggedIn") : false; - - // eslint-disable-next-line react/destructuring-assignment - isFavorite = ({ id }) => this.state.starsByPlugin[id] !== undefined; - /** * Show only plugins which are part of this category. * * @param name name of category */ - handleCategorySelect = (category) => { - this.setState({ loading: true, selectedCategory: category }); - this.refreshPluginList({ category }) +const handleCategorySelect = (category) => { + setLoading(true) + setSelectedCategory(category) + refreshPluginList({ category }) } - handleToggleSort = () => { - this.setState((prevState) => ({ +const handleToggleSort = () => { + setIsSortOpen((prevState) => ({ isSortOpen: !prevState.isSortOpen })) } - // eslint-disable-next-line no-unused-vars - handleSortingSelect = (sort) => { - /** - * @todo sort plugins - */ - this.handleToggleSort() - } - - /** - * 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 query = params.get('q'); - const { match } = this.props; - - const searchParams = { - limit: 20, - offset: 0, - ...search - }; + const handleSortingSelect = () => { /** - * 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`. + * @todo sort plugins */ - else if (query) - searchParams.name_title_category = query; - - let plugins; - try { - plugins = await this.client.getPluginMetas(searchParams); - } catch (error) { - this.showNotifications(new HttpApiCallError(error)); - return; - } - - // plugin list and category list are available always, even if not logged in - const nextState = { - loading: false, - paginationOffset: searchParams.offset, - paginationLimit: searchParams.limit, - plugins - }; - - if (this.isLoggedIn()) { - try { - const stars = await this.client.getPluginStars(); - const starsByPlugin = {}; - stars.data.forEach((star) => { - const pluginId = star.meta_id; - starsByPlugin[pluginId] = star; - }); - nextState.starsByPlugin = starsByPlugin; - } catch (error) { - this.showNotifications(new HttpApiCallError(error)); - } - } - - // finally update the state once with pluginList, categories, and maybe starsByPlugin - this.setState(nextState); - } - - /** - * Fetch and accumulate all existing categories from the backend. - * Temporary, until there is a backend function for this. - * @returns void - */ - async fetchAllCategories() { - let catplugins; - try { - catplugins = await this.client.getPlugins({ - limit: 1e6, - offset: 0, - name_title_category: null, - }); - } catch (error) { - this.showNotifications(new HttpApiCallError(error)); - return; - } - - const { categories } = this.state; - // count the frequency of catplugins which belong to categories - // eslint-disable-next-line no-restricted-syntax - for (const { category } of catplugins.data) - if (category) - categories.set( - category, - categories.has(category) ? - categories.get(category) + 1 : 1); - - this.setState({ categories }); + handleToggleSort() } /** @@ -227,18 +220,8 @@ export class Plugins extends Component { * (Does not necessarily send to the backend that the plugin was unfavorited.) * @param pluginId */ - removePluginStar(pluginId) { - this.setPluginStar(pluginId, undefined); - } - - /** - * Show a notification for some network error. - * @param error error message - */ - showNotifications(error) { - this.setState({ - error: error.message, - }) +const removePluginStar = (pluginId) => { + setPluginStar(pluginId, undefined); } /** @@ -248,18 +231,18 @@ export class Plugins extends Component { * @param plugin * @return {Promise} */ - async favPlugin(plugin) { +const favPlugin= async (plugin) => { // Early state change for instant visual feedback - this.setPluginStar(plugin.id, {}); + setPluginStar(plugin.id, {}); try { - const star = await this.client.createPluginStar({ + const star = await client.createPluginStar({ plugin_name: plugin.name, }); - this.setPluginStar(plugin.id, star.data); + setPluginStar(plugin.id, star.data); } catch (err) { - this.removePluginStar(plugin.id); - this.showNotifications(new HttpApiCallError(err)); + removePluginStar(plugin.id); + showNotifications(new HttpApiCallError(err)); } } @@ -269,19 +252,18 @@ export class Plugins extends Component { * @param plugin * @return {Promise} */ - async unfavPlugin(plugin) { - // eslint-disable-next-line react/destructuring-assignment - const previousStarState = { ...this.state.starsByPlugin[plugin.id] }; + const unfavPlugin = async (plugin) =>{ + const previousStarState = { ...starsByPlugin[plugin.id] }; // Early state change for instant visual feedback - this.removePluginStar(plugin.id); + removePluginStar(plugin.id); try { - const star = await this.client.getPluginStar(previousStarState.id); + const star = await client.getPluginStar(previousStarState.id); await star.delete(); } catch (err) { - this.setPluginStar(plugin.id, previousStarState); - this.showNotifications(new HttpApiCallError(err)); + setPluginStar(plugin.id, previousStarState); + showNotifications(new HttpApiCallError(err)); } } @@ -291,35 +273,34 @@ export class Plugins extends Component { * @param plugin * @return {Promise} */ - togglePluginFavorited(plugin) { - if (this.isLoggedIn()) { - if (this.isFavorite(plugin)) { - this.unfavPlugin(plugin); +const togglePluginFavorited = (plugin) => { + if (isLoggedIn()) { + if (isFavorite(plugin)) { + unfavPlugin(plugin); } else { - this.favPlugin(plugin); + favPlugin(plugin); } } else { - this.showNotifications(new Error('Login required to favorite this plugin.')); + showNotifications(new Error('Login required to favorite this plugin.')); } } - render() { - // convert map into the data structure expected by - const { categories, plugins, loading, error } = this.state; - const { paginationOffset, paginationLimit, isSortOpen, selectedCategory } = this.state; - const categoryEntries = Array.from(categories.entries(), ([name, count]) => ({ + // convert map into the data structure expected by + const categoryEntries = categories ? Array.from(categories.entries(), ([name, count]) => ({ name, length: count - })); + })) : []; const pluginList = new Map() + if (plugins) { // eslint-disable-next-line no-restricted-syntax for (const plugin of plugins.data) { plugin.authors = removeEmail(plugin.authors.split(',')) pluginList.set(plugin.name, plugin) } + } // Render the pluginList if the plugins have been fetched const PluginListBody = () => { @@ -329,26 +310,29 @@ export class Plugins extends Component { this.togglePluginFavorited(plugin)} + isLoggedIn={isLoggedIn()} + isFavorite={isFavorite(plugin)} + onStarClicked={() => togglePluginFavorited(plugin)} /> )); + if (loading) + return

loading

return new Array(6).fill().map((e, i) => ( // eslint-disable-next-line react/no-array-index-key )); } + const PaginationControls = () => (
) - - const pluginsCount=plugins.totalCount > 0 ? plugins.totalCount : 0; - + + + const pluginsCount= plugins.totalCount <= 0 ? 0 : plugins.totalCount return (
@@ -379,7 +363,7 @@ export class Plugins extends Component { position='top-right' variant='danger' closeable - onClose={() => this.setState({ error: null })} + onClose={() => setError(null)} /> )} @@ -414,7 +398,8 @@ export class Plugins extends Component { ) : (

- {pluginsCount} plugins found + {pluginsCount} + plugins found

Showing {paginationOffset + 1} to {' '} { @@ -434,11 +419,11 @@ export class Plugins extends Component { handleSortingSelect()} isOpen={isSortOpen} toggle={ handleToggleSort()} toggleIndicator={CaretDownIcon}> Sort by @@ -468,7 +453,7 @@ export class Plugins extends Component { handleCategorySelect()} selected={selectedCategory} /> @@ -477,10 +462,12 @@ export class Plugins extends Component {
); } -} + Plugins.propTypes = { store: PropTypes.objectOf(PropTypes.object).isRequired, }; export default ChrisStore.withStore(Plugins); + +