Complete history channel

This commit is contained in:
Paulo Gustavo Veiga 2021-02-05 13:15:36 -08:00
parent 87a12c25b1
commit 85d44786c7
16 changed files with 317 additions and 38 deletions

View File

@ -23,15 +23,18 @@
"action.history": {
"defaultMessage": "History"
},
"action.history-description": {
"defaultMessage": "List of changes introduced in the last 90 days."
},
"action.history-title": {
"defaultMessage": "Version history"
},
"action.import": {
"defaultMessage": "Import"
},
"action.info": {
"defaultMessage": "Info"
},
"action.info-title": {
"defaultMessage": "Info"
},
"action.label": {
"defaultMessage": "Add Label"
},
@ -50,6 +53,12 @@
"action.rename": {
"defaultMessage": "Rename"
},
"action.rename-description-placeholder": {
"defaultMessage": "Description"
},
"action.rename-name-placeholder": {
"defaultMessage": "Name"
},
"action.share": {
"defaultMessage": "Share"
},
@ -68,6 +77,12 @@
"duplicate.title": {
"defaultMessage": "Duplicate"
},
"expired.description": {
"defaultMessage": "Your current session has expired. Please, sign in and try again."
},
"expired.title": {
"defaultMessage": "Your session has expired"
},
"footer.aboutus": {
"defaultMessage": "About Us"
},
@ -116,6 +131,9 @@
"login.desc": {
"defaultMessage": "Log into your account"
},
"login.email": {
"defaultMessage": "Email"
},
"login.error": {
"defaultMessage": "The email address or password you entered is not valid."
},
@ -126,6 +144,9 @@
"defaultMessage": "Although HSQLDB is bundled with WiseMapping by default during the installation, we do not recommend this database for production use. Please consider using MySQL 5.7 instead. You can find more information how to configure MySQL",
"description": "Missing production database configured"
},
"login.password": {
"defaultMessage": "Password"
},
"login.remberme": {
"defaultMessage": "Remember me"
},
@ -141,6 +162,33 @@
"login.userinactive": {
"defaultMessage": "Sorry, your account has not been activated yet. You'll receive a notification email when it becomes active. Stay tuned!."
},
"map.creator": {
"defaultMessage": "Creator"
},
"map.last-update": {
"defaultMessage": "Last Update"
},
"map.more-actions": {
"defaultMessage": "More Actions"
},
"map.name": {
"defaultMessage": "Name"
},
"maps.empty-result": {
"defaultMessage": "No matching record found with the current filter criteria."
},
"maps.modified": {
"defaultMessage": "Modified"
},
"maps.modified-by": {
"defaultMessage": "Modified By"
},
"maps.revert": {
"defaultMessage": "Revert"
},
"maps.view": {
"defaultMessage": "View"
},
"menu.account": {
"defaultMessage": "Account"
},

View File

@ -25,7 +25,7 @@ export type MapInfo = {
role: 'owner' | 'editor' | 'viewer'
}
export type HistoryChange = {
export type ChangeHistory = {
id: number;
creator: string;
modified: string;
@ -62,6 +62,9 @@ interface Client {
registerNewUser(user: NewUser): Promise<void>;
resetPassword(email: string): Promise<void>;
fetchHistory(id:number):Promise<ChangeHistory[]>;
revertHistory(id:number,cid:number): Promise<void>
}

View File

@ -1,4 +1,4 @@
import Client, { BasicMapInfo, Label, MapInfo, NewUser } from '..';
import Client, { BasicMapInfo, ChangeHistory, Label, MapInfo, NewUser } from '..';
class MockClient implements Client {
private maps: MapInfo[] = [];
@ -41,6 +41,9 @@ class MockClient implements Client {
];
}
revertHistory(id: number, cid: number): Promise<void> {
return Promise.resolve();
}
createMap(map: BasicMapInfo): Promise<number> {
throw new Error("Method not implemented.");
@ -91,6 +94,46 @@ class MockClient implements Client {
})
};
}
fetchHistory(id: number): Promise<ChangeHistory[]> {
const result = [{
id: 1,
creator: 'Paulo',
modified: '2008-06-02T00:00:00Z'
},
{
id: 2,
creator: 'Paulo',
modified: '2008-06-02T00:00:00Z'
}
,
{
id: 3,
creator: 'Paulo',
modified: '2008-06-02T00:00:00Z'
},
{
id: 4,
creator: 'Paulo',
modified: '2008-06-02T00:00:00Z'
},
{
id: 5,
creator: 'Paulo',
modified: '2008-06-02T00:00:00Z'
},
{
id: 6,
creator: 'Paulo',
modified: '2008-06-02T00:00:00Z'
},
{
id: 7,
creator: 'Paulo',
modified: '2008-06-02T00:00:00Z'
}
]
return Promise.resolve(result);
}
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<number> {

View File

@ -1,6 +1,6 @@
import axios from 'axios';
import { useIntl } from 'react-intl';
import Client, { ErrorInfo, MapInfo, BasicMapInfo, NewUser, Label } from '..';
import Client, { ErrorInfo, MapInfo, BasicMapInfo, NewUser, Label, ChangeHistory } from '..';
export default class RestClient implements Client {
private baseUrl: string;
@ -11,6 +11,14 @@ export default class RestClient implements Client {
this.baseUrl = baseUrl;
this.sessionExpired = sessionExpired;
}
revertHistory(id: number, cid: number): Promise<void> {
// '/c/restful/maps/${mindmapId}/history'
throw new Error('Method not implemented.');
}
fetchHistory(id: number): Promise<ChangeHistory[]> {
throw new Error('Method not implemented.');
}
fetchMapInfo(id: number): Promise<BasicMapInfo> {
throw new Error('Method not implemented.');
@ -29,7 +37,7 @@ export default class RestClient implements Client {
case 401:
case 302:
this.sessionExpired();
result = { msg: intl.formatMessage({ id: "expired.title", defaultMessage: "Your current session has expired. Please, sign in and try again." })}
result = { msg: intl.formatMessage({ id: "expired.description", defaultMessage: "Your current session has expired. Please, sign in and try again." })}
break;
default:
if (data) {

View File

@ -47,6 +47,18 @@
"value": "History"
}
],
"action.history-description": [
{
"type": 0,
"value": "List of changes introduced in the last 90 days."
}
],
"action.history-title": [
{
"type": 0,
"value": "Version history"
}
],
"action.import": [
{
"type": 0,
@ -59,12 +71,6 @@
"value": "Info"
}
],
"action.info-title": [
{
"type": 0,
"value": "Info"
}
],
"action.label": [
{
"type": 0,
@ -101,6 +107,18 @@
"value": "Rename"
}
],
"action.rename-description-placeholder": [
{
"type": 0,
"value": "Description"
}
],
"action.rename-name-placeholder": [
{
"type": 0,
"value": "Name"
}
],
"action.share": [
{
"type": 0,
@ -137,6 +155,18 @@
"value": "Duplicate"
}
],
"expired.description": [
{
"type": 0,
"value": "Your current session has expired. Please, sign in and try again."
}
],
"expired.title": [
{
"type": 0,
"value": "Your session has expired"
}
],
"footer.aboutus": [
{
"type": 0,
@ -233,6 +263,12 @@
"value": "Log into your account"
}
],
"login.email": [
{
"type": 0,
"value": "Email"
}
],
"login.error": [
{
"type": 0,
@ -251,6 +287,12 @@
"value": "Although HSQLDB is bundled with WiseMapping by default during the installation, we do not recommend this database for production use. Please consider using MySQL 5.7 instead. You can find more information how to configure MySQL"
}
],
"login.password": [
{
"type": 0,
"value": "Password"
}
],
"login.remberme": [
{
"type": 0,
@ -281,6 +323,60 @@
"value": "Sorry, your account has not been activated yet. You'll receive a notification email when it becomes active. Stay tuned!."
}
],
"map.creator": [
{
"type": 0,
"value": "Creator"
}
],
"map.last-update": [
{
"type": 0,
"value": "Last Update"
}
],
"map.more-actions": [
{
"type": 0,
"value": "More Actions"
}
],
"map.name": [
{
"type": 0,
"value": "Name"
}
],
"maps.empty-result": [
{
"type": 0,
"value": "No matching record found with the current filter criteria."
}
],
"maps.modified": [
{
"type": 0,
"value": "Modified"
}
],
"maps.modified-by": [
{
"type": 0,
"value": "Modified By"
}
],
"maps.revert": [
{
"type": 0,
"value": "Revert"
}
],
"maps.view": [
{
"type": 0,
"value": "View"
}
],
"menu.account": [
{
"type": 0,

View File

@ -49,7 +49,7 @@ const ForgotPassword = () => {
<GlobalError error={error} />
<form onSubmit={handleOnSubmit}>
<Input type="email" name="email" label={{ id: "forgot.email", defaultMessage: "Email" }}
<Input type="email" name="email" label={intl.formatMessage({ id: "forgot.email", defaultMessage: "Email" })}
autoComplete="email" onChange={e => setEmail(e.target.value)} error={error}/>
<SubmitButton value={intl.formatMessage({ id: "forgot.register", defaultMessage: "Send recovery link" })} />

View File

@ -7,7 +7,7 @@ type InputProps = {
name: string;
error?: ErrorInfo;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
label: MessageDescriptor;
label: string;
required?: boolean;
type: string;
value?: string
@ -27,7 +27,7 @@ const Input = (props: InputProps) => {
const fullWidth = props.fullWidth != undefined ? props.required : true;
return (
<TextField name={name} type={props.type} label={intl.formatMessage(props.label)}
<TextField name={name} type={props.type} label={props.label}
value={value} onChange={onChange}
error={Boolean(fieldError)} helperText={fieldError}
variant="outlined" required={required} fullWidth={fullWidth} margin="dense"/>

View File

@ -67,8 +67,8 @@ const LoginPage = () => {
<FormControl>
<form action="/c/perform-login" method="POST" >
<Input name="username" type="email" label={{ id: "login.email", defaultMessage: "Email" }} required autoComplete="email" />
<Input name="password" type="password" label={{ id: "login.password", defaultMessage: "Password" }} required autoComplete="current-password" />
<Input name="username" type="email" label={intl.formatMessage({ id: "login.email", defaultMessage: "Email" })} required autoComplete="email" />
<Input name="password" type="password" label={intl.formatMessage({ id: "login.password", defaultMessage: "Password" })} required autoComplete="current-password" />
<div>
<input name="_spring_security_login.remberme" id="staySignIn" type="checkbox" />
<label htmlFor="staySignIn"><FormattedMessage id="login.remberme" defaultMessage="Remember me" /></label>

View File

@ -68,10 +68,10 @@ const CreateDialog = (props: CreateProps) => {
submitButton={intl.formatMessage({ id: 'create.button', defaultMessage: 'Create' })}>
<FormControl fullWidth={true}>
<Input name="title" type="text" label={{ id: "action.rename-name-placeholder", defaultMessage: "Name" }}
<Input name="title" type="text" label={intl.formatMessage({ id: "action.rename-name-placeholder", defaultMessage: "Name" })}
value={model.title} onChange={handleOnChange} error={error} fullWidth={true} />
<Input name="description" type="text" label={{ id: "action.rename-description-placeholder", defaultMessage: "Description" }}
<Input name="description" type="text" label={intl.formatMessage({ id: "action.rename-description-placeholder", defaultMessage: "Description" })}
value={model.description} onChange={handleOnChange} required={false} fullWidth={true} />
</FormControl>
</BaseDialog>

View File

@ -77,10 +77,10 @@ const DuplicateDialog = (props: DialogProps) => {
submitButton={intl.formatMessage({ id: 'duplicate.title', defaultMessage: 'Duplicate' })}>
<FormControl fullWidth={true}>
<Input name="title" type="text" label={{ id: "action.rename-name-placeholder", defaultMessage: "Name" }}
<Input name="title" type="text" label={intl.formatMessage({ id: "action.rename-name-placeholder", defaultMessage: "Name" })}
value={model.title} onChange={handleOnChange} error={error} fullWidth={true} />
<Input name="description" type="text" label={{ id: "action.rename-description-placeholder", defaultMessage: "Description" }}
<Input name="description" type="text" label={intl.formatMessage({ id: "action.rename-description-placeholder", defaultMessage: "Description" })}
value={model.description} onChange={handleOnChange} required={false} fullWidth={true} />
</FormControl>
</BaseDialog>

View File

@ -0,0 +1,75 @@
import React, { ErrorInfo } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useSelector } from "react-redux";
import Client, { ChangeHistory } from "../../../../client";
import { activeInstance } from '../../../../redux/clientSlice';
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "..";
import BaseDialog from "../base-dialog";
import { Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip } from "@material-ui/core";
import moment from "moment";
const HistoryDialog = (props: DialogProps) => {
const intl = useIntl();
const mapId = props.mapId;
const client: Client = useSelector(activeInstance);
const { isLoading, error, data } = useQuery<unknown, ErrorInfo, ChangeHistory[]>('history', () => {
return client.fetchHistory(mapId);
});
const changeHistory: ChangeHistory[] = data ? data : [];
const handleOnClose = (): void => {
props.onClose();
};
const handleOnClick = (event,vid): void => {
event.preventDefault();
client.revertHistory(mapId,vid)
.then((mapId)=>{
handleOnClose();
})
};
return (
<div>
<BaseDialog
open={props.open} onClose={handleOnClose}
title={intl.formatMessage({ id: "action.history-title", defaultMessage: "Version history" })}
description={intl.formatMessage({ id: "action.history-description", defaultMessage: "List of changes introduced in the last 90 days." })} >
<TableContainer component={Paper} style={{ maxHeight: '200px' }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell align="left"><FormattedMessage id="maps.modified-by" defaultMessage="Modified By" /></TableCell>
<TableCell align="left"><FormattedMessage id="maps.modified" defaultMessage="Modified" /></TableCell>
<TableCell align="left"></TableCell>
<TableCell align="left"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{changeHistory.map((row) => (
<TableRow key={row.id}>
<TableCell align="left">{row.creator}</TableCell>
<TableCell align="left">
<Tooltip title={moment(row.modified).format("lll")} placement="bottom-start">
<span>{moment(row.modified).fromNow()}</span>
</Tooltip>
</TableCell>
<TableCell align="left"><Link href={`c/maps/${mapId}/${row.id}/view`} target="history"><FormattedMessage id="maps.view" defaultMessage="View" /></Link></TableCell>
<TableCell align="left"><Link href="#" onClick={(e)=>handleOnClick(e,row.id)}><FormattedMessage id="maps.revert" defaultMessage="Revert" /></Link></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</BaseDialog>
</div>
);
}
export default HistoryDialog;

View File

@ -1,5 +1,5 @@
import React from 'react';
import RenameDialog from './rename';
import RenameDialog from './rename-dialog';
import DeleteDialog from './delete-dialog';
import { ActionType } from '../action-chooser';
import { ErrorInfo, MapInfo } from '../../../client';
@ -10,6 +10,7 @@ import { activeInstance } from '../../../redux/clientSlice';
import DuplicateDialog from './duplicate-dialog';
import { useHistory } from 'react-router-dom';
import CreateDialog from './create-dialog';
import HistoryDialog from './history-dialog';
export type BasicMapInfo = {
name: string;
@ -46,6 +47,7 @@ const ActionDispatcher = (props: ActionDialogProps) => {
<DeleteDialog open={action === 'delete'} onClose={handleOnClose} mapId={mapId} />
<RenameDialog open={action === 'rename'} onClose={handleOnClose} mapId={mapId} />
<DuplicateDialog open={action === 'duplicate'} onClose={handleOnClose} mapId={mapId} />
<HistoryDialog open={action === 'history'} onClose={handleOnClose} mapId={mapId} />
</span >
);
}

View File

@ -76,10 +76,10 @@ const RenameDialog = (props: DialogProps) => {
submitButton={intl.formatMessage({ id: 'rename.title', defaultMessage: 'Rename' })}>
<FormControl fullWidth={true}>
<Input name="title" type="text" label={{ id: "action.rename-name-placeholder", defaultMessage: "Name" }}
<Input name="title" type="text" label={intl.formatMessage({ id: "action.rename-name-placeholder", defaultMessage: "Name" })}
value={model.title} onChange={handleOnChange} error={error} fullWidth={true} />
<Input name="description" type="text" label={{ id: "action.rename-description-placeholder", defaultMessage: "Description" }}
<Input name="description" type="text" label={intl.formatMessage({ id: "action.rename-description-placeholder", defaultMessage: "Description" })}
value={model.description} onChange={handleOnChange} required={false} fullWidth={true} />
</FormControl>
</BaseDialog>

View File

@ -259,7 +259,7 @@ const HandleClientStatus = () => {
<DialogContent>
<Alert severity="error">
<AlertTitle><FormattedMessage id="expired.title" defaultMessage="Your current session has expired. Please, sign in and try again." /></AlertTitle>
<AlertTitle><FormattedMessage id="expired.description" defaultMessage="Your current session has expired. Please, sign in and try again." /></AlertTitle>
</Alert>
</DialogContent>

View File

@ -28,7 +28,7 @@ import { Button, InputBase, Link } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import moment from 'moment'
import { Filter, LabelFilter } from '..';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { DeleteOutlined, LabelTwoTone } from '@material-ui/icons';
import Alert from '@material-ui/lab/Alert';
@ -71,12 +71,6 @@ interface HeadCell {
style?: CSSProperties;
}
const headCells: HeadCell[] = [
{ id: 'title', numeric: false, label: 'Name' },
{ id: 'labels', numeric: false },
{ id: 'creator', numeric: false, label: 'Creator', style: { width: '60px' } },
{ id: 'modified', numeric: true, label: 'Last Update', style: { width: '30px' } }
];
interface EnhancedTableProps {
classes: ReturnType<typeof useStyles>;
@ -89,12 +83,21 @@ interface EnhancedTableProps {
}
function EnhancedTableHead(props: EnhancedTableProps) {
const intl = useIntl();
const { classes, onSelectAllClick, order, orderBy, numSelected, rowCount, onRequestSort } = props;
const createSortHandler = (property: keyof MapInfo) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};
const headCells: HeadCell[] = [
{ id: 'title', numeric: false, label: intl.formatMessage({ id: 'map.name', defaultMessage: 'Name' }) },
{ id: 'labels', numeric: false },
{ id: 'creator', numeric: false, label: intl.formatMessage({ id: 'map.creator', defaultMessage: 'Creator' }), style: { width: '70px', whiteSpace: 'nowrap' } },
{ id: 'modified', numeric: true, label: intl.formatMessage({ id: 'map.last-update', defaultMessage: 'Last Update' }), style: { width: '70px', whiteSpace: 'nowrap' } }
];
return (
<TableHead>
<TableRow>
@ -196,6 +199,7 @@ export const MapsList = (props: MapsListProps) => {
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const client: Client = useSelector(activeInstance);
const intl = useIntl();
useEffect(() => {
setSelected([]);
@ -456,7 +460,7 @@ export const MapsList = (props: MapsListProps) => {
</TableCell>
<TableCell className={classes.bodyCell}>
<Tooltip title="Others">
<Tooltip title={intl.formatMessage({ id: 'map.more-actions', defaultMessage: 'More Actions' })}>
<IconButton aria-label="Others" size="small" onClick={handleActionClick(row.id)}>
<MoreHorizIcon color="action" />
</IconButton>

View File

@ -71,16 +71,16 @@ const RegistrationForm = () => {
<form onSubmit={handleOnSubmit}>
<GlobalError error={error} />
<Input name="email" type="email" onChange={handleOnChange} label={{ id: "registration.email", defaultMessage: "Email" }}
<Input name="email" type="email" onChange={handleOnChange} label={intl.formatMessage({ id: "registration.email", defaultMessage: "Email" })}
autoComplete="email" error={error}/>
<Input name="firstname" type="text" onChange={handleOnChange} label={{ id: "registration.firstname", defaultMessage: "First Name" }}
<Input name="firstname" type="text" onChange={handleOnChange} label={intl.formatMessage({ id: "registration.firstname", defaultMessage: "First Name" })}
autoComplete="given-name" error={error}/>
<Input name="lastname" type="text" onChange={handleOnChange} label={{ id: "registration.lastname", defaultMessage: "Last Name" }}
<Input name="lastname" type="text" onChange={handleOnChange} label={intl.formatMessage({ id: "registration.lastname", defaultMessage: "Last Name" })}
autoComplete="family-name" error={error}/>
<Input name="password" type="password" onChange={handleOnChange} label={{ id: "registration.password", defaultMessage: "Password" }}
<Input name="password" type="password" onChange={handleOnChange} label={intl.formatMessage({ id: "registration.password", defaultMessage: "Password" })}
autoComplete="new-password" error={error}/>
<div style={{ width: '330px', padding: '5px 0px 5px 20px' }}>