diff --git a/src/actions/directory.ts b/src/actions/directory.ts index 08bb694..5124cd7 100644 --- a/src/actions/directory.ts +++ b/src/actions/directory.ts @@ -4,7 +4,7 @@ import { normalize } from 'normalizr' import { apiFetch } from 'src/api' import { setEntity, setEntities } from 'src/actions/entities' import { startRequest, finishRequest } from 'src/actions/requests' -import { groupSchema, userSchema } from 'src/store/schemas' +import { groupSchema, userSchema, logSchema } from 'src/store/schemas' import { objectToQuerystring } from 'src/utils' import { AppThunkAction, Entity, RequestKey, EntityType, User } from 'src/types' @@ -110,3 +110,51 @@ export const fetchGroupMembers = (id: string, type?: string, continuation?: stri throw err } } + +interface GroupLogsResponse { + logs: Entity[] + continuation?: string +} + +export const fetchLogs = (id: string, continuation?: string): AppThunkAction => async dispatch => { + dispatch(startRequest(RequestKey.FetchGroupLogs)) + + try { + const response = await apiFetch({ + path: `/api/group/${id}/logs?${objectToQuerystring({ continuation })}`, + }) + + const users = normalize(response.logs, [logSchema]) + + dispatch(setEntities(users.entities)) + dispatch(finishRequest(RequestKey.FetchGroupLogs, true)) + } catch (err) { + dispatch(finishRequest(RequestKey.FetchGroupLogs, false)) + throw err + } +} + +interface CreateInvitationResponse { + code: string +} + +export const createInvitation = (id: string, expiration?: number, limit?: number): AppThunkAction => async dispatch => { + dispatch(startRequest(RequestKey.CreateInvitation)) + + try { + const response = await apiFetch({ + path: `/api/group/${id}/invitation`, + method: 'post', + body: { + expiration, + limit, + } + }) + + dispatch(finishRequest(RequestKey.CreateInvitation, true)) + return response.code + } catch (err) { + dispatch(finishRequest(RequestKey.CreateInvitation, false)) + throw err + } +} diff --git a/src/api/errors.ts b/src/api/errors.ts index f8495de..df2ce54 100644 --- a/src/api/errors.ts +++ b/src/api/errors.ts @@ -4,7 +4,7 @@ import { setFieldNotification } from 'src/actions/forms' import { showNotification } from 'src/actions/notifications' import { AppThunkDispatch, FormNotification, NotificationType } from 'src/types' -export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, history: History) { +export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, history?: History) { if (err instanceof ServerError) { dispatch(showNotification(NotificationType.Error, 'Server Error')) } @@ -20,7 +20,7 @@ export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, histo if (err instanceof UnauthorizedError) { dispatch(showNotification(NotificationType.Error, 'You need to be logged in.')) - history.push('/login') + if (history) history.push('/login') } if (err instanceof NotFoundError) { diff --git a/src/components/group-logs/group-logs.tsx b/src/components/group-logs/group-logs.tsx new file mode 100644 index 0000000..c33b7f1 --- /dev/null +++ b/src/components/group-logs/group-logs.tsx @@ -0,0 +1,39 @@ +import React, { FC, useEffect } from 'react' +import noop from 'lodash/noop' +import moment from 'moment' +import { GroupLog } from 'src/types' + +export interface Props { + group: string + logs?: GroupLog[] + fetchLogs?: () => void +} + +const MemberList: FC = ({ group, logs = [], fetchLogs = noop }) => { + useEffect(() => { + if (logs.length === 0) fetchLogs() + }, [group]) + + return ( + + + + + + + + + + {logs.map(log => ( + + + + + + ))} + +
WhoWhatWhen
{log.user.id}{log.content}{moment(log.created).format('MMMM Do YYYY, h:mm:ss a')}
+ ) +} + +export default MemberList diff --git a/src/components/group-logs/index.tsx b/src/components/group-logs/index.tsx new file mode 100644 index 0000000..259c8fb --- /dev/null +++ b/src/components/group-logs/index.tsx @@ -0,0 +1,26 @@ +import { connect } from 'react-redux' +import { handleApiError } from 'src/api/errors' +import { fetchLogs } from 'src/actions/directory' +import { getLogs } from 'src/selectors/directory' +import { AppState, AppThunkDispatch } from 'src/types' + +import GroupLogs, { Props } from './group-logs' + +const mapStateToProps = (state: AppState) => ({ + logs: getLogs(state), +}) + +const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ + fetchLogs: () => { + try { + dispatch(fetchLogs(ownProps.group)) + } catch (err) { + handleApiError(err, dispatch) + } + }, +}) + +export default connect( + mapStateToProps, + mapDispatchToProps +)(GroupLogs) diff --git a/src/components/member-list/index.tsx b/src/components/member-list/index.tsx index e642972..7843819 100644 --- a/src/components/member-list/index.tsx +++ b/src/components/member-list/index.tsx @@ -1,17 +1,26 @@ -import React, { FC } from 'react' +import { connect } from 'react-redux' +import { handleApiError } from 'src/api/errors' +import { fetchGroupMembers } from 'src/actions/directory' +import { getGroupMembers } from 'src/selectors/directory' +import { AppState, AppThunkDispatch } from 'src/types' -import { User } from 'src/types' +import MemberList, { Props } from './member-list' -import MemberListItem from './member-list-item' +const mapStateToProps = (state: AppState, ownProps: Props) => ({ + members: getGroupMembers(state, ownProps.group), +}) -interface Props { - members: User[] -} +const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ + fetchGroupMembers: () => { + try { + dispatch(fetchGroupMembers(ownProps.group)) + } catch (err) { + handleApiError(err, dispatch) + } + }, +}) -const MemberList: FC = ({ members }) => ( -
- {members.map(member => )} -
-) - -export default MemberList +export default connect( + mapStateToProps, + mapDispatchToProps +)(MemberList) diff --git a/src/components/member-list/member-list.tsx b/src/components/member-list/member-list.tsx new file mode 100644 index 0000000..336bc2f --- /dev/null +++ b/src/components/member-list/member-list.tsx @@ -0,0 +1,25 @@ +import React, { FC, useEffect } from 'react' +import noop from 'lodash/noop' +import { User } from 'src/types' + +import MemberListItem from './member-list-item' + +export interface Props { + group: string + members?: User[] + fetchGroupMembers?: () => void +} + +const MemberList: FC = ({ group, members = [], fetchGroupMembers = noop }) => { + useEffect(() => { + fetchGroupMembers() + }, [group]) + + return ( +
+ {members.map(member => )} +
+ ) +} + +export default MemberList diff --git a/src/components/pages/group-admin/group-admin.tsx b/src/components/pages/group-admin/group-admin.tsx index 4fc2ebd..d83c983 100644 --- a/src/components/pages/group-admin/group-admin.tsx +++ b/src/components/pages/group-admin/group-admin.tsx @@ -9,6 +9,8 @@ import { Group, GroupMembershipType, User } from 'src/types' import PageHeader from 'src/components/page-header' import MemberList from 'src/components/member-list' +import GroupLogs from 'src/components/group-logs' +import Loading from 'src/components/pages/loading' interface Tab { id: string @@ -22,11 +24,33 @@ interface Params { export interface Props extends RouteComponentProps { group?: Group - members?: User[] fetchGroup: () => void + createInvitation: (expiration: number, limit: number) => void } -const GroupAdmin: FC = ({ group, members = [], fetchGroup, match, history }) => { +const GroupAdmin: FC = ({ + group, + fetchGroup, + createInvitation, + match, + history, +}) => { + const tab = match.params.tab || '' + const tabs: Tab[] = [ + { + id: '', + label: 'General', + }, + { + id: 'members', + label: 'Members', + }, + { + id: 'logs', + label: 'Logs', + }, + ] + useEffect(() => { fetchGroup() }, []) @@ -42,26 +66,7 @@ const GroupAdmin: FC = ({ group, members = [], fetchGroup, match, history } }, [group]) - if (!group) { - return ( -
- -
-
- ) - } - - const selectedTab = match.params.tab ? match.params.tab : '' - const tabs: Tab[] = [ - { - id: '', - label: 'General', - }, - { - id: 'members', - label: 'Members', - } - ] + if (!group) return return (
@@ -72,7 +77,7 @@ const GroupAdmin: FC = ({ group, members = [], fetchGroup, match, history
    {tabs.map(t => ( -
  • +
  • {t.label} @@ -82,7 +87,7 @@ const GroupAdmin: FC = ({ group, members = [], fetchGroup, match, history
- {selectedTab === '' && + {tab === '' &&
@@ -117,9 +122,8 @@ const GroupAdmin: FC = ({ group, members = [], fetchGroup, match, history
} - {match.params.tab === 'members' && - - } + {tab === 'members' && } + {tab === 'logs' && }
diff --git a/src/components/pages/group-admin/index.ts b/src/components/pages/group-admin/index.ts index 27d2aa8..d5e6991 100644 --- a/src/components/pages/group-admin/index.ts +++ b/src/components/pages/group-admin/index.ts @@ -1,7 +1,6 @@ import { connect } from 'react-redux' import { handleApiError } from 'src/api/errors' -import { fetchGroup, fetchGroupMembers } from 'src/actions/directory' -import { getGroupMembers } from 'src/selectors/directory' +import { fetchGroup, createInvitation } from 'src/actions/directory' import { getEntity } from 'src/selectors/entities' import { AppState, EntityType, Group, AppThunkDispatch } from 'src/types' @@ -9,18 +8,23 @@ import GroupAdmin, { Props } from './group-admin' const mapStateToProps = (state: AppState, ownProps: Props) => ({ group: getEntity(state, EntityType.Group, ownProps.match.params.id), - members: getGroupMembers(state, ownProps.match.params.id), }) const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ fetchGroup: () => { try { dispatch(fetchGroup(ownProps.match.params.id)) - dispatch(fetchGroupMembers(ownProps.match.params.id)) } catch (err) { handleApiError(err, dispatch, ownProps.history) } - } + }, + createInvitation: (expiration: number, limit: number) => { + try { + dispatch(createInvitation(ownProps.match.params.id, expiration, limit)) + } catch (err) { + handleApiError(err, dispatch, ownProps.history) + } + }, }) export default connect( diff --git a/src/components/pages/loading/index.tsx b/src/components/pages/loading/index.tsx new file mode 100644 index 0000000..7db7418 --- /dev/null +++ b/src/components/pages/loading/index.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react' +import PageHeader from 'src/components/page-header' + +const Loading: FC = () => ( +
+ +
+
+) + +export default Loading diff --git a/src/selectors/directory.ts b/src/selectors/directory.ts index 7a6da07..a393487 100644 --- a/src/selectors/directory.ts +++ b/src/selectors/directory.ts @@ -2,9 +2,9 @@ import { denormalize } from 'normalizr' import { createSelector } from 'reselect' import filter from 'lodash/filter' -import { groupSchema, userSchema } from '../store/schemas' +import { groupSchema, userSchema, logSchema } from '../store/schemas' import { getEntityStore } from './entities' -import { AppState, Group, User, EntityType } from 'src/types' +import { AppState, Group, User, EntityType, GroupLog } from 'src/types' export const getGroupIds = (state: AppState) => state.directory.groups @@ -17,3 +17,5 @@ export const getGroupMembers = (state: AppState, group: string) => { const users = state.entities[EntityType.User] return denormalize(filter(users, user => user.group === group), [userSchema], state.entities) as User[] } + +export const getLogs = (state: AppState) => denormalize(state.entities[EntityType.Log], [logSchema], state.entities) as GroupLog[] diff --git a/src/store/schemas.ts b/src/store/schemas.ts index 845f522..2fd89a3 100644 --- a/src/store/schemas.ts +++ b/src/store/schemas.ts @@ -1,7 +1,12 @@ import { schema } from 'normalizr' +import { EntityType } from 'src/types' -export const groupSchema = new schema.Entity('groups') +export const groupSchema = new schema.Entity(EntityType.Group) -export const userSchema = new schema.Entity('users', { +export const userSchema = new schema.Entity(EntityType.User, { group: groupSchema, }) + +export const logSchema = new schema.Entity(EntityType.Log, { + user: userSchema, +}) diff --git a/src/types/entities.ts b/src/types/entities.ts index 87f3664..c903129 100644 --- a/src/types/entities.ts +++ b/src/types/entities.ts @@ -1,6 +1,7 @@ export enum EntityType { User = 'users', Group = 'groups', + Log = 'log', } export enum GroupMembershipType { @@ -28,6 +29,12 @@ export type User = Entity & { coverImageUrl?: string } +export type GroupLog = Entity & { + user: User + content: string + created: number +} + export interface EntityCollection { [id: string]: Entity } diff --git a/src/types/store.ts b/src/types/store.ts index 9f8607f..41846e7 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -17,6 +17,8 @@ export enum RequestKey { Register = 'register', Authenticate = 'authenticate', FetchGroupMembers = 'fetch_group_members', + FetchGroupLogs = 'fetch_group_logs', + CreateInvitation = 'create_invitation', } export type FormValue = string | number | boolean