Skip to content

Commit d47b950

Browse files
authored
feat: show changelog and plugin tag for apps (#580)
* feat: show changelog and plugin tag for apps * feat: display latest updates in About section of app
1 parent e0b4f41 commit d47b950

20 files changed

+1197
-114
lines changed

jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ const config = {
44
'src/**/*.{js,jsx,ts,tsx}',
55
'!<rootDir>/node_modules/',
66
],
7+
moduleNameMapper: {
8+
'react-markdown': '<rootDir>/src/test-utils/react-markdown-mock.jsx',
9+
'\\.(css|scss)$': 'identity-obj-proxy',
10+
},
711
coverageThreshold: {
812
global: {
913
// TODO: The following should be ~50%

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,17 @@
3434
"@testing-library/react": "^16.2.0",
3535
"@testing-library/user-event": "^14.6.1",
3636
"@types/jest": "^29.5.11",
37+
"identity-obj-proxy": "^3.0.0",
3738
"jest": "^29.7.0"
3839
},
3940
"dependencies": {
40-
"@dhis2/app-runtime": "^3.2.1",
41+
"@dhis2/app-runtime": "^3.13.1",
4142
"@dhis2/d2-i18n": "^1.1.3",
4243
"@dhis2/ui": "^10.1.11",
4344
"moment": "^2.29.0",
4445
"prop-types": "^15.8.1",
4546
"query-string": "^6.14.1",
47+
"react-markdown": "^9.0.3",
4648
"react-router-dom": "^5.2.0",
4749
"semver": "^7.3.4",
4850
"use-debounce": "^6.0.0",

src/components/AppCard/AppCard.jsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import PropTypes from 'prop-types'
22
import React from 'react'
3+
import { appTypeToDisplayName } from '../AppDetails/appDisplayConfig.js'
4+
import PluginTag from '../AppDetails/PluginTag.jsx'
35
import { AppIcon } from '../AppIcon/AppIcon.jsx'
46
import styles from './AppCard.module.css'
57

68
export const AppCard = ({
9+
appType,
10+
hasPlugin,
11+
pluginType,
712
iconSrc,
813
appName,
914
appDeveloper,
@@ -15,6 +20,16 @@ export const AppCard = ({
1520
<div>
1621
<h2 className={styles.appCardName}>{appName}</h2>
1722
<span className={styles.appCardMetadata}>{appDeveloper}</span>
23+
{hasPlugin && (
24+
<div className={styles.tags}>
25+
{appTypeToDisplayName[appType] && (
26+
<span className={styles.appCardType}>
27+
{appTypeToDisplayName[appType]}
28+
</span>
29+
)}
30+
<PluginTag hasPlugin={hasPlugin} pluginType={pluginType} />
31+
</div>
32+
)}
1833
<span className={styles.appCardMetadata}>
1934
{appVersion && `Version ${appVersion}`}
2035
</span>
@@ -26,6 +41,9 @@ AppCard.propTypes = {
2641
appName: PropTypes.string.isRequired,
2742
onClick: PropTypes.func.isRequired,
2843
appDeveloper: PropTypes.string,
44+
appType: PropTypes.string,
2945
appVersion: PropTypes.string,
46+
hasPlugin: PropTypes.bool,
3047
iconSrc: PropTypes.string,
48+
pluginType: PropTypes.string,
3149
}

src/components/AppCard/AppCard.module.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,10 @@
3535
font-size: 14px;
3636
color: var(--colors-grey700);
3737
}
38+
39+
.tags {
40+
composes: appCardMetadata;
41+
display: flex;
42+
align-items: center;
43+
gap: 5px;
44+
}

src/components/AppDetails/AppDetails.jsx

Lines changed: 123 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import i18n from '@dhis2/d2-i18n'
2-
import { Button, Card, Divider } from '@dhis2/ui'
2+
import {
3+
Button,
4+
Card,
5+
Divider,
6+
IconTerminalWindow16,
7+
IconUser16,
8+
Tab,
9+
TabBar,
10+
} from '@dhis2/ui'
311
import moment from 'moment'
412
import PropTypes from 'prop-types'
513
import React, { useState } from 'react'
14+
import { useHistory, useLocation } from 'react-router-dom'
615
import { getAppIconSrc } from '../../get-app-icon-src.js'
716
import { getLatestVersion } from '../../get-latest-version.js'
817
import { AppIcon } from '../AppIcon/AppIcon.jsx'
918
import styles from './AppDetails.module.css'
19+
import { appTypeToDisplayName } from './appDisplayConfig.js'
1020
import { Description } from './Description.jsx'
21+
import { LatestUpdates } from './LatestUpdates.jsx'
1122
import { ManageInstalledVersion } from './ManageInstalledVersion.jsx'
23+
import PluginTag from './PluginTag.jsx'
1224
import { Versions } from './Versions.jsx'
1325

1426
const Metadata = ({ installedVersion, versions }) => {
@@ -89,6 +101,7 @@ export const AppDetails = ({
89101
appHubApp,
90102
onVersionInstall,
91103
onUninstall,
104+
changelog,
92105
}) => {
93106
const appName = installedApp ? installedApp.name : appHubApp.name
94107
const appDeveloper = appHubApp
@@ -105,6 +118,19 @@ export const AppDetails = ({
105118
.map((i) => i.imageUrl)
106119
const versions = appHubApp?.versions.sort((a, b) => b.created - a.created)
107120

121+
const history = useHistory()
122+
123+
const location = useLocation()
124+
125+
const selectedTab =
126+
new URLSearchParams(location.search).get('tab') ?? 'about'
127+
128+
const selectTab = (tabName) => () => {
129+
history.push('?tab=' + tabName)
130+
}
131+
132+
const hasChangelog = !!changelog && Object.keys(changelog)?.length > 0
133+
108134
return (
109135
<Card className={styles.appCard}>
110136
<header className={styles.header}>
@@ -113,14 +139,29 @@ export const AppDetails = ({
113139
</div>
114140
<div>
115141
<h1 className={styles.headerName}>{appName}</h1>
116-
{appDeveloper && (
117-
<span className={styles.headerDeveloper}>
118-
{i18n.t('by {{- developer}}', {
119-
developer: appDeveloper,
120-
context: 'developer of application',
121-
})}
122-
</span>
123-
)}
142+
<div className={styles.appTags}>
143+
{appDeveloper && (
144+
<div className={styles.tagWithIcon}>
145+
<IconUser16 />
146+
147+
{appDeveloper}
148+
</div>
149+
)}
150+
<div
151+
data-test="app-type"
152+
className={styles.tagWithIcon}
153+
>
154+
<IconTerminalWindow16 />
155+
{appTypeToDisplayName[appHubApp?.appType] ??
156+
appHubApp?.appType}
157+
</div>
158+
{appHubApp?.hasPlugin && (
159+
<PluginTag
160+
hasPlugin={appHubApp.hasPlugin}
161+
pluginType={appHubApp.pluginType}
162+
/>
163+
)}
164+
</div>
124165
</div>
125166
<div>
126167
{installedApp?.launchUrl && (
@@ -137,55 +178,86 @@ export const AppDetails = ({
137178
</div>
138179
</header>
139180
<Divider />
140-
<section className={[styles.section, styles.mainSection].join(' ')}>
141-
<div>
142-
<h2 className={styles.sectionHeader}>
143-
{i18n.t('About this app')}
144-
</h2>
145-
{description ? (
146-
<Description description={description} />
147-
) : (
148-
<em>
149-
{i18n.t(
150-
'The developer of this application has not provided a description'
151-
)}
152-
</em>
153-
)}
154-
</div>
155-
<div>
156-
<ManageInstalledVersion
157-
installedApp={installedApp}
158-
versions={appHubApp?.versions}
159-
onVersionInstall={onVersionInstall}
160-
onUninstall={onUninstall}
161-
/>
162-
{installedApp && appHubApp && (
181+
<TabBar dataTest="tabbar-appview">
182+
<Tab
183+
onClick={selectTab('about')}
184+
selected={selectedTab == 'about'}
185+
>
186+
About
187+
</Tab>
188+
<Tab
189+
onClick={selectTab('previous-releases')}
190+
selected={selectedTab == 'previous-releases'}
191+
>
192+
Previous releases
193+
</Tab>
194+
</TabBar>
195+
196+
{selectedTab == 'about' && (
197+
<>
198+
<section
199+
className={[styles.section, styles.mainSection].join(
200+
' '
201+
)}
202+
>
163203
<div>
164204
<h2 className={styles.sectionHeader}>
165-
{i18n.t('Additional information')}
205+
{i18n.t('About this app')}
166206
</h2>
167-
<Metadata
168-
installedVersion={installedApp.version}
169-
versions={versions}
207+
{description ? (
208+
<Description description={description} />
209+
) : (
210+
<em>
211+
{i18n.t(
212+
'The developer of this application has not provided a description'
213+
)}
214+
</em>
215+
)}
216+
{hasChangelog && (
217+
<LatestUpdates
218+
installedVersion={installedApp?.version}
219+
versions={versions}
220+
changelog={changelog}
221+
/>
222+
)}
223+
</div>
224+
<div>
225+
<ManageInstalledVersion
226+
installedApp={installedApp}
227+
versions={appHubApp?.versions}
228+
onVersionInstall={onVersionInstall}
229+
onUninstall={onUninstall}
170230
/>
231+
{installedApp && appHubApp && (
232+
<div>
233+
<h2 className={styles.sectionHeader}>
234+
{i18n.t('Additional information')}
235+
</h2>
236+
237+
<Metadata
238+
installedVersion={installedApp.version}
239+
versions={versions}
240+
/>
241+
</div>
242+
)}
171243
</div>
172-
)}
173-
</div>
174-
</section>
175-
{screenshots?.length > 0 && (
176-
<>
177-
<Divider />
178-
<section className={styles.section}>
179-
<h2 className={styles.sectionHeader}>
180-
{i18n.t('Screenshots')}
181-
</h2>
182-
<Screenshots screenshots={screenshots} />
244+
{screenshots?.length > 0 && (
245+
<div>
246+
<Divider />
247+
<section className={styles.section}>
248+
<h2 className={styles.sectionHeader}>
249+
{i18n.t('Screenshots')}
250+
</h2>
251+
<Screenshots screenshots={screenshots} />
252+
</section>
253+
</div>
254+
)}
183255
</section>
184256
</>
185257
)}
186-
{appHubApp && (
258+
259+
{appHubApp && selectedTab === 'previous-releases' && (
187260
<>
188-
<Divider />
189261
<section className={styles.section}>
190262
<h2 className={styles.sectionHeader}>
191263
{i18n.t(
@@ -196,6 +268,7 @@ export const AppDetails = ({
196268
installedVersion={installedApp?.version}
197269
versions={versions}
198270
onVersionInstall={onVersionInstall}
271+
changelog={changelog}
199272
/>
200273
</section>
201274
</>
@@ -207,6 +280,7 @@ export const AppDetails = ({
207280
AppDetails.propTypes = {
208281
onVersionInstall: PropTypes.func.isRequired,
209282
appHubApp: PropTypes.object,
283+
changelog: PropTypes.object,
210284
installedApp: PropTypes.object,
211285
onUninstall: PropTypes.func,
212286
}

src/components/AppDetails/AppDetails.module.css

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@
2828
color: var(--colors-grey700);
2929
}
3030

31+
.appTags {
32+
display: flex;
33+
gap: 10px;
34+
font-size: 1rem;
35+
color: var(--colors-grey800);
36+
}
37+
38+
.tagWithIcon {
39+
display: flex;
40+
gap: var(--spacers-dp4);
41+
color: var(--colors-grey800);
42+
align-items: center;
43+
}
44+
3145
.sectionHeader {
3246
margin: 0;
3347
margin-bottom: var(--spacers-dp16);
@@ -110,10 +124,6 @@
110124
box-shadow: 0 0 0 3px #2196f37d;
111125
}
112126

113-
.versionsContainer {
114-
border: 1px solid var(--colors-grey300);
115-
}
116-
117127
.versionsFilters {
118128
padding: var(--spacers-dp16) var(--spacers-dp8);
119129
}

0 commit comments

Comments
 (0)