From 60dae19c05902e305278fde02377ca877a1363cc Mon Sep 17 00:00:00 2001 From: Avishka-Shamendra Date: Tue, 3 Sep 2024 11:44:33 +0530 Subject: [PATCH] Update devportal UI to support search enhancements --- .../main/webapp/site/public/locales/en.json | 5 +- .../Apis/Listing/APICards/DefThumb.jsx | 190 ++++++++++ .../Apis/Listing/APICards/DocThumb.jsx | 340 +++++++----------- .../Apis/Listing/APICards/ImageGenerator.jsx | 4 + .../components/Apis/Listing/ApiTableView.jsx | 24 +- .../Base/Header/Search/SearchUtils.jsx | 29 +- .../source/src/app/data/defaultTheme.js | 7 + 7 files changed, 388 insertions(+), 211 deletions(-) create mode 100644 portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DefThumb.jsx diff --git a/portals/devportal/src/main/webapp/site/public/locales/en.json b/portals/devportal/src/main/webapp/site/public/locales/en.json index caa0c72675d..7fdd9b76f29 100644 --- a/portals/devportal/src/main/webapp/site/public/locales/en.json +++ b/portals/devportal/src/main/webapp/site/public/locales/en.json @@ -355,7 +355,9 @@ "Apis.Listing.ApiThumb.version": "Version", "Apis.Listing.CategoryListingCategories.categoriesNotFound": "Categories cannot be found", "Apis.Listing.CategoryListingCategories.title": "API Categories", - "Apis.Listing.DocThumb.apiName": "Api Name", + "Apis.Listing.DefThumb.apiName": "API Name", + "Apis.Listing.DefThumb.apiVersion": "API Version", + "Apis.Listing.DocThumb.apiName": "API Name", "Apis.Listing.DocThumb.apiVersion": "API Version", "Apis.Listing.DocThumb.sourceType": "Source Type:", "Apis.Listing.Listing.ApiTagCloud.title": "Tags / API Categories", @@ -373,6 +375,7 @@ "Apis.Listing.StarRatingBar.users": "users", "Apis.Listing.StarRatingBar.you": "You", "Apis.Listing.SubscriptionPolicySelect.subscribe": "Subscribe", + "Apis.Listing.TableView.TableView.def.flag": "[Def]", "Apis.Listing.TableView.TableView.doc.flag": "[Doc]", "Apis.Listing.TagCloudListing.apigroups.main": "API Groups", "Apis.Listing.TagCloudListingTags.allApis": "All Apis", diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DefThumb.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DefThumb.jsx new file mode 100644 index 00000000000..18110d3bb9f --- /dev/null +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DefThumb.jsx @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useState } from 'react'; +import { styled } from '@mui/material/styles'; +import { useTheme } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import { useHistory } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardMedia from '@mui/material/CardMedia'; +import { FormattedMessage } from 'react-intl'; +import ImageGenerator from './ImageGenerator'; + +const PREFIX = 'DefinitionThumb'; + +const classes = { + root: `${PREFIX}-root`, + media: `${PREFIX}-media`, + content: `${PREFIX}-content`, + actions: `${PREFIX}-actions`, + header: `${PREFIX}-header`, + info: `${PREFIX}-info`, + apiName: `${PREFIX}-apiName`, + version: `${PREFIX}-version`, + subtitle: `${PREFIX}-subtitle`, +}; + +const StyledCard = styled(Card)(({ theme }) => ({ + [`&.${classes.root}`]: { + width: theme.custom.thumbnail.width, + backgroundColor: '#f5f5f5', + minHeight: 330, + margin: theme.spacing(2), + cursor: 'pointer', + display: 'flex', + flexDirection: 'column', + transition: 'background-color 0.3s ease', + '&:hover': { + backgroundColor: theme.palette.grey[300], + }, + }, + [`& .${classes.media}`]: { + height: 200, + }, + [`& .${classes.content}`]: { + flexGrow: 1, + paddingBottom: theme.spacing(1), + }, + [`& .${classes.actions}`]: { + display: 'flex', + justifyContent: 'space-between', + padding: theme.spacing(1), + }, + [`& .${classes.header}`]: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'normal', + wordBreak: 'break-word', + }, + [`& .${classes.info}`]: { + display: 'flex', + justifyContent: 'space-between', + }, + [`& .${classes.apiName}`]: { + flex: 1, + }, + [`& .${classes.version}`]: { + flex: 1, + textAlign: 'right', + }, + [`& .${classes.subtitle}`]: { + color: theme.palette.grey[600], + fontSize: '0.75rem', + }, +})); + +const DefinitionThumb = ({ def }) => { + const [state] = useState({ + category: null, + selectedIcon: null, + color: null, + backgroundIndex: null, + imageObj: null, + }); + + const theme = useTheme(); + const history = useHistory(); + const detailsLink = `/apis/${def.apiUUID}/overview`; + const { + name, apiName, apiVersion, + } = def; + const { + category, selectedIcon, color, backgroundIndex, + } = state; + + useEffect(() => { + return () => { + if (state.imageObj) { + window.URL.revokeObjectURL(state.imageObj); + } + }; + }, [state.imageObj]); + + const handleCardClick = () => { + history.push(detailsLink); + }; + + return ( + + {theme.custom.thumbnail.defaultApiImage ? ( + + ) : ( + + )} + + + {name} + +
+ + {apiName} + + + {apiVersion} + +
+
+ + + + + + +
+
+
+ ); +}; + +DefinitionThumb.propTypes = { + def: PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + apiName: PropTypes.string, + apiVersion: PropTypes.string, + apiContext: PropTypes.string, + apiUUID: PropTypes.string, + apiProvider: PropTypes.string, + apiType: PropTypes.string, + }).isRequired, +}; + +export default DefinitionThumb; diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DocThumb.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DocThumb.jsx index 3d35382c3b3..96a649458d8 100755 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DocThumb.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/DocThumb.jsx @@ -16,245 +16,173 @@ * under the License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { styled } from '@mui/material/styles'; +import { useTheme } from '@mui/material'; import Typography from '@mui/material/Typography'; -import { Link } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardMedia from '@mui/material/CardMedia'; import { FormattedMessage } from 'react-intl'; -import MaterialIcons from 'MaterialIcons'; -import { useTheme } from '@mui/material'; import ImageGenerator from './ImageGenerator'; -import { ApiContext } from '../../Details/ApiContext'; const PREFIX = 'DocThumbLegacy'; const classes = { - thumbContent: `${PREFIX}-thumbContent`, - thumbLeft: `${PREFIX}-thumbLeft`, - thumbRight: `${PREFIX}-thumbRight`, - thumbInfo: `${PREFIX}-thumbInfo`, - thumbHeader: `${PREFIX}-thumbHeader`, - contextBox: `${PREFIX}-contextBox`, - thumbWrapper: `${PREFIX}-thumbWrapper`, - deleteIcon: `${PREFIX}-deleteIcon`, - textWrapper: `${PREFIX}-textWrapper`, - imageWrapper: `${PREFIX}-imageWrapper`, - imageOverlap: `${PREFIX}-imageOverlap`, + root: `${PREFIX}-root`, + media: `${PREFIX}-media`, + content: `${PREFIX}-content`, + actions: `${PREFIX}-actions`, + header: `${PREFIX}-header`, + info: `${PREFIX}-info`, + apiName: `${PREFIX}-apiName`, + version: `${PREFIX}-version`, + subtitle: `${PREFIX}-subtitle`, }; -const Root = styled('div')(( - { - theme, +const StyledCard = styled(Card)(({ theme }) => ({ + [`&.${classes.root}`]: { + width: theme.custom.thumbnail.width, + backgroundColor: '#f5f5f5', + minHeight: 330, + margin: theme.spacing(2), + cursor: 'pointer', + transition: 'background-color 0.3s ease', + '&:hover': { + backgroundColor: theme.palette.grey[300], + }, }, -) => ({ - [`& .${classes.thumbContent}`]: { - width: theme.custom.thumbnail.width - theme.spacing(1), - backgroundColor: theme.palette.background.paper, - padding: theme.spacing(1), - minHeight: 130, + [`& .${classes.media}`]: { + height: 200, }, - - [`& .${classes.thumbLeft}`]: { - alignSelf: 'flex-start', - flex: 1, + [`& .${classes.content}`]: { + paddingBottom: theme.spacing(1), }, - - [`& .${classes.thumbRight}`]: { - alignSelf: 'flex-end', + [`& .${classes.actions}`]: { display: 'flex', - flexDirection: 'column', - }, - - [`& .${classes.thumbInfo}`]: { - display: 'flex', - }, - - [`& .${classes.thumbHeader}`]: { - width: theme.custom.thumbnail.width - theme.spacing(1), - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - cursor: 'pointer', - margin: 0, + justifyContent: 'space-between', + padding: theme.spacing(1), }, - - [`& .${classes.contextBox}`]: { - width: parseInt((theme.custom.thumbnail.width - theme.spacing(1)) / 2, 10), + [`& .${classes.header}`]: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', - cursor: 'pointer', - margin: 0, - display: 'inline-block', - lineHeight: '1em', }, - - [`&.${classes.thumbWrapper}`]: { - position: 'relative', - paddingTop: 15, - marginLeft: theme.spacing(2), - }, - - [`& .${classes.deleteIcon}`]: { - fill: 'red', + [`& .${classes.info}`]: { + display: 'flex', + justifyContent: 'space-between', }, - - [`& .${classes.textWrapper}`]: { - color: theme.palette.text.secondary, - textDecoration: 'none', + [`& .${classes.apiName}`]: { + flex: 1, }, - - [`& .${classes.imageWrapper}`]: { - color: theme.palette.text.secondary, - backgroundColor: theme.palette.background.paper, - width: theme.custom.thumbnail.width + theme.spacing(1), - display: 'flex', - alignItems: 'center', - justifyContent: 'center', + [`& .${classes.version}`]: { + flex: 1, + textAlign: 'right', }, - - [`& .${classes.imageOverlap}`]: { - position: 'absolute', - bottom: 1, - backgroundColor: theme.custom.thumbnail.contentBackgroundColor, + [`& .${classes.subtitle}`]: { + color: theme.palette.grey[600], + fontSize: '0.75rem', }, })); -const windowURL = window.URL || window.webkitURL; -/** - * - * - * @class DocThumbLegacy - * @extends {React.Component} - */ -class DocThumbLegacy extends React.Component { - /** - * Creates an instance of DocThumbLegacy. - * @param {JSON} props properties - * @memberof DocThumbLegacy - */ - constructor(props) { - super(props); - this.state = { - category: MaterialIcons.categories[0].name, - selectedIcon: null, - color: null, - backgroundIndex: null, - imageObj: null, - }; - } - - /** - * Clean up resource - */ - componentWillUnmount() { - const { thumbnail, imageObj } = this.state; - if (thumbnail) { - windowURL.revokeObjectURL(imageObj); - } - } +const DocThumbLegacy = ({ doc }) => { + const [state] = useState({ + category: null, + selectedIcon: null, + color: null, + backgroundIndex: null, + imageObj: null, + }); - /** - * @returns {JSX} doc thumbnail - * @memberof DocThumbLegacy - */ - render() { - const { - selectedIcon, color, backgroundIndex, category, - } = this.state; - const { doc, theme } = this.props; - const { - doc: { - name, sourceType, apiName, apiVersion, id, apiUUID, - }, - } = this.props; - const detailsLink = '/apis/' + apiUUID + '/documents/' + id + '/details'; - const { thumbnail } = theme.custom; - const imageWidth = thumbnail.width; - const defaultImage = thumbnail.defaultApiImage; - - const ImageView = ( - - ); + const theme = useTheme(); + const history = useHistory(); + const detailsLink = `/apis/${doc.apiUUID}/documents/${doc.id}/details`; + const { + category, selectedIcon, color, backgroundIndex, + } = state; + const { + name, sourceType, apiName, apiVersion, + } = doc; + + useEffect(() => { + return () => { + if (state.imageObj) { + window.URL.revokeObjectURL(state.imageObj); + } + }; + }, [state.imageObj]); - return ( - - - {!defaultImage && ImageView} - {defaultImage && document} - + const handleCardClick = () => { + history.push(detailsLink); + }; -
+ {theme.custom.thumbnail.defaultApiImage ? ( + + ) : ( + + )} + + - - - {name} - - - - - {sourceType} + {name} + + + + {sourceType} + +
+ + {apiName} + + + {apiVersion} + +
+
+ + + + + -
-
- {apiName} - - - -
-
- - {apiVersion} - - - - -
-
- - ); - } -} +
+ + ); +}; DocThumbLegacy.propTypes = { - classes: PropTypes.shape({}).isRequired, - theme: PropTypes.shape({}).isRequired, + doc: PropTypes.shape({ + name: PropTypes.string, + sourceType: PropTypes.string, + apiName: PropTypes.string, + apiVersion: PropTypes.string, + id: PropTypes.string, + apiUUID: PropTypes.string, + }).isRequired, }; -DocThumbLegacy.contextType = ApiContext; - -function DocThumb(props) { - const { doc } = props; - const theme = useTheme(); - return ( - - ); -} - -export default (DocThumb); +export default DocThumbLegacy; diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/ImageGenerator.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/ImageGenerator.jsx index 3d36ebd907c..7f726845a50 100755 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/ImageGenerator.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/APICards/ImageGenerator.jsx @@ -78,6 +78,8 @@ class ImageGeneratorLegacy extends PureComponent { IconElement = key; } else if (api.type === 'DOC') { IconElement = theme.custom.thumbnail.document.icon; + } else if (api.type === 'DEFINITION') { + IconElement = theme.custom.thumbnail.definition.icon; } else { count = MaterialIcons.categories[10].icons.length; const randomIconIndex = (str.charCodeAt(0) + str.charCodeAt(str.length - 1)) % count; @@ -87,6 +89,8 @@ class ImageGeneratorLegacy extends PureComponent { // Obtain or generate background color pair if (api.type === 'DOC') { colorPair = theme.custom.thumbnail.document.backgrounds; + } else if (api.type === 'DEFINITION') { + colorPair = theme.custom.thumbnail.definition.backgrounds; } else if (backgroundIndex && colorPairs.length > backgroundIndex) { colorPair = colorPairs[backgroundIndex]; } else { diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/ApiTableView.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/ApiTableView.jsx index 3dece98983c..23b3ce57b7c 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/ApiTableView.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Apis/Listing/ApiTableView.jsx @@ -44,6 +44,7 @@ import { useTheme } from '@mui/material'; import ImageGenerator from './APICards/ImageGenerator'; import ApiThumb from './ApiThumb'; import DocThumb from './APICards/DocThumb'; +import DefinitionThumb from './APICards/DefThumb'; import { ApiContext } from '../Details/ApiContext'; import NoApi from './NoApi'; @@ -378,6 +379,25 @@ class ApiTableViewLegacy extends React.Component { ); + } else if (artifact.type === 'DEFINITION') { + return ( + + code + + + {' '} + + {' '} + {apiName} + + + ); } const strokeColor = theme.palette.getContrastText(theme.custom.listView.tableBodyEvenBackgrund); return ( @@ -486,7 +506,7 @@ class ApiTableViewLegacy extends React.Component { if (tableMeta.rowData) { const artifact = tableViewObj.state.data[tableMeta.rowIndex]; if (artifact) { - if (artifact.type !== 'DOC') { + if (artifact.type !== 'DOC' && artifact.type !== 'DEFINITION') { const apiId = tableMeta.rowData[0]; const avgRating = tableMeta.rowData[8]; return ( @@ -579,6 +599,8 @@ class ApiTableViewLegacy extends React.Component { if (artifact) { if (artifact.type === 'DOC') { return ; + } else if (artifact.type === 'DEFINITION') { + return ; } else { return ( diff --git a/portals/devportal/src/main/webapp/source/src/app/components/Base/Header/Search/SearchUtils.jsx b/portals/devportal/src/main/webapp/source/src/app/components/Base/Header/Search/SearchUtils.jsx index 1dc3b904b44..e3ea4c834a3 100644 --- a/portals/devportal/src/main/webapp/source/src/app/components/Base/Header/Search/SearchUtils.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/components/Base/Header/Search/SearchUtils.jsx @@ -32,6 +32,7 @@ import SearchOutlined from '@mui/icons-material/SearchOutlined'; import { Link } from 'react-router-dom'; import APIsIcon from '@mui/icons-material/SettingsApplicationsOutlined'; import DocumentsIcon from '@mui/icons-material/LibraryBooks'; +import CodeIcon from '@mui/icons-material/Code'; import NativeSelect from '@mui/material/NativeSelect'; import { FormattedMessage } from 'react-intl'; import CircularProgress from '@mui/material/CircularProgress'; @@ -168,6 +169,20 @@ function renderInput(inputProps) { ); } +/** + * Get search result path + */ +function getPath(suggestion) { + switch (suggestion.type) { + case 'API': + return `/apis/${suggestion.id}/overview`; + case 'DEFINITION': + return `/apis/${suggestion.apiUUID}/overview`; + default: + return `/apis/${suggestion.apiUUID}/documents/${suggestion.id}/details`; + } +} + /** * * Use your imagination to define how suggestions are rendered. @@ -178,16 +193,24 @@ function renderInput(inputProps) { function renderSuggestion(suggestion, { query, isHighlighted }) { const matches = match(suggestion.name, query); const parts = parse(suggestion.name, matches); - const path = suggestion.type === 'API' ? `/apis/${suggestion.id}/overview` - : `/apis/${suggestion.apiUUID}/documents/${suggestion.id}/details`; + const path = getPath(suggestion); // TODO: Style the version ( and apiName if docs) apearing in the menu item const suffix = suggestion.type === 'API' ? suggestion.version : (suggestion.apiName + ' ' + suggestion.apiVersion); + const getIcon = (type) => { + if (type === 'API') { + return ; + } else if (type === 'DEFINITION') { + return ; + } else { + return ; + } + }; return ( <> - { suggestion.type === 'API' ? : } + { getIcon(suggestion.type) }