Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update notes components #419

Merged
merged 17 commits into from
Jul 3, 2018
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
567cfeb
Revert "jb - Update BoundForm to allow update of object through new p…
joshuasbates May 25, 2018
2921fea
Merge branch 'master' of https://github.com/appfolio/react-gears
joshuasbates Jun 8, 2018
eaf5781
Merge branch 'master' of https://github.com/appfolio/react-gears
joshuasbates Jun 15, 2018
cbdfeb5
jb - Make Notes help bubble text more customizable
joshuasbates May 25, 2018
15c8c25
jb - Render Notes as a block panel; add missing tests
joshuasbates May 25, 2018
1ee453d
jb - EditableNote: update callbacks to specify the note; propagate cl…
joshuasbates May 29, 2018
26e35d5
jb - EditableNote: add input feedback support for note errors
joshuasbates May 29, 2018
f39c175
jb - EditableNote: add disabled state while note is being saved
joshuasbates May 29, 2018
408e41a
jb - Break out Note header into a separate component to be shared wit…
joshuasbates May 30, 2018
bd104a6
jb - EditableNote: support children for ease-of-reuse for custom notes
joshuasbates May 30, 2018
4e81572
jb - Update documnetation for Notes and add for EditableNote
joshuasbates Jun 15, 2018
164f906
gt - Remove Notes header and controls
gthomas-appfolio Jun 29, 2018
24f7955
gt - Update tests
gthomas-appfolio Jun 29, 2018
42fa96f
gt - Add props per feedback
gthomas-appfolio Jun 29, 2018
bd26e7d
gt - Update stories
gthomas-appfolio Jun 29, 2018
66e68c4
jb - Minor cleanup doc cleanup; minor visual tweaks
joshuasbates Jul 2, 2018
c496e8c
jb - DeletedNote: Use Button instead of <a> for undo link (consistenc…
joshuasbates Jul 3, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/DeletedNote.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import Alert from './Alert';

class DeletedNote extends React.Component {
static propTypes = {
className: PropTypes.string,
note: PropTypes.object.isRequired,
onUndelete: PropTypes.func
};

render() {
const { note, onUndelete } = this.props;
const { className, note, onUndelete } = this.props;

return (
<Alert color="success" icon>
<Alert color="success" icon className={className}>
Note deleted.
{onUndelete ? <a ref="undo" href="#" className="ml-1" onClick={() => onUndelete(note)}>undo</a> : null}
</Alert>
Expand Down
54 changes: 40 additions & 14 deletions src/components/EditableNote.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,58 @@ import Button from './Button';
import ButtonToolbar from './ButtonToolbar';
import Card from './Card';
import CardBlock from './CardBlock';
import FormLabelGroup from './FormLabelGroup';
import Input from './Input';
import NoteHeader from './NoteHeader';

class EditableNote extends React.Component {
static propTypes = {
note: PropTypes.object.isRequired,
children: PropTypes.node,
className: PropTypes.string,
note: PropTypes.shape({
date: PropTypes.object,
errors: PropTypes.string,
text: PropTypes.string
}),
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired
onSave: PropTypes.func.isRequired,
rows: PropTypes.number,
saving: PropTypes.bool
};

static defaultProps = {
className: 'bg-white mb-3',
rows: 4,
saving: false
};

render() {
const { note, onCancel, onChange, onSave } = this.props;
const { children, className, note, onCancel, onChange, onSave, rows, saving } = this.props;
const { date, errors, text } = note;

return (
<Card>
<Card className={className}>
{date && <NoteHeader note={note} />}
<CardBlock>
<Input
ref="text"
type="textarea"
value={note.text}
rows="5"
onChange={onChange}
/>
<ButtonToolbar className="mt-3">
<Button ref="save" color="primary" onClick={onSave}>Save</Button>
<Button ref="cancel" onClick={onCancel}>Cancel</Button>
<FormLabelGroup feedback={errors} stacked>
<Input
autoFocus
disabled={saving}
ref="text"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this ref for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were left over from the original impl. The unit test uses these refs to find elements. We should get away from that, IMHO, but I didn't go that far.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, just curious

rows={rows}
state={errors && 'danger'}
type="textarea"
value={text}
onChange={event => onChange(event, note)}
/>
</FormLabelGroup>
{children}
<ButtonToolbar className="mt-3 mb-0">
<Button ref="save" color="primary" disabled={saving} onClick={() => onSave(note)}>
{saving ? 'Saving...' : 'Save'}
</Button>
<Button ref="cancel" disabled={saving} onClick={() => onCancel(note)}>Cancel</Button>
</ButtonToolbar>
</CardBlock>
</Card>
Expand Down
92 changes: 42 additions & 50 deletions src/components/Note.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,72 @@
import fecha from 'fecha';
import PropTypes from 'prop-types';
import React from 'react';

import Badge from './Badge';
import Button from './Button';
import Card from './Card';
import CardBlock from './CardBlock';
import CardHeader from './CardHeader';
import CardText from './CardText';
import NoteHeader from './NoteHeader.js';
import DeletedNote from './DeletedNote.js';
import EditableNote from './EditableNote.js';

// TODO extract to date helper, i18n:
const dateFormat = (date, format) => fecha.format(date, format);

class Note extends React.Component {
static displayName = 'Note';

static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
note: PropTypes.object,
note: PropTypes.shape({
deleted: PropTypes.bool,
editing: PropTypes.bool,
text: PropTypes.string.isRequired
}).isRequired,
onCancel: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
onEdit: PropTypes.func,
onSave: PropTypes.func,
onUndelete: PropTypes.func
onUndelete: PropTypes.func,
rows: PropTypes.number,
saving: PropTypes.boolean
};

static defaultProps = {
className: ''
className: 'bg-white mb-3',
rows: EditableNote.defaultProps.rows,
saving: EditableNote.defaultProps.saving
};

render() {
const { children, className, note, onCancel, onChange, onSave, onDelete, onEdit, onUndelete } = this.props;
const { date, deleted, edited, editing, from, text } = note;
const { children, className, note, onCancel, onChange, onDelete, onEdit, onSave, onUndelete, rows, saving }
= this.props;
const { deleted, editing, text } = note;

if (deleted) {
return (
<DeletedNote
className={className}
note={note}
onUndelete={onUndelete}
/>);
} else if (editing) {
return (
<EditableNote
className={className}
note={note}
onCancel={onCancel}
onChange={onChange}
onSave={onSave}
rows={rows}
saving={saving}
/>);
}
return (
<div className={`mb-3 ${className}`}>
{deleted ?
<DeletedNote
ref="deleted"
note={note}
onUndelete={onUndelete}
/> :
editing ?
<EditableNote
ref="editable"
note={note}
onCancel={onCancel}
onChange={onChange}
onSave={onSave}
/> :
<Card color="info" outline>
<CardHeader className="d-flex justify-content-start p-2 bg-info">
{edited ? <span ref="edited"><Badge color="primary text-uppercase mr-2">Edited</Badge></span> : null}
<span className="text-muted">
<span className="hidden-xs-down">
{edited ? 'Last edited' : 'Posted'} {from ? <span ref="from">by {from}</span> : ' '} on <span ref="date">{dateFormat(date, 'ddd, MMMM D, YYYY "at" h:mm A')}</span>
</span>
<span className="hidden-sm-up">
{from ? <span>{from} </span> : null}<span ref="shortDate">{dateFormat(date, 'M/D/YY h:mm A')}</span>
</span>
</span>
<span className="ml-auto">
{onEdit ? <Button color="link" ref="edit" onClick={() => onEdit(note)} className="mr-3 p-0">edit</Button> : null}
{onDelete ? <Button color="link" ref="delete" onClick={() => onDelete(note)} className="p-0">delete</Button> : null}
</span>
</CardHeader>
<CardBlock>
<CardText style={{ whiteSpace: 'pre-wrap' }}>{text}</CardText>
{children}
</CardBlock>
</Card>
}
</div>
<Card color="info" className={className} outline>
<NoteHeader note={note} onDelete={onDelete} onEdit={onEdit} />
<CardBlock>
<CardText style={{ whiteSpace: 'pre-wrap' }}>{text}</CardText>
{children}
</CardBlock>
</Card>
);
}
}
Expand Down
65 changes: 65 additions & 0 deletions src/components/NoteHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import fecha from 'fecha';
import PropTypes from 'prop-types';
import React from 'react';
import classnames from 'classnames';
import Badge from './Badge';
import Button from './Button';
import CardHeader from './CardHeader';

// TODO extract to date helper, i18n:
const dateFormat = (date, format) => fecha.format(date, format);

class NoteHeader extends React.Component {
static displayName = 'NoteHeader';

static propTypes = {
note: PropTypes.shape({
date: PropTypes.object.isRequired,
edited: PropTypes.bool,
from: PropTypes.string,
}).isRequired,
onDelete: PropTypes.func,
onEdit: PropTypes.func,
};

render() {
const { note, onDelete, onEdit } = this.props;
const { date, edited, from } = note;

const headerClassNames = classnames(
'd-flex',
'flex-wrap',
'align-items-center',
'justify-content-between',
'py-2',
'pr-2',
'bg-info'
);

return (
<CardHeader className={headerClassNames}>
<div
className="d-inline-flex align-items-center"
ref="title"
>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text-muted property seems to have been dropped. It appears to be a part of the Saffron standard.

In use: https://qa.qa.appfolio.com/maintenance/mobile?assignedUserId=any#/133920/notes

{edited && <Badge color="primary" className="text-uppercase mr-2 js-note-header__edited">Edited</Badge>}
<span className="m-0 my-1 mr-auto">
<span className="hidden-xs-down">
{edited ? 'Last edited' : 'Posted'}
{from ? <span className="js-note-header__from">{` by ${from}`}</span> : ' '} on <span className="js-note-header__date">{dateFormat(date, 'ddd, MMMM D, YYYY "at" h:mm A')}</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit odd to me that there are different date formats for Edited and non-edited notes, but that's the way it is in APM. Perhaps it makes sense to have these customizable. Not a big deal though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, the date format is the same, only the labeling of that date changes:

"Posted by Gary Thomas on Mon, July 2, 2018 at 2:33 PM"
vs.
"Last edited by Gary Thomas on Mon, July 2, 2018 at 2:33 PM"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant compared to short dateformat below on line 52: dateFormat(date, 'M/D/YY h:mm A') but I see now that's for small viewport widths.

</span>
<span className="hidden-sm-up">
{from ? <span>{from} </span> : null}<span className="js-note-header__shortDate">{dateFormat(date, 'M/D/YY h:mm A')}</span>
</span>
</span>
</div>
<div className="d-inline-flex">
{onEdit ? <Button color="link" onClick={() => onEdit(note)} className="js-note-header__edit mr-3 p-0">edit</Button> : null}
{onDelete ? <Button color="link" onClick={() => onDelete(note)} className="js-note-header__delete p-0">delete</Button> : null}
</div>
</CardHeader>
);
}
}

export default NoteHeader;
52 changes: 10 additions & 42 deletions src/components/Notes.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import PropTypes from 'prop-types';
import React from 'react';

import Button from './Button';
import Col from './Col';
import HelpBubble from './HelpBubble';
import Icon from './Icon';
import Note from './Note';
import Row from './Row';

export default class Notes extends React.Component {

static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
onAdd: PropTypes.func,
onCancel: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
onDownload: PropTypes.func,
onEdit: PropTypes.func,
onSave: PropTypes.func,
onUndelete: PropTypes.func,
notes: PropTypes.array,
notes: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired
}))
}

static defaultProps = {
Expand All @@ -29,52 +24,25 @@ export default class Notes extends React.Component {
}

render() {
const { children, className, notes, onAdd, onDownload, onCancel, onChange, onDelete, onEdit, onSave, onUndelete } = this.props;
const { children, className, notes, onCancel, onChange,
onDelete, onEdit, onSave, onUndelete } = this.props;

return (
<div className={className}>
<Row className="mb-3">
<Col>
<h3>
Notes
<HelpBubble
title="Notes"
placement="right"
className="text-primary ml-1"
>
Make notes on several different pages. Notes are private to property managers.
The 'Download PDF' button will download all the notes on a page as a PDF to your computer.
</HelpBubble>
</h3>
</Col>
<Col className="text-right">
{onDownload ?
<Button size="sm" className="mr-1" onClick={onDownload}>
<Icon name="download" fixedWidth />
<span className="hidden-xs-down">Download PDF</span>
</Button>
: null}
{onAdd ?
<Button size="sm" onClick={onAdd}>
<Icon name="plus-circle" fixedWidth />
<span className="hidden-xs-down">Add Note</span>
</Button>
: null}
</Col>
</Row>
{children ||
notes.map((note, i) =>
notes.map(note => (
<Note
key={note.id ? `js-note-${note.id}` : null}
note={note}
key={i}
onCancel={onCancel}
onChange={onChange}
onDelete={onDelete}
onEdit={onEdit}
onSave={onSave}
onUndelete={onUndelete}
saving={note.editing && note.saving}
/>
)
))
}
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import Modal from './components/Modal.js';
import MonthCalendar from './components/MonthCalendar.js';
import MonthInput from './components/MonthInput.js';
import Note from './components/Note.js';
import NoteHeader from './components/NoteHeader.js';
import Notes from './components/Notes.js';
import Paginator from './components/Paginator';
import PatternInput from './components/PatternInput.js';
Expand Down Expand Up @@ -230,6 +231,7 @@ export {
MonthCalendar,
MonthInput,
Note,
NoteHeader,
Notes,
PatternInput,
Progress,
Expand Down
Loading