import quip from "quip-apps-api" ; import quiptext from "quiptext" ; import moment from "moment" ; import isDate from "date-fns/isDate" ; import startOfWeek from "date-fns/startOfWeek" ; import { isSameDay } from "./util" ; import debounce from "lodash.debounce" ; import { DateRange } from "./types" ; export type Event = { color : "RED" | "ORANGE" | "YELLOW" | "GREEN" | "BLUE" | "VIOLET" ; dateRange : string ; titleText : string ; } ; export const colors = [ quip . apps . ui . ColorMap . RED . KEY , quip . apps . ui . ColorMap . ORANGE . KEY , quip . apps . ui . ColorMap . YELLOW . KEY , quip . apps . ui . ColorMap . GREEN . KEY , quip . apps . ui . ColorMap . BLUE . KEY , quip . apps . ui . ColorMap . VIOLET . KEY , ] ; const formatDisplayMonthForStorage = ( d ) : string => { return String ( d ) ; } ; const formatDefaultMonthForApi = ( d ) : string => { return moment ( d ) . format ( "YYYY-MM" ) ; } ; const parseDefaultMonthFromStorage = ( displayMonth ) : Date => { const regex = /^\d{4}-[01][0-9]$/g ; if ( regex . test ( displayMonth ) ) { const tempDate = new Date ( displayMonth ) ; return new Date ( tempDate . setMonth ( tempDate . getMonth ( ) + 1 ) ) ; } return new Date ( displayMonth ) ; } ; const parseDisplayMonthFromApi = ( s ) : Date => { const date = moment ( s , "YYYY-MM" ) . toDate ( ) ; date . setDate ( date . getDate ( ) + 5 ) ; return date ; } ; const formatAllDayDateForStorage = ( d ) : string => { return `${ d . getFullYear ( ) },${ d . getMonth ( ) },${ d . getDate ( ) }` ; } ; const formatAllDayDateForApi = ( d ) : string => { return moment ( d ) . format ( "YYYY-MM-DD" ) ; } ; const parseAllDayStringFromStorage = ( s ) : Date => { if ( typeof s === "number" ) { return new Date ( s ) ; } else if ( s . includes ( "," ) ) { const [ year , monthIndex , day ] = s . split ( "," ) ; return new Date ( window . parseInt ( year , 10 ) , window . parseInt ( monthIndex , 10 ) , window . parseInt ( day , 10 ) ) ; } else { throw new Error ( s + " is not a valid date from storage." ) ; } } ; const parseAllDayStringFromApi = ( s ) : Date => { return moment ( s , "YYYY-MM-DD" ) . toDate ( ) ; } ; const eventPlaceholderText = quiptext ( "New Event" ) ; export class RootRecord extends quip . apps . RootRecord { private listener ; static getProperties ( ) { return { events : quip . apps . RecordList . Type ( EventRecord ) , displayMonth : "string" , } ; } static getDefaultProperties ( ) { return { displayMonth : String ( new Date ( ) ) , events : [ ] , } ; } initialize ( ) { this . listener = this . listen ( this . setStatePayload_ ) ; } setStatePayload_ = debounce ( ( ) => { if ( typeof quip . apps . setPayload === "function" ) { quip . apps . setPayload ( this . getExportState ( ) ) ; } } , 2000 ) ; getEvents ( ) { return this . get ( "events" ) . getRecords ( ) ; } getEventsByStartDate ( start ) { return this . getEvents ( ) . filter ( event => isSameDay ( event . getDateRange ( ) . start , start ) ) ; } getLastEvent ( ) { const records = this . get ( "events" ) . getRecords ( ) ; if ( ! records . length ) { return ; } return records . reduce ( ( a , b ) => { return b . get ( "created" ) > a . get ( "created" ) ? b : a ; } ) ; } populateDisplayMonth ( displayMonth ) { const displayMonthDate = parseDisplayMonthFromApi ( displayMonth ) ; this . set ( "displayMonth" , formatDisplayMonthForStorage ( displayMonthDate ) ) ; } populateEvents ( events ) { events . forEach ( event => { const start = parseAllDayStringFromApi ( event . dateRange . start ) ; const end = parseAllDayStringFromApi ( event . dateRange . end ) ; this . get ( "events" ) . add ( { dateRange : JSON . stringify ( { start : formatAllDayDateForStorage ( start ) , end : formatAllDayDateForStorage ( end ) , } ) , color : event . color , title : { "RichText_placeholderText" : eventPlaceholderText , "RichText_defaultText" : event . content , } , } , this . getNextIndexForStartDate ( start ) ) ; } ) ; quip . apps . recordQuipMetric ( "events_populated" , undefined ) ; return this . get ( "events" ) ; } addEvent ( start , end ) { let color = quip . apps . ui . ColorMap . RED . KEY ; const lastEvent = this . getLastEvent ( ) ; if ( lastEvent ) { const lastColor = lastEvent . getColor ( ) ; const lastColorIndex = colors . indexOf ( lastColor ) ; let nextColorIndex = lastColorIndex + 1 ; if ( nextColorIndex >= colors . length ) { nextColorIndex = 0 ; } color = colors [ nextColorIndex ] ; } const newEvent = this . get ( "events" ) . add ( { dateRange : JSON . stringify ( { start : formatAllDayDateForStorage ( start ) , end : formatAllDayDateForStorage ( end ) , } ) , color , } , this . getNextIndexForStartDate ( start ) ) ; quip . apps . recordQuipMetric ( "add_event" , { event_id : newEvent . id ( ) , } ) ; return newEvent ; } getExportState ( ) { let events = this . getEvents ( ) . map ( event => { const dateRange = event . getDateRange ( ) ; dateRange . start = formatAllDayDateForApi ( dateRange . start ) ; dateRange . end = formatAllDayDateForApi ( dateRange . end ) ; return { color : event . getColor ( ) , dateRange , content : event . getTitleText ( ) , } ; } ) ; return JSON . stringify ( { events , displayMonth : formatDefaultMonthForApi ( this . getDisplayMonth ( ) ) , } ) ; } getNextIndexForStartDate ( startDate , excludeEvent ? ) { let nextIndex = 0 ; let events = this . getEvents ( ) ; if ( excludeEvent ) { events = events . filter ( event => event . id ( ) !== excludeEvent . id ( ) ) ; } if ( ! events . length ) { return nextIndex ; } const t = startDate . getTime ( ) ; for ( let i = 0 , event ; ( event = events [ i ] ) ; i ++ ) { if ( t < event . getDateRange ( ) . start . getTime ( ) ) { nextIndex = i ; break ; } nextIndex = i + 1 ; } return nextIndex ; } getDisplayMonth ( ) { const displayMonth = this . get ( "displayMonth" ) ; return parseDefaultMonthFromStorage ( displayMonth ) ; } setDisplayMonth ( date ) { this . set ( "displayMonth" , formatDisplayMonthForStorage ( date ) ) ; quip . apps . recordQuipMetric ( "set_display_month" , { } ) ; } } export class EventRecord extends quip . apps . Record { private listener ; private commentsListener ; private titleContentListener ; domNode : Element | undefined | null ; domNodesEvent : { [ x ] : Element ; } ; static getProperties ( ) { return { color : "string" , created : "number" , dateRange : "string" , title : quip . apps . RichTextRecord , } ; } static getDefaultProperties ( ) { return { color : quip . apps . ui . ColorMap . RED . KEY , created : Date . now ( ) , title : { RichText_placeholderText : eventPlaceholderText , } , } ; } initialize ( ) { this . domNode = null ; this . domNodesEvent = { } ; this . listener = this . listen ( this . notifyParent ) ; this . commentsListener = this . listenToComments ( this . notifyParent ) ; if ( this . get ( "title" ) ) { this . titleContentListener = this . get ( "title" ) . listenToContent ( this . notifyParent ) ; } } delete ( ) { if ( this . listener ) { this . unlisten ( this . notifyParent ) ; } if ( this . commentsListener ) { this . unlistenToComments ( this . notifyParent ) ; } if ( this . titleContentListener ) { if ( this . get ( "title" ) ) { this . get ( "title" ) . unlistenToContent ( this . notifyParent ) ; } } quip . apps . recordQuipMetric ( "delete_event" , { event_id : this . id ( ) , } ) ; super . delete ( ) ; } notifyParent = ( ) => { this . getParentRecord ( ) . notifyListeners ( ) ; } ; supportsComments ( ) { return true ; } getTitleText ( ) { if ( this . get ( "title" ) ) { return this . get ( "title" ) . getTextContent ( ) ; } } getDom ( ) { return this . domNode ; } setDom ( el ? ) { this . domNode = el ; } getDomNodesForEvent ( ) { return this . domNodesEvent ; } getDomEvent ( weekStartTime ? ) { if ( ! this . domNodesEvent ) { return ; } weekStartTime = weekStartTime || startOfWeek ( this . getDateRange ( ) . start ) . getTime ( ) ; return this . domNodesEvent [ weekStartTime ] ; } setDomEvent ( eventForWeek ? ) { if ( ! eventForWeek ) { this . domNodesEvent = { } ; return ; } if ( ! this . domNodesEvent ) { this . domNodesEvent = { } ; } const { el , weekStartTime } = eventForWeek ; this . domNodesEvent [ weekStartTime ] = el ; } getIndex ( ) { return this . getContainingList ( ) . indexOf ( this . id ( ) ) ; } setIndex ( i ) { if ( typeof i !== "number" ) { throw new Error ( "Cannot setIndex without a number value: " + i ) ; } this . getContainingList ( ) . move ( this , i ) ; } getDateRange ( ) { const { start , end } = JSON . parse ( this . get ( "dateRange" ) ) ; return { start : parseAllDayStringFromStorage ( start ) , end : parseAllDayStringFromStorage ( end ) , } ; } setDateRange ( start , end ) { if ( ! ( isDate ( start ) && isDate ( end ) ) ) { console . error ( "start" , start , "end" , end ) ; throw new Error ( "start and end must both be Date types" ) ; } this . set ( "dateRange" , JSON . stringify ( { start : formatAllDayDateForStorage ( start ) , end : formatAllDayDateForStorage ( end ) , } ) ) ; quip . apps . recordQuipMetric ( "move_event" , { event_id : this . id ( ) , } ) ; } getColor ( ) { return this . get ( "color" ) ; } setColor ( color ) { this . set ( "color" , color ) ; } } quip . apps . registerClass ( RootRecord , "Root" ) ; quip . apps . registerClass ( EventRecord , "project-calendar-event" ) ;