Dwayne Harris 5 years ago
parent
commit
a7bfcfc270
  1. 32
      src/actions/apps.ts
  2. 3
      src/api/fetch.ts
  3. 6
      src/components/pages/self.tsx
  4. 91
      src/components/pages/view-app.tsx
  5. 12
      src/hooks/index.ts
  6. 5
      src/types/entities.ts
  7. 2
      src/types/store.ts
  8. 31
      src/utils/normalization.ts

32
src/actions/apps.ts

@ -131,3 +131,35 @@ export const fetchApp = (id: string): AppThunkAction => async dispatch => {
throw err
}
}
export const installApp = (id: string): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.InstallApp))
try {
await apiFetch({
path: `/api/app/${id}/install`,
method: 'post'
})
dispatch(finishRequest(RequestKey.InstallApp, true))
} catch (err) {
dispatch(finishRequest(RequestKey.InstallApp, false))
throw err
}
}
export const uninstallApp = (id: string): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.UninstallApp))
try {
await apiFetch({
path: `/api/app/${id}/uninstall`,
method: 'post'
})
dispatch(finishRequest(RequestKey.UninstallApp, true))
} catch (err) {
dispatch(finishRequest(RequestKey.UninstallApp, false))
throw err
}
}

3
src/api/fetch.ts

@ -76,10 +76,11 @@ export const apiFetch: APIFetch = async (options: FetchOptions) => {
const doFetch = async () => {
const headers = new Headers({
...options.headers,
'Content-Type': contentType,
'Accept': contentType,
})
if (body) headers.append('Content-Type', contentType)
const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)
if (accessToken) headers.append('Authorization', `Bearer ${accessToken}`)

6
src/components/pages/self.tsx

@ -7,7 +7,7 @@ import { faDoorOpen, faCheckCircle, faPlusCircle, faIdCard, faEnvelope, faUserSh
import { unauthenticate, updateSelf } from 'src/actions/authentication'
import { initForm, initField } from 'src/actions/forms'
import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication'
import { getAuthenticatedUser } from 'src/selectors/authentication'
import { getForm } from 'src/selectors/forms'
import { handleApiError } from 'src/api/errors'
@ -38,12 +38,10 @@ const Self: FC = () => {
const dispatch = useDispatch()
const history = useHistory()
const checked = useSelector<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const form = useSelector<AppState, Form>(getForm)
useAuthenticationCheck(checked, authenticated, history)
useAuthenticationCheck()
const handleLogout = () => {
localStorage.clear()

91
src/components/pages/view-app.tsx

@ -1,18 +1,21 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom'
import classNames from 'classnames'
import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchApp } from 'src/actions/apps'
import { fetchApp, installApp, uninstallApp } from 'src/actions/apps'
import { getAuthenticatedUserId } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { getIsFetching } from 'src/selectors/requests'
import { setTitle } from 'src/utils'
import { AppState, AppThunkDispatch, EntityType, App } from 'src/types'
import { AppState, AppThunkDispatch, EntityType, App, RequestKey } from 'src/types'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
import { ClassDictionary } from 'src/types'
interface Params {
id: string
@ -22,6 +25,7 @@ const ViewApp: FC = () => {
const { id } = useParams<Params>()
const app = useSelector<AppState, App | undefined>(state => getEntity<App>(state, EntityType.App, id))
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
const fetching = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.InstallApp) || getIsFetching(state, RequestKey.UninstallApp))
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
@ -41,48 +45,79 @@ const ViewApp: FC = () => {
const isCreator = app.user.id === selfId
const renderButton = () => {
if (app.installed) {
const handleClick = async () => {
await dispatch(uninstallApp(id))
await dispatch(fetchApp(id))
}
const classes: ClassDictionary = {
'button': true,
'is-danger': true,
'is-loading': fetching,
}
return <button className={classNames(classes)} onClick={() => handleClick()}>Uninstall</button>
} else {
const handleClick = async () => {
await dispatch(installApp(id))
await dispatch(fetchApp(id))
}
const classes: ClassDictionary = {
'button': true,
'is-success': true,
'is-loading': fetching,
}
return <button className={classNames(classes)} onClick={() => handleClick()}>Install</button>
}
}
return (
<div>
<PageHeader title={app.name} />
<div className="main-content">
<nav className="level">
<div className="level-item has-text-centered">
<div>
<p className="heading">Users</p>
<p className="title">0</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Rating</p>
<p className="title">{app.rating || '0'}</p>
<div className="centered-content">
<nav className="level">
<div className="level-item has-text-centered">
<div>
<p className="heading">Users</p>
<p className="title">{app.users}</p>
</div>
</div>
</div>
{app.companyName &&
<div className="level-item has-text-centered">
<div>
<p className="heading">Company</p>
<p className="title">{app.companyName}</p>
<p className="heading">Rating</p>
<p className="title">{app.rating}</p>
</div>
</div>
}
<div className="level-item has-text-centered">
<div>
<p className="heading">Updated</p>
<p className="title">{moment(app.updated).format('MMMM Do, YYYY')}</p>
{app.companyName &&
<div className="level-item has-text-centered">
<div>
<p className="heading">Company</p>
<p className="title">{app.companyName}</p>
</div>
</div>
}
<div className="level-item has-text-centered">
<div>
<p className="heading">Updated</p>
<p className="title">{moment(app.updated).format('MMMM Do, YYYY')}</p>
</div>
</div>
</div>
</nav>
</nav>
<div className="centered-content">
<p>{app.about}</p>
</div>
<br />
{renderButton()}
</div>
</div>
</div>
)

12
src/hooks/index.ts

@ -1,8 +1,16 @@
import { useEffect, useRef, EffectCallback } from 'react'
import { History } from 'history'
import { useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import isEqual from 'lodash/isEqual'
export const useAuthenticationCheck = (checked: boolean, authenticated: boolean, history: History) => {
import { getAuthenticated, getChecked } from 'src/selectors/authentication'
import { AppState } from 'src/types'
export const useAuthenticationCheck = () => {
const checked = useSelector<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const history = useHistory()
useEffect(() => {
if (checked && !authenticated) history.push('/login')
}, [checked, authenticated])

5
src/types/entities.ts

@ -4,6 +4,7 @@ export enum EntityType {
Log = 'logs',
Invitation = 'invitations',
App = 'apps',
Installation = 'installations',
}
export enum GroupMembershipType {
@ -24,10 +25,9 @@ export type Group = Entity & {
about: string
}
export type Installation = {
export type Installation = Entity & {
app: App
settings: object
created: number
}
export type User = Entity & {
@ -38,7 +38,6 @@ export type User = Entity & {
coverImageUrl?: string
requiresApproval: boolean
privacy: string
installations: Installation[]
}
export type GroupLog = Entity & {

2
src/types/store.ts

@ -24,6 +24,8 @@ export enum RequestKey {
FetchInvitations = 'fetch_invitations',
FetchSelfApps = 'fetch_self_apps',
FetchUserAvailability = 'fetch_user_availability',
InstallApp = 'install_app',
UninstallApp = 'uninstall_app',
Register = 'register',
UpdateGroup = 'update_group',
UpdateSelf = 'update_self',

31
src/utils/normalization.ts

@ -6,6 +6,7 @@ import {
Invitation,
GroupLog,
App,
Installation,
} from '../types'
import compact from 'lodash/compact'
@ -53,17 +54,10 @@ export function normalize(entities: Entity[], type: EntityType): NormalizeResult
case EntityType.User:
keys = entities.map(entity => {
const user = entity as User
const { installations = [] } = user
return set(type, newStore, {
...user,
group: set(EntityType.Group, newStore, user.group),
installations: installations.map(installation => {
return {
...installation,
app: set(EntityType.App, newStore, installation.app),
}
})
})
})
@ -104,6 +98,15 @@ export function normalize(entities: Entity[], type: EntityType): NormalizeResult
})
break
case EntityType.Installation:
keys = entities.map(entity => {
const installation = entity as Installation
return set(type, newStore, {
...installation,
app: set(EntityType.App, newStore, installation.app),
})
})
}
return {
@ -122,12 +125,6 @@ export function denormalize(keys: string[], type: EntityType, store: EntityStore
return {
...user,
group: get(EntityType.Group, store, user.group),
installations: user.installations.map(installation => {
return {
...installation,
app: get(EntityType.App, store, installation.app),
}
})
}
case EntityType.Group:
return get(type, store, key)
@ -155,6 +152,14 @@ export function denormalize(keys: string[], type: EntityType, store: EntityStore
...app,
user: get(EntityType.User, store, app.user),
}
case EntityType.Installation:
const installation = get(type, store, key)
if (!installation) return
return {
...installation,
app: get(EntityType.App, store, installation.app),
}
}
})

Loading…
Cancel
Save