Skip to content

Commit

Permalink
fix: move to rrule-rust (#10181)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <matt.krick@gmail.com>
  • Loading branch information
mattkrick authored Sep 11, 2024
1 parent e48732b commit 2952c3d
Show file tree
Hide file tree
Showing 25 changed files with 469 additions and 447 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ jobs:

- name: Store Artifacts from Failed Tests
if: failure()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: test-results
path: packages/integration-tests/test-results/
Expand Down
4 changes: 2 additions & 2 deletions codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"defaultScalarType": "string",
"enumsAsTypes": true,
"optionalResolveType": true,
"scalars": {"DateTime": "Date", "File": "TFile", "RRule": "RRule"}
"scalars": {"DateTime": "Date", "File": "TFile", "RRule": "RRuleSet"}
},
"generates": {
"packages/server/graphql/private/resolverTypes.ts": {
Expand Down Expand Up @@ -146,7 +146,7 @@
"PokerMeeting": "../../database/types/MeetingPoker#default as MeetingPoker",
"PokerMeetingMember": "../../database/types/MeetingPokerMeetingMember#default as PokerMeetingMemberDB",
"PokerTemplate": "../../database/types/PokerTemplate#default as PokerTemplateDB",
"RRule": "rrule#RRule",
"RRule": "rrule-rust#RRuleSet",
"Reactable": "../../database/types/Reactable#Reactable",
"Reactji": "../types/Reactji#ReactjiSource",
"ReflectPrompt": "../../database/types/RetrospectivePrompt#default",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@
"@graphql-codegen/typescript-resolvers": "^4.0.1",
"@graphql-tools/merge": "^9.0.0",
"@sucrase/webpack-loader": "^2.0.0",
"@swc/core": "^1.3.96",
"@swc/core": "^1.7.22",
"@swc/jest": "^0.2.36",
"@tailwindcss/container-queries": "^0.1.0",
"@tailwindcss/forms": "^0.5.3",
"@types/dotenv": "^6.1.1",
Expand Down
22 changes: 12 additions & 10 deletions packages/client/components/AnalyticsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,18 @@ if (datadogEnabled) {
datadogRum.startSessionReplayRecording()
}

amplitude.init(window.__ACTION__.AMPLITUDE_WRITE_KEY, {
defaultTracking: {
attribution: false,
pageViews: false,
sessions: false,
formInteractions: false,
fileDownloads: false
},
logLevel: __PRODUCTION__ ? amplitude.Types.LogLevel.None : amplitude.Types.LogLevel.Debug
})
if (window.__ACTION__.AMPLITUDE_WRITE_KEY) {
amplitude.init(window.__ACTION__.AMPLITUDE_WRITE_KEY, {
defaultTracking: {
attribution: false,
pageViews: false,
sessions: false,
formInteractions: false,
fileDownloads: false
},
logLevel: __PRODUCTION__ ? amplitude.Types.LogLevel.None : amplitude.Types.LogLevel.Debug
})
}

const AnalyticsPage = () => {
const atmosphere = useAtmosphere()
Expand Down
25 changes: 10 additions & 15 deletions packages/client/components/Recurrence/RecurrenceSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import clsx from 'clsx'
import dayjs from 'dayjs'
import dayjs, {Dayjs} from 'dayjs'
import timezonePlugin from 'dayjs/plugin/timezone'
import utcPlugin from 'dayjs/plugin/utc'
import React, {PropsWithChildren, useEffect} from 'react'
import {Frequency, RRule} from 'rrule'
import {MenuPosition} from '../../hooks/useCoords'
import useMenu from '../../hooks/useMenu'
import {getJSDateFromRRuleDate, getRRuleDateFromJSDate} from '../../shared/rruleUtil'
import {fromRRuleDateTime, toRRuleDateTime} from '../../shared/rruleUtil'
import plural from '../../utils/plural'
import DropdownMenuToggle from '../DropdownMenuToggle'
import {toHumanReadable} from './HumanReadableRecurrenceRule'
import {Day, RecurrenceDayCheckbox} from './RecurrenceDayCheckbox'
import {RecurrenceTimePicker} from './RecurrenceTimePicker'
dayjs.extend(utcPlugin)

dayjs.extend(utcPlugin)
dayjs.extend(timezonePlugin)
export const ALL_DAYS: Day[] = [
{
name: 'Monday',
Expand Down Expand Up @@ -174,16 +176,10 @@ export const RecurrenceSettings = (props: Props) => {
? rrule.options.byweekday.map((weekday) => ALL_DAYS.find((day) => day.intVal === weekday)!)
: []
)
const [recurrenceStartTime, setRecurrenceStartTime] = React.useState<Date>(
const [recurrenceStartTime, setRecurrenceStartTime] = React.useState<Dayjs>(
rrule
? getJSDateFromRRuleDate(rrule.options.dtstart)
: dayjs()
.add(1, 'day')
.set('hour', 6)
.set('minute', 0)
.set('second', 0)
.set('millisecond', 0)
.toDate() // suggest 6:00 AM tomorrow
? fromRRuleDateTime(rrule)
: dayjs().add(1, 'day').set('hour', 6).set('minute', 0).set('second', 0).set('millisecond', 0) // suggest 6:00 AM tomorrow
)

const {timeZone} = Intl.DateTimeFormat().resolvedOptions()
Expand Down Expand Up @@ -222,14 +218,13 @@ export const RecurrenceSettings = (props: Props) => {
freq: Frequency.WEEKLY,
interval: recurrenceInterval,
byweekday: recurrenceDays.map((day) => day.rruleVal),
dtstart: getRRuleDateFromJSDate(recurrenceStartTime),
dtstart: toRRuleDateTime(recurrenceStartTime),
tzid: timeZone
})
: null

onRruleUpdated(rrule)
}, [recurrenceDays, recurrenceInterval, recurrenceStartTime])

return (
<div className='space-y-4 p-4'>
<div className='space-y-1'>
Expand Down Expand Up @@ -292,7 +287,7 @@ export const RecurrenceSettings = (props: Props) => {
<Label>Each instance starts at</Label>
<DropdownMenuToggle
className='w-full text-sm'
defaultText={`${dayjs(recurrenceStartTime).format('h:mm A')} (${timeZone})`}
defaultText={`${recurrenceStartTime.local().format('h:mm A')} (${timeZone})`}
onClick={togglePortal}
ref={originRef}
size='small'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import dayjs, {Dayjs} from 'dayjs'
import ms from 'ms'
import React from 'react'
import {MenuProps} from '../../hooks/useMenu'
Expand All @@ -7,7 +7,7 @@ import MenuItem from '../MenuItem'

interface Props {
menuProps: MenuProps
onClick: (n: Date) => void
onClick: (n: Dayjs) => void
}

const OPTIONS = [...Array(96).keys()].map((n) => n * ms('15m'))
Expand All @@ -16,20 +16,19 @@ const DEFAULT_MEETING_START_TIME_IDX = OPTIONS.findIndex((n) => n === ms('6h'))

export const RecurrenceTimePicker = (props: Props) => {
const {menuProps, onClick} = props
const startOfToday = new Date().setHours(0, 0, 0, 0)
return (
<Menu
{...menuProps}
ariaLabel={'Select the time when a recurring meeting will be created'}
defaultActiveIdx={DEFAULT_MEETING_START_TIME_IDX}
>
{OPTIONS.map((n, idx) => {
const proposedTime = dayjs(startOfToday + n).add(1, 'day')
const proposedTime = dayjs().add(1, 'day').startOf('day').add(n, 'ms')
return (
<MenuItem
key={idx}
label={proposedTime.format('h:mm A')}
onClick={() => onClick(proposedTime.toDate())}
onClick={() => onClick(proposedTime)}
/>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export const UpdateRecurrenceSettingsModal = (props: Props) => {
return (
<UpdateRecurrenceSettingsModalRoot>
<input
className='form-input rounded border border-solid border-slate-500 p-2 font-sans text-base hover:border-slate-600 focus:border-slate-600 focus:outline-none focus:ring-1 focus:ring-slate-600'
className='form-input border-none p-4 font-sans text-base outline-none focus:outline-none focus:ring-1 focus:ring-slate-600'
type='text'
name='title'
placeholder={placeholder}
Expand Down
7 changes: 1 addition & 6 deletions packages/client/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ module.exports = {
testEnvironment: 'node',
transform: {
'\\.(gql|graphql)$': '../server/__tests__/jest-transform-graphql-shim.js',
'^.+\\.tsx?$': [
'ts-jest',
{
diagnostics: false
}
]
'^.+\\.(t|j)sx?$': ['@swc/jest']
},
modulePaths: ['<rootDir>/packages/'],
moduleNameMapper: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,48 +121,50 @@ const ProviderList = (props: Props) => {
{
name: 'Atlassian',
connected: !!integrations?.atlassian?.accessToken,
component: <AtlassianProviderRow teamId={teamId} retry={retry} viewer={viewer} />
component: (
<AtlassianProviderRow key='atlassian' teamId={teamId} retry={retry} viewer={viewer} />
)
},
{
name: 'Jira Server',
connected:
!!integrations?.jiraServer?.auth?.isActive && integrations.jiraServer?.sharedProviders[0],
component: <JiraServerProviderRow teamId={teamId} viewerRef={viewer} />
component: <JiraServerProviderRow key='jira' teamId={teamId} viewerRef={viewer} />
},
{
name: 'GitHub',
connected: !!integrations?.github?.accessToken,
component: <GitHubProviderRow teamId={teamId} viewer={viewer} />
component: <GitHubProviderRow key='github' teamId={teamId} viewer={viewer} />
},
{
name: 'GitLab',
connected: !!integrations?.gitlab.auth,
component: <GitLabProviderRow teamId={teamId} viewerRef={viewer} />
component: <GitLabProviderRow key='gitlab' teamId={teamId} viewerRef={viewer} />
},
{
name: 'Mattermost',
connected: !!integrations?.mattermost.auth,
component: <MattermostProviderRow teamId={teamId} viewerRef={viewer} />
component: <MattermostProviderRow key='mm' teamId={teamId} viewerRef={viewer} />
},
{
name: 'Slack',
connected: integrations?.slack?.isActive,
component: <SlackProviderRow teamId={teamId} viewer={viewer} />
component: <SlackProviderRow key='slack' teamId={teamId} viewer={viewer} />
},
{
name: 'Azure DevOps',
connected: !!integrations?.azureDevOps.auth?.accessToken,
component: <AzureDevOpsProviderRow teamId={teamId} viewerRef={viewer} />
component: <AzureDevOpsProviderRow key='azure' teamId={teamId} viewerRef={viewer} />
},
{
name: 'MS Teams',
connected: !!integrations?.msTeams.auth,
component: <MSTeamsProviderRow teamId={teamId} viewerRef={viewer} />
component: <MSTeamsProviderRow key='teams' teamId={teamId} viewerRef={viewer} />
},
{
name: 'Gcal Integration',
connected: !!integrations?.gcal?.auth,
component: <GcalProviderRow viewerRef={viewer} teamId={teamId} />
component: <GcalProviderRow key='gcal' viewerRef={viewer} teamId={teamId} />
}
]

Expand Down
32 changes: 32 additions & 0 deletions packages/client/shared/__tests__/rruleUtil.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import dayjs from 'dayjs'
import {fromDateTime, toDateTime} from '../rruleUtil'

test('toDateTime: Should handle TZID', () => {
// at noon UTC what time is it in Phoenix?
const now = dayjs('2022-01-01T12:00:00Z')
const tzid = 'America/Phoenix'
const str = toDateTime(now, tzid)
expect(str).toBe('20220101T050000')
})

test('toDateTime: Should handle UTC TZID', () => {
// at noon UTC what time is it in UTC?
const now = dayjs('2022-01-01T12:00:00Z')
const tzid = 'UTC'
const str = toDateTime(now, tzid)
expect(str).toBe('20220101T120000')
})

test('fromDateTime: Should handle TZID', () => {
const dateTimeStr = '20220101T050000'
const tzid = 'America/Phoenix'
const day = fromDateTime(dateTimeStr, tzid)
expect(day.toISOString()).toBe('2022-01-01T12:00:00.000Z')
})

test('fromDateTime: Should handle UTC TZID', () => {
const dateTimeStr = '20220101T050000'
const tzid = 'UTC'
const day = fromDateTime(dateTimeStr, tzid)
expect(day.toISOString()).toBe('2022-01-01T05:00:00.000Z')
})
46 changes: 29 additions & 17 deletions packages/client/shared/rruleUtil.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import {datetime} from 'rrule'
import dayjs, {Dayjs} from 'dayjs'
import customParsePlugin from 'dayjs/plugin/customParseFormat'
import timezonePlugin from 'dayjs/plugin/timezone'
import utcPlugin from 'dayjs/plugin/utc'
import {RRule} from 'rrule'

export const getRRuleDateFromJSDate = (date: Date) => {
return datetime(
date.getFullYear(),
date.getMonth() + 1,
date.getDate(),
date.getHours(),
date.getMinutes()
)
dayjs.extend(customParsePlugin)
dayjs.extend(utcPlugin)
dayjs.extend(timezonePlugin)

// the RRule package requires dstart to be a date object set to a negative UTC offset. It's ugly!
export const toRRuleDateTime = (date: Dayjs) => {
return date.tz('UTC', true).toDate()
}

export const fromRRuleDateTime = (rrule: RRule) => {
const {options} = rrule
const {dtstart, tzid} = options
const tzidTimeStr = `${dtstart.getUTCFullYear()}-${dtstart.getUTCMonth() + 1}-${dtstart.getUTCDate()} ${dtstart.getUTCHours()}:${dtstart.getUTCMinutes()}`
return tzid ? dayjs.tz(tzidTimeStr, tzid) : dayjs(tzidTimeStr)
}

// These are used by rrule-rust on the server, which has a special DateTime object
export const toDateTime = (date: Dayjs, tzid: string) => {
return tzid
? date.tz(tzid).format('YYYYMMDD[T]HHmmss')
: date.utc().format('YYYYMMDD[T]HHmmss[Z]')
}

export const getJSDateFromRRuleDate = (rruleDate: Date) => {
return new Date(
rruleDate.getUTCFullYear(),
rruleDate.getUTCMonth(),
rruleDate.getUTCDate(),
rruleDate.getUTCHours(),
rruleDate.getUTCMinutes()
)
export const fromDateTime = (rfc5545String: string, tzid: string) => {
const rawDate = dayjs.utc(rfc5545String, 'YYYYMMDD[T]HHmmss')
return tzid ? dayjs.tz(rawDate.format('YYYY-MM-DD HH:mm'), tzid) : rawDate
}
Loading

0 comments on commit 2952c3d

Please sign in to comment.