Skip to content

Commit

Permalink
React/calendar react (ChurchCRM#4716)
Browse files Browse the repository at this point in the history
* Add React Enviornment

* Convert Calendar to React

* comments

* don't bother nuking host-side node_modules at provision

* make the webpack build work (and on windows)

* cleanup react code.

* two spaces = tab indentation

* add pinned calendars using  react-select

* tell webpack to use development output mode

* add handler for pinned calendars (not quite saving the state yet though)

* move files

* cleanup react code for event editor

* split editor and viewer to separate files.  link things up.

* cleanup and now populate pinned calendars dropdown

* rename Calendar interface; loading calendars over AJAX for dropdown list

* updates to pinnedcalendar ui now properly updates state.event

* Fix Schema.xml, and add PinnedCalednars to event object array render

* working display and partially working udpate of pinned calendars

* update CRMEvent interface from Schema.xml

* begin trying to get new event creation working

* working display and edit for pinned calendars and event type

* fix merge versions

* update package-lock

* add react-datepicker; ensure copied from node_modules to skin/external

* Added event handler for start and end time

* update the API backend to accept event properties as defined in the TS interface file

* add react-moment to render formatted start and end dates

* add interface definitions for the javascript window object's i18next for TypeScript

* use i18next, and cleanup wording

* add build-react step to travis file

* fix conflicts with master package.json

* update gruntfile from master

* update package-lock, composer, upgrade

* add a window object to reference whether FullCalendarJS is finished loading

* add test for new react event creation form

* add missing inputId for the react-select elements

* update react-datepicker; drop react-moment

* add accessor to the global momentjs inside typescript

* cleanup display formatting of DatePicker

* use TypeScript Date native toString method

* Ensure state.event is properly initialized to the interface definition with Date() objects

* Use a stringify replacer to ensure Date objects are submitted in ISO8601 combined format

example: 2007-04-05T12:30-02:00

* minor syntax cleanup

* Ensure CRMEvent interface uses Date not string

* remove dead code

* cleanup console.log entries before mergning to master

* remove weird vagrant node_modules sync stuff

* undo change to vagrant bootstrap - leave the node_modules directory un-sync'd as it was
  • Loading branch information
crossan007 authored and DawoudIO committed Feb 23, 2019
1 parent 0cce932 commit 9c97c2a
Show file tree
Hide file tree
Showing 24 changed files with 13,545 additions and 5,304 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ src/logs/*.log
error.log

nbproject/*
/src/skin/js-react/
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ install:
- npm install
- npm run composer-install
- npm run orm-gen
- npm run build-react
- npm run tests-install

# linters
Expand Down
7 changes: 7 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ module.exports = function (grunt) {
src: ['node_modules/select2/dist/js/select2.min.js',
'node_modules/select2/dist/css/select2.min.css'],
dest: 'src/skin/external/select2'
},
{
expand: true,
filter: 'isFile',
flatten: true,
src: ['node_modules/react-datepicker/dist/react-datepicker.min.css'],
dest: 'src/skin/external/react-datepicker'
}
]
}
Expand Down
17,667 changes: 12,825 additions & 4,842 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 25 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
},
"homepage": "http://www.churchcrm.io",
"devDependencies": {
"@types/jquery": "^3.3.1",
"@types/react": "^16.1.0",
"@types/react-bootstrap": "^0.32.6",
"@types/react-dom": "^16.0.4",
"@types/react-modal": "^3.1.2",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-react": "^6.24.1",
"grunt": "^1.0.3",
"grunt-confirm": "^1.0.7",
"grunt-contrib-clean": "^1.0.0",
Expand All @@ -33,7 +42,15 @@
"grunt-poeditor-ab": "^0.1.9",
"i18next-extract-gettext": "^3.2.1",
"i18next-xhr-backend": "^1.4.1",
"node-sha1": "^1.0.1"
"node-sha1": "^1.0.1",
"react": "^16.3.0",
"react-bootstrap": "^0.32.1",
"react-dom": "^16.3.0",
"react-modal": "^3.3.2",
"ts-loader": "^4.1.0",
"typescript": "^2.8.1",
"webpack": "4.19.1",
"webpack-cli": "^2.1.4"
},
"dependencies": {
"admin-lte": "^2.4.*",
Expand All @@ -46,7 +63,7 @@
"chart.js": "^1.1.*",
"ckeditor": "^4.11.1",
"font-awesome": "^4.7.0",
"fullcalendar": "^3.0.1",
"fullcalendar": "^3.9.0",
"i18n": "^0.8.3",
"i18next": "8.2.1",
"initial-js": "0.3.4",
Expand All @@ -55,8 +72,10 @@
"jquery-steps": "^1.1.0",
"jquery-validation": "^1.16.0",
"node-sass": "^4.11.0",
"select2": "^4.0.5",
"pace-js": "^1.0.2"
"pace-js": "^1.0.2",
"react-datepicker": "^2.1.0",
"react-select": "^2.1.2",
"select2": "^4.0.5"
},
"scripts": {
"install": "grunt clean && grunt updateVersions && grunt curl-dir && grunt copy && grunt sass && grunt lineending",
Expand All @@ -74,6 +93,7 @@
"composer-install": "cd src/ && composer install && cd .. && grunt lineending",
"composer-update": " cd src/ && composer update && composer dump-autoload && cd ../tests/ && composer update && cd .. && grunt lineending",
"tests-install": "scripts/tests-install.sh",
"test": "scripts/tests-run.sh"
"test": "scripts/tests-run.sh",
"build-react": "webpack"
}
}
2 changes: 1 addition & 1 deletion propel/schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@
</table>

<table name="calendar_events" idMethod="native" phpName="CalendarEvent" isCrossRef="true" description="This is a join-table to link an event with a calendar">
<column name="calendar_id" phpName="GroupId" type="SMALLINT" size="8" sqlType="mediumint(8) unsigned" required="true" primaryKey="true"/>
<column name="calendar_id" phpName="CalendarId" type="SMALLINT" size="8" sqlType="mediumint(8) unsigned" required="true" primaryKey="true"/>
<column name="event_id" phpName="EventId" type="SMALLINT" size="8" sqlType="mediumint(8) unsigned" required="true" primaryKey="true"/>

<foreign-key foreignTable="calendars">
Expand Down
39 changes: 39 additions & 0 deletions react/calendar-event-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import CRMEvent from './interfaces/CRMEvent';
import ExistingEvent from './components/ExistingEvent';
declare global {
interface Window {
// Since TypeScript requires a definition for all methods, let's tell it how to handle the javascript objects already in the page
showEventForm(object): void,
showNewEventForm(start: string,end: string): void,
CRM: {
// we need to access this method of CRMJSOM, so let's tell TypeScript how to use it
refreshAllFullCalendarSources(): void
},
// React does have it's own i18next implementation, but for now, lets use the one that's already being loaded
i18next: {
t(string): string
},
// instead of loading the whole react-moment class, we can just use the one that's already on window.
moment: any
}
}

window.showEventForm = function(event) {
const unmount = function() {
ReactDOM.unmountComponentAtNode( document.getElementById('calendar-event-react-app'));
window.CRM.refreshAllFullCalendarSources()
}
unmount();
ReactDOM.render(<ExistingEvent onClose={unmount} eventId={event.id}/>, document.getElementById('calendar-event-react-app'));
}

window.showNewEventForm = function(start,end) {
const unmount = function() {
ReactDOM.unmountComponentAtNode( document.getElementById('calendar-event-react-app'));
window.CRM.refreshAllFullCalendarSources()
}
unmount();
ReactDOM.render(<ExistingEvent onClose={unmount} eventId={0} />, document.getElementById('calendar-event-react-app'));
}
89 changes: 89 additions & 0 deletions react/components/EventPropertiesEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as React from 'react';
import CRMEvent from '../interfaces/CRMEvent';
import Calendar from '../interfaces/Calendar';
import EventType from '../interfaces/EventType';
import { Modal, FormControl } from 'react-bootstrap';
import Select from 'react-select';
import DatePicker from 'react-datepicker';


const EventPropertiesEditor: React.FunctionComponent<{ event: CRMEvent, calendars: Array<Calendar>, eventTypes: Array<EventType>, changeHandler: (event:React.ChangeEvent)=>void, handleStartDateChange: (date: any)=>void, handleEndDateChange: (date: any)=>void, pinnedCalendarChanged: (event: Array<Object>) => void, eventTypeChanged: (event: Array<Object>) => void }> = ({ event, calendars, eventTypes, changeHandler, handleStartDateChange, handleEndDateChange, pinnedCalendarChanged, eventTypeChanged }) => {
//map the Calendar data type (returned from CRM API) into something that react-select can present as dropdown choices
var calendarOptions=calendars.map((Pcal:Calendar) => ({value: Pcal.Id, label: Pcal.Name}) );
var EventTypeOptions=eventTypes.map((eventType:EventType) => ({value: eventType.Id, label: eventType.Name}) );
var initialPinnedCalendarValue=calendars.map((Pcal:Calendar) => {if (event.PinnedCalendars.includes(Pcal.Id) ) { return {value: Pcal.Id, label: Pcal.Name}} } );
var initialEventTypeValue= eventTypes.map( (eventType:EventType) => { if (event.Type == eventType.Id) { return { value: eventType.Id, label: eventType.Name } } } );
return (
<table className="table modal-table">
<tbody>
<tr>
<td className="LabelColumn">
{window.i18next.t('Event Type')}
</td>
<td className="TextColumn">
<Select name="EventType" inputId="EventType" options={EventTypeOptions} value={initialEventTypeValue} onChange={eventTypeChanged} />
</td>
</tr>
<tr>
<td className="LabelColumn">
{window.i18next.t('Description')}
</td>
<td className="TextColumn">
<textarea name="Desc" value={event.Desc} onChange={changeHandler} />
</td>
</tr>
<tr>
<td className="LabelColumn">
{window.i18next.t('Start Date')}
</td>
<td className="TextColumn">
<DatePicker
name="Start"
selected={event.Start}
onChange={handleStartDateChange}
showTimeSelect
timeFormat="h:mm"
timeIntervals={15}
dateFormat="MMMM d, yyyy h:mm aa"
timeCaption="time" />
</td>
</tr>
<tr>
<td className="LabelColumn">
{window.i18next.t('End Date')}
</td>
<td className="TextColumn">
<DatePicker
name="End"
minDate={event.Start}
selected={event.End}
onChange={handleEndDateChange}
showTimeSelect
timeFormat="h:mm"
timeIntervals={15}
dateFormat="MMMM d, yyyy h:mm aa"
timeCaption="time" />
</td>
</tr>
<tr>
<td className="LabelColumn">
{window.i18next.t('Pinned Calendars')}
</td>
<td className="TextColumn">
<Select name="PinnedCalendars" inputId="PinnedCalendars" options={calendarOptions} value={initialPinnedCalendarValue} onChange={pinnedCalendarChanged} isMulti="true" />
</td>
</tr>
<tr>
<td className="LabelColumn">
{window.i18next.t('Text')}
</td>
<td className="TextColumn">
<textarea name="Text" value={event.Text} onChange={changeHandler} />
</td>
</tr>
</tbody>
</table>
)}


export default EventPropertiesEditor
79 changes: 79 additions & 0 deletions react/components/EventPropertiesViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from 'react';
import CRMEvent from '../interfaces/CRMEvent';
import Calendar from '../interfaces/Calendar';
import EventType from '../interfaces/EventType';

const EventPropertiesViewer: React.FunctionComponent<{ event: CRMEvent, calendars: Array<Calendar>, eventTypes: Array<EventType> }> = ({ event, calendars, eventTypes }) => {
return (
<table className="table modal-table">
<tbody>
<tr>
<td>
{window.i18next.t('Type')}
</td>
<td>
{
eventTypes.map(
(eventType: EventType)=> {
if (event.Type != null && event.Type == eventType.Id) {
return (<p>{eventType.Name}</p>)
}
}
)
}
</td>
</tr>
<tr>
<td>
{window.i18next.t('Event Description')}
</td>
<td>
{event.Desc}
</td>
</tr>
<tr>
<td>
{window.i18next.t('Start Date')}
</td>
<td>
{event.Start.toString()}
</td>
</tr>
<tr>
<td>
{window.i18next.t('End Date')}
</td>
<td>
{event.End.toString()}
</td>
</tr>
<tr>
<td>
{window.i18next.t('Pinned Calendars')}
</td>
<td>
<ul>
{
calendars.map(
(calendar: Calendar)=> {
if (event.PinnedCalendars != null && event.PinnedCalendars.includes(calendar.Id)) {
return (<li>{calendar.Name}</li>)
}
}
)
}
</ul>
</td>
</tr>
<tr>
<td>
{window.i18next.t('Text')}
</td>
<td>
{event.Text}
</td>
</tr>
</tbody>
</table>
)}
export default EventPropertiesViewer;
Loading

0 comments on commit 9c97c2a

Please sign in to comment.