From f9f537e3dd28aa543770aea158e4eff65be0b261 Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 31 Jan 2019 14:48:28 -0500 Subject: impl GCalendar sync model --- src/App.js | 46 +++++++-------- src/PatternTable.js | 10 ++-- src/gapi.js | 167 +++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 178 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/App.js b/src/App.js index 6e9dc27..4d23440 100755 --- a/src/App.js +++ b/src/App.js @@ -80,14 +80,6 @@ class Dashboard extends React.Component { calendars: {} }; - handleChangePage = (event, page) => { - this.setState({ page }); - }; - - handleChangeRowsPerPage = event => { - this.setState({ rowsPerPage: event.target.value }); - }; - updatePattern = (field, idx, value) => { let patterns = this.state.patterns; patterns[idx][field] = value; @@ -114,32 +106,31 @@ class Dashboard extends React.Component { alert("Please choose a valid time range."); return; } - let start = this.state.startDate.toISOString(); - let end = this.state.endDate.toISOString(); + let start = this.state.startDate.toDate(); + let end = this.state.endDate.toDate(); + console.log(start, end); let event_pms = []; - for (let id in this.cached.calendars) { - event_pms.push( - this.state.token - .then(gapi.genEventsGetter(id, start, end)) - .then(items => this.cached.calendars[id].events = items)); - } + for (let id in this.cached.calendars) + event_pms.push(this.cached.calendars[id].cal.getEvents(start, end).then( + r => { return { id, events: r } })); - Promise.all(event_pms).then(() => { + Promise.all(event_pms).then(all_events => { + let events = {}; let results = {}; // pattern idx => time let cal_results = {}; // cal id => time + all_events.forEach(e => events[e.id] = e.events); for (let i = 0; i < this.state.patterns.length; i++) results[i] = 0; for (let id in this.cached.calendars) { + if (!events[id]) continue; let patterns = filterPatterns(this.state.patterns, this.cached.calendars[id].name); - if (!this.cached.calendars[id].events) continue; - this.cached.calendars[id].events.forEach(event => { - if (event.status !== "confirmed") return; + events[id].forEach(event => { patterns.forEach(p => { if (!p.event.regex.test(event.summary)) return; - if (cal_results[id] === undefined) { + if (!cal_results.hasOwnProperty(id)) { cal_results[id] = 0; } - let duration = (new Date(event.end.dateTime) - new Date(event.start.dateTime)) / 60000; + let duration = (event.end - event.start) / 60000; results[p.idx] += duration; cal_results[id] += duration; }); @@ -156,6 +147,7 @@ class Dashboard extends React.Component { value: (cal_results[id] / 60.0), color: this.cached.calendars[id].color.background}); } + //console.log(patternGraphData, calendarGraphData); this.setState({ patternGraphData, calendarGraphData }); }); }; @@ -170,8 +162,8 @@ class Dashboard extends React.Component { items.forEach(item => { this.cached.calendars[item.id] = { name: item.summary, - events: {}, - color: colors[item.colorId] + color: colors[item.colorId], + cal: new gapi.GCalendar(item.id, item.summary) }; }); this.setState({ patterns: items.map((item, idx) => { @@ -210,7 +202,11 @@ class Dashboard extends React.Component { style={{marginBottom: '0.12em', marginLeft: '0.5em'}} onClick={() => this.newPattern()}> - + diff --git a/src/PatternTable.js b/src/PatternTable.js index 001c468..19ac292 100644 --- a/src/PatternTable.js +++ b/src/PatternTable.js @@ -45,11 +45,11 @@ class PatternTable extends React.Component { rowsPerPage: 5, }; - handleChangePage(event, page) { + handleChangePage = (event, page) => { this.setState({ page }); } - handleChangeRowsPerPage(event) { + handleChangeRowsPerPage = event => { this.setState({ rowsPerPage: event.target.value }); } @@ -69,13 +69,13 @@ class PatternTable extends React.Component { this.updatePattern(s.field, p.idx, event.target.value)}/> + onChange={event => this.props.onUpdatePattern(s.field, p.idx, event.target.value)}/> )}) } this.removePattern(p.idx)} /> + onClick={() => this.props.onRemovePattern(p.idx)} /> )); @@ -116,6 +116,8 @@ PatternTable.propTypes = { classes: PropTypes.object.isRequired, patterns: PropTypes.array.isRequired, cached: PropTypes.object.isRequired, + onRemovePattern: PropTypes.func.isRequired, + onUpdatePattern: PropTypes.func.isRequired, }; export default withStyles(styles)(PatternTable); diff --git a/src/gapi.js b/src/gapi.js index 975d4d9..c66a9ff 100644 --- a/src/gapi.js +++ b/src/gapi.js @@ -12,28 +12,163 @@ export function getAuthToken() { } export function getCalendars(token) { - return fetch(gapi_base + '/users/me/calendarList?' + to_params({access_token: token}), + return fetch(`${gapi_base}/users/me/calendarList?${to_params({access_token: token})}`, { method: 'GET', async: true }) .then(response => response.json()) .then(data => data.items); } -export function genEventsGetter(calId, timeMin, timeMax) { - return token => fetch(gapi_base + '/calendars/' + calId + '/events?' + to_params({ - access_token: token, - timeMin, - timeMax - }), { method: 'GET', async: true }) - .then(response => { - if (response.status === 200) - return response.json() - else throw `got response ${response.status}`; - }) - .catch(e => { console.log(e); return []; }) - .then(data => data.items); +export function getColors(token) { + return fetch(`${gapi_base}/colors?${to_params({access_token: token})}`, + { method: 'GET', async: true }) + .then(response => response.json()); } -export function getColors(token) { - return fetch(gapi_base + '/colors?' + to_params({access_token: token}), { method: 'GET', async: true }) +function getEvent(calId, eventId, token) { + return fetch(`${gapi_base}/calendars/${calId}/events/${eventId}?${to_params({access_token: token})}`, + { method: 'GET', async: true }) .then(response => response.json()); } + +function getEvents(calId, token, syncToken, resultsPerRequest = '') { + let results = []; + const singleFetch = (pageToken, syncToken) => fetch(`${gapi_base}/calendars/${calId}/events?${to_params({ + access_token: token, + pageToken, + syncToken, + maxResults: resultsPerRequest + })}`, { method: 'GET', async: true }) + .then(response => { + if (response.status === 200) + return response.json(); + else throw {}; + }) + .catch(e => e) + .then(data => { + if (!data.items) + return ({ + nextSyncToken: '', + results: [] + }); + results.push(...data.items); + if (data.nextPageToken) { + return singleFetch(data.nextPageToken, ''); + } else { + return ({ + nextSyncToken: data.nextSyncToken, + results + }); + } + }) + + return singleFetch('', syncToken); +} + +export class GCalendar { + constructor(calId, name) { + this.calId = calId; + this.name = name; + this.token = getAuthToken(); + this.syncToken = ''; + this.cache = {}; + } + + static dateToCacheKey(date) { + return Math.floor(date / 8.64e7); + } + + getSlot(k) { + if (!this.cache[k]) + this.cache[k] = {}; + return this.cache[k]; + } + + static slotStartDate(k) { return new Date(k * 8.64e7); } + static slotEndDate(k) { return new Date((k + 1) * 8.64e7); } + + addEvent(e) { + let ks = GCalendar.dateToCacheKey(e.start); + let ke = GCalendar.dateToCacheKey(new Date(e.end.getTime() - 1)); + if (ks === ke) + this.getSlot(ks)[e.id] = { + start: e.start, + end: e.end, + id: e.id }; + else + { + this.getSlot(ks)[e.id] = { + start: e.start, + end: GCalendar.slotEndDate(ks), + id: e.id }; + this.getSlot(ke)[e.id] = { + start: GCalendar.slotStartDate(ke), + end: e.end, + id: e.id }; + for (let k = ks + 1; k < ke; k++) + this.getSlot(k)[e.id] = { + start: GCalendar.slotStartDate(k), + end: GCalendar.slotEndDate(k), + id: e.id }; + } + } + + removeEvent(e) { + let ks = GCalendar.dateToCacheKey(e.start); + let ke = GCalendar.dateToCacheKey(new Date(e.end.getTime() - 1)); + for (let k = ks; k <= ke; k++) + delete this.getSlot(k)[e.id]; + } + + getSlotEvents(k, start, end) { + let s = this.getSlot(k); + let results = []; + for (let id in s) { + if (!(s[id].start >= end || s[id].end <= start)) + { + let nstart = s[id].start < start ? start: s[id].start; + let nend = s[id].end > end ? end: s[id].end; + if (nstart > nend) console.log(s[id], start, end); + results.push({ + id, + start: s[id].start < start ? start: s[id].start, + end: s[id].end > end ? end: s[id].end + }); + } + } + return results; + } + + getCachedEvents(start, end) { + let ks = GCalendar.dateToCacheKey(start); + let ke = GCalendar.dateToCacheKey(new Date(end.getTime() - 1)); + let results = this.getSlotEvents(ks, start, end); + for (let k = ks + 1; k < ke; k++) + { + let s = this.getSlot(k); + for (let id in s) + results.push(s[id]); + } + if (ke > ks) + results.push(...this.getSlotEvents(ke, start, end)); + return results; + } + + sync() { + return this.token.then(token => getEvents(this.calId, token, this.syncToken).then(r => { + this.syncToken = r.nextSyncToken; + let pm_results = r.results.map(e => e.start ? Promise.resolve(e) : getEvent(this.calId, e.id, token)); + return Promise.all(pm_results).then(results => results.forEach(e => { + e.start = new Date(e.start.dateTime); + e.end = new Date(e.end.dateTime); + if (e.status === 'confirmed') + this.addEvent(e); + else if (e.status === 'cancelled') + this.removeEvent(e); + })); + })); + } + + getEvents(start, end) { + return this.sync().then(() => this.getCachedEvents(start, end)); + } +} -- cgit v1.2.3