= args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | logs: [
15 | 'Deleted 1 subscription',
16 | 'Deleted 27 post drafts',
17 | 'Skipped 13 published posts',
18 | '> View published posts here: https://interval.com/blog/author/storybook (text after link) ',
19 | 'Deleted 13 comments',
20 | ],
21 | isCompleted: true,
22 | }
23 |
24 | export const Empty = Template.bind({})
25 | Empty.args = {
26 | logs: [],
27 | isCompleted: false,
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/WrapOnUnderscores.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 |
3 | export default function WrapOnUnderscores({ label }: { label: string }) {
4 | const pieces = useMemo(() => label.split('_'), [label])
5 | return (
6 | <>
7 | {pieces.map((piece, i) => (
8 |
9 | {piece}
10 | {i < pieces.length - 1 && (
11 | <>
12 | _
13 | >
14 | )}
15 |
16 | ))}
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayCode/DisplayCode.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.Code',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Customer information',
15 | code: 'console.log("Hello world")',
16 | language: 'javascript',
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayCode/index.tsx:
--------------------------------------------------------------------------------
1 | import { RCTResponderProps } from '~/components/RenderIOCall'
2 | import HighlightedCodeBlock from '~/components/HighlightedCodeBlock'
3 |
4 | export default function DisplayCode(props: RCTResponderProps<'DISPLAY_CODE'>) {
5 | return (
6 |
7 | {props.label}
8 |
9 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayHTML/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react'
2 |
3 | import { RCTResponderProps } from '~/components/RenderIOCall'
4 | import RenderHTML from '~/components/RenderHTML'
5 |
6 | const DisplayMarkdown = memo(function DisplayHTML({
7 | label,
8 | html,
9 | }: RCTResponderProps<'DISPLAY_HTML'>) {
10 | return (
11 |
12 |
{label}
13 |
14 |
15 | )
16 | })
17 |
18 | export default DisplayMarkdown
19 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayHTML/lazy.tsx:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react'
2 |
3 | const DisplayHTML = lazy(() =>
4 | // @ts-ignore
5 | import.meta.env.SSR ? import('./stub') : import('.')
6 | )
7 |
8 | export { DisplayHTML as default }
9 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayHTML/stub.tsx:
--------------------------------------------------------------------------------
1 | import { RCTResponderProps } from '~/components/RenderIOCall'
2 |
3 | export default function DisplayHTMLStub(
4 | _props: RCTResponderProps<'DISPLAY_HTML'>
5 | ) {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayHeading/DisplayHeading.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.Heading',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Customer information',
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayImage/DisplayImage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.Image',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Sample image',
15 | url: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayImage/index.tsx:
--------------------------------------------------------------------------------
1 | import { RCTResponderProps } from '~/components/RenderIOCall'
2 | import classNames from 'classnames'
3 |
4 | export default function DisplayImage(
5 | props: RCTResponderProps<'DISPLAY_IMAGE'>
6 | ) {
7 | return (
8 |
9 |
{props.label}
10 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayLink/DisplayLink.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.Link',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Customer information',
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayMarkdown/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react'
2 | import dedent from 'ts-dedent'
3 |
4 | import { RCTResponderProps } from '~/components/RenderIOCall'
5 | import RenderMarkdown from '~/components/RenderMarkdown'
6 |
7 | const DisplayMarkdown = memo(function DisplayMarkdown({
8 | label,
9 | }: RCTResponderProps<'DISPLAY_MARKDOWN'>) {
10 | return (
11 |
12 |
13 |
14 | )
15 | })
16 |
17 | export default DisplayMarkdown
18 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayMarkdown/lazy.tsx:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react'
2 |
3 | const DisplayMarkdown = lazy(() =>
4 | // @ts-ignore
5 | import.meta.env.SSR ? import('./stub') : import('.')
6 | )
7 |
8 | export { DisplayMarkdown as default }
9 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayMarkdown/stub.tsx:
--------------------------------------------------------------------------------
1 | import { RCTResponderProps } from '~/components/RenderIOCall'
2 |
3 | export default function DisplayMarkdownStub(
4 | _props: RCTResponderProps<'DISPLAY_MARKDOWN'>
5 | ) {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayObject/DisplayObject.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.Object',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | data: {
15 | isTrue: true,
16 | isFalse: false,
17 | number: 15,
18 | nullValue: null,
19 | nested: {
20 | name: 'Interval',
21 | },
22 | longList: Array(100)
23 | .fill(0)
24 | .map((_, i) => `Item ${i}`),
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayProgressIndeterminate/DisplayProgressIndeterminate.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.Progress.Indeterminate',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Fetching communities...',
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayProgressIndeterminate/index.tsx:
--------------------------------------------------------------------------------
1 | import IVSpinner from '~/components/IVSpinner'
2 | import { RCTResponderProps } from '~/components/RenderIOCall'
3 |
4 | export default function DisplayProgressIndeterminate(
5 | props: RCTResponderProps<'DISPLAY_PROGRESS_INDETERMINATE'>
6 | ) {
7 | return (
8 |
9 |
{props.label}
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayProgressSteps/DisplayProgressSteps.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.ProgressSteps',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Exporting communities',
15 | subTitle: "We're exporting all communities. This may take a while.",
16 | currentStep: 'movement-studio',
17 | steps: {
18 | completed: 1,
19 | total: 3,
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/io-methods/DisplayVideo/DisplayVideo.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Display.Video',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Sample video',
15 | url: 'https://upload.wikimedia.org/wikipedia/commons/a/ad/The_Kid_scenes.ogv',
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/io-methods/InputBoolean/InputBoolean.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Input.Boolean',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Add to mailing list',
15 | onUpdatePendingReturnValue: () => {
16 | /**/
17 | },
18 | onStateChange: () => {
19 | /**/
20 | },
21 | }
22 |
23 | export const WithHelpText = Template.bind({})
24 | WithHelpText.args = {
25 | label: 'Send confirmation email',
26 | helpText: 'Will be delivered to alex@interval.com',
27 | onUpdatePendingReturnValue: () => {
28 | /**/
29 | },
30 | onStateChange: () => {
31 | /**/
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/io-methods/InputBoolean/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import IVCheckbox from '~/components/IVCheckbox'
3 | import { RCTResponderProps } from '~/components/RenderIOCall'
4 | import { IOComponentError } from '~/components/RenderIOCall/ComponentError'
5 |
6 | export default function InputBoolean(
7 | props: RCTResponderProps<'INPUT_BOOLEAN'>
8 | ) {
9 | const [isChecked, setIsChecked] = useState(
10 | !(props.value instanceof IOComponentError) ? props.value : false
11 | )
12 |
13 | const toggleChecked = (value: boolean) => {
14 | setIsChecked(value)
15 | props.onUpdatePendingReturnValue(value)
16 | }
17 |
18 | return (
19 |
20 | toggleChecked(e.target.checked)}
29 | />
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/io-methods/InputEmail/InputEmail.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Input.Email',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Email address',
15 | placeholder: 'you@example.com',
16 | onUpdatePendingReturnValue: () => {
17 | /**/
18 | },
19 | onStateChange: () => {
20 | /**/
21 | },
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/io-methods/InputNumber/InputNumber.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Input.Number',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Enter a number',
15 | onUpdatePendingReturnValue: () => {
16 | /**/
17 | },
18 | onStateChange: () => {
19 | /**/
20 | },
21 | }
22 |
23 | export const MinMax = Template.bind({})
24 | MinMax.args = {
25 | label: 'Enter a number between 1-10',
26 | min: 1,
27 | max: 10,
28 | onUpdatePendingReturnValue: () => {
29 | /**/
30 | },
31 | onStateChange: () => {
32 | /**/
33 | },
34 | }
35 |
36 | export const Money = Template.bind({})
37 | Money.args = {
38 | label: 'Amount',
39 | placeholder: '5',
40 | currency: 'USD',
41 | onUpdatePendingReturnValue: () => {
42 | /**/
43 | },
44 | onStateChange: () => {
45 | /**/
46 | },
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/io-methods/InputRichText/InputRichText.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 | import IVSpinner from '~/components/IVSpinner'
5 |
6 | export default {
7 | title: 'TransactionUI/Input.RichText',
8 | component: Component,
9 | } as Meta
10 |
11 | const Template: StoryFn = args => (
12 |
13 | }>
14 |
15 |
16 |
17 | )
18 |
19 | export const Default = Template.bind({})
20 | Default.parameters = {
21 | docs: {
22 | source: {
23 | code: 'Disabled for this story, see https://github.com/storybookjs/storybook/issues/11554',
24 | },
25 | },
26 | }
27 | Default.args = {
28 | label: 'Email body',
29 | placeholder: 'Hello, world!',
30 | onUpdatePendingReturnValue: () => {
31 | /**/
32 | },
33 | onStateChange: () => {
34 | /**/
35 | },
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/io-methods/InputSlider/InputSlider.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Input.Slider',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | id: 'range',
15 | label: 'Enter a number',
16 | min: 0,
17 | max: 10,
18 | onUpdatePendingReturnValue: () => {
19 | /**/
20 | },
21 | onStateChange: () => {
22 | /**/
23 | },
24 | }
25 |
26 | export const Decimals = Template.bind({})
27 | Decimals.args = {
28 | id: 'range',
29 | label: 'Temperature',
30 | min: 0,
31 | max: 2,
32 | step: 0.1,
33 | helpText:
34 | 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.',
35 | onUpdatePendingReturnValue: () => {
36 | /**/
37 | },
38 | onStateChange: () => {
39 | /**/
40 | },
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/io-methods/ListProgress/ListProgress.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/ListProgress',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Refunds',
15 | items: [
16 | {
17 | label: 'alex@interval.com',
18 | isComplete: true,
19 | resultDescription: 'Refunded $5.00',
20 | },
21 | {
22 | label: 'dan@interval.com',
23 | isComplete: true,
24 | resultDescription: 'Refunded $5.00',
25 | },
26 | {
27 | label: 'jacob@interval.com',
28 | isComplete: false,
29 | resultDescription: null,
30 | },
31 | {
32 | label: 'ryan@interval.com',
33 | isComplete: false,
34 | resultDescription: null,
35 | },
36 | ],
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/io-methods/SelectMultiple/SelectMultiple.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Select.Multiple',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | options: [
15 | { label: 'Create', value: 'create' },
16 | { label: 'Read', value: 'read' },
17 | { label: 'Update', value: 'update' },
18 | { label: 'Delete', value: 'delete' },
19 | ],
20 | onUpdatePendingReturnValue: () => {
21 | /**/
22 | },
23 | onStateChange: () => {
24 | /**/
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/io-methods/UploadFile/UploadFile.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StoryFn, Meta } from '@storybook/react'
3 | import Component from '.'
4 |
5 | export default {
6 | title: 'TransactionUI/Upload.File',
7 | component: Component,
8 | } as Meta
9 |
10 | const Template: StoryFn = args =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | label: 'Upload a file',
15 | onUpdatePendingReturnValue: () => {
16 | /**/
17 | },
18 | onStateChange: () => {
19 | /**/
20 | },
21 | }
22 |
23 | export const Image = Template.bind({})
24 | Image.args = {
25 | label: 'Upload an image',
26 | helpText: 'Mostly any image allowed',
27 | allowedExtensions: ['.jpg', '.jpeg', '.png'],
28 | onUpdatePendingReturnValue: () => {
29 | /**/
30 | },
31 | onStateChange: () => {
32 | /**/
33 | },
34 | }
35 |
--------------------------------------------------------------------------------
/src/emails/action-notification/index.ts:
--------------------------------------------------------------------------------
1 | import emailSender from '../sender'
2 | import { NotificationResult } from '../notification'
3 |
4 | type ActionNotificationMetadata = {
5 | orgSlug: string
6 | actionName: string
7 | transactionId: string
8 | actionRunner: string
9 | createdAt: string
10 | }
11 |
12 | export type ActionNotificationTemplateProps = {
13 | message: string
14 | title?: string
15 | metadata: ActionNotificationMetadata
16 | failedDetails: NotificationResult[]
17 | }
18 |
19 | export default emailSender(
20 | 'action-notification',
21 | props =>
22 | `Interval notification: ${props.title || '' + props.metadata.actionName}`
23 | )
24 |
--------------------------------------------------------------------------------
/src/emails/action-notification/preview.ts:
--------------------------------------------------------------------------------
1 | import sendTemplate from '.'
2 |
3 | export default sendTemplate(
4 | 'accounts@interval.com',
5 | {
6 | message: 'A charge of $10 has been refunded.',
7 | title: 'Refund over threshold',
8 | metadata: {
9 | orgSlug: 'demo',
10 | actionName: 'Refund charge',
11 | transactionId: 'transaction_id',
12 | actionRunner: 'example@interval.com',
13 | createdAt: 'Apr 18, 2022, 11:11 AM',
14 | },
15 | failedDetails: [
16 | {
17 | error: 'No such Slack user in your workspace: @not_a_person',
18 | to: '@not_a_person',
19 | method: 'SLACK',
20 | },
21 | {
22 | error:
23 | 'The Interval app is not installed in this Slack channel: #interval-notifications',
24 | to: '#interval-notifications',
25 | method: 'SLACK',
26 | },
27 | ],
28 | },
29 | { preview: true }
30 | )
31 |
--------------------------------------------------------------------------------
/src/emails/confirm-email/index.ts:
--------------------------------------------------------------------------------
1 | import emailSender from '../sender'
2 |
3 | export type ConfirmEmailTemplateProps = {
4 | confirmUrl: string
5 | isEmailChange: boolean
6 | }
7 |
8 | export default emailSender(
9 | 'confirm-email',
10 | () => `Please confirm your email address on Interval`
11 | )
12 |
--------------------------------------------------------------------------------
/src/emails/confirm-email/preview.ts:
--------------------------------------------------------------------------------
1 | import sendTemplate from '.'
2 |
3 | export default sendTemplate(
4 | 'accounts@interval.com',
5 | {
6 | confirmUrl: 'http://localhost:3000/confirm-email?token=abcd',
7 | isEmailChange: false,
8 | },
9 | { preview: true }
10 | )
11 |
--------------------------------------------------------------------------------
/src/emails/forgot-password/index.ts:
--------------------------------------------------------------------------------
1 | import emailSender from '../sender'
2 |
3 | export type ForgotPasswordTemplateProps = {
4 | resetUrl: string
5 | }
6 |
7 | export default emailSender(
8 | 'forgot-password',
9 | () => 'Password reset request',
10 | { preheader: "We've received a request to reset your password." }
11 | )
12 |
--------------------------------------------------------------------------------
/src/emails/forgot-password/preview.ts:
--------------------------------------------------------------------------------
1 | import sendTemplate from '.'
2 |
3 | export default sendTemplate(
4 | 'alex@interval.com',
5 | {
6 | resetUrl: 'https://interval.com/reset-password?seal=seal123',
7 | },
8 | { preview: true }
9 | )
10 |
--------------------------------------------------------------------------------
/src/emails/index.ts:
--------------------------------------------------------------------------------
1 | export { default as inviteNewUser } from './invite-new-user'
2 | export { default as forgotPassword } from './forgot-password'
3 | export { default as confirmEmail } from './confirm-email'
4 | export { default as actionNotification } from './action-notification'
5 | export { default as emailNotification } from './notification'
6 |
--------------------------------------------------------------------------------
/src/emails/invite-new-user/index.ts:
--------------------------------------------------------------------------------
1 | import emailSender from '../sender'
2 |
3 | export type InviteNewUserTemplateProps = {
4 | organizationName: string
5 | signupUrl: string
6 | preheader: string
7 | }
8 |
9 | export default emailSender(
10 | 'invite-new-user',
11 | props => `${props.organizationName} has invited you to join them on Interval`
12 | )
13 |
--------------------------------------------------------------------------------
/src/emails/invite-new-user/preview.ts:
--------------------------------------------------------------------------------
1 | import sendTemplate from '.'
2 |
3 | export default sendTemplate(
4 | 'accounts@interval.com',
5 | {
6 | organizationName: 'Interval',
7 | signupUrl: 'http://localhost:3000/accept-invitation?token=abcd',
8 | preheader: "You've been invited to join Foo Corp on Interval.",
9 | },
10 | { preview: true }
11 | )
12 |
--------------------------------------------------------------------------------
/src/emails/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "signal": "SIGTERM",
3 | "ext": "hbs,ts",
4 | "exec": "ts-node -r tsconfig-paths/register src/emails/preview-server.ts"
5 | }
6 |
--------------------------------------------------------------------------------
/src/emails/notification/index.ts:
--------------------------------------------------------------------------------
1 | import emailSender from '../sender'
2 | import { NotificationMethod } from '@prisma/client'
3 |
4 | export interface NotificationResult {
5 | method?: NotificationMethod
6 | to: string
7 | error?: string
8 | }
9 |
10 | export type NotificationMetadata = {
11 | orgSlug: string
12 | createdAt: string
13 | }
14 |
15 | export type NotificationTemplateProps = {
16 | message: string
17 | title?: string
18 | metadata: NotificationMetadata
19 | failedDetails: NotificationResult[]
20 | }
21 |
22 | export default emailSender(
23 | 'notification',
24 | props => `Interval notification: ${props.title || '' + props.message}`
25 | )
26 |
--------------------------------------------------------------------------------
/src/emails/notification/preview.ts:
--------------------------------------------------------------------------------
1 | import sendTemplate from '.'
2 |
3 | export default sendTemplate(
4 | 'accounts@interval.com',
5 | {
6 | message: 'A charge of $10 has been refunded.',
7 | title: 'Refund over threshold',
8 | metadata: {
9 | orgSlug: 'demo',
10 | createdAt: 'Apr 18, 2022, 11:11 AM',
11 | },
12 | failedDetails: [
13 | {
14 | error: 'No such Slack user in your workspace: @not_a_person',
15 | to: '@not_a_person',
16 | method: 'SLACK',
17 | },
18 | {
19 | error:
20 | 'The Interval app is not installed in this Slack channel: #interval-notifications',
21 | to: '#interval-notifications',
22 | method: 'SLACK',
23 | },
24 | ],
25 | },
26 | { preview: true }
27 | )
28 |
--------------------------------------------------------------------------------
/src/icons/compiled/AddRow.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const AddRowIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default AddRowIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/BulletedList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const BulletedListIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default BulletedListIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Cancel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CancelIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default CancelIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/CaretDown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CaretDownIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default CaretDownIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Check.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CheckIcon = (props: SVGProps) => (
4 |
13 |
18 |
19 | )
20 | export default CheckIcon
21 |
--------------------------------------------------------------------------------
/src/icons/compiled/CheckCircle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CheckCircleIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default CheckCircleIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/CheckCircleOutline.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CheckCircleOutlineIcon = (props: SVGProps) => (
4 |
12 |
18 |
19 | )
20 | export default CheckCircleOutlineIcon
21 |
--------------------------------------------------------------------------------
/src/icons/compiled/ChevronDown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ChevronDownIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ChevronDownIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/ChevronLeft.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ChevronLeftIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ChevronLeftIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/ChevronRight.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ChevronRightIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ChevronRightIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/ChevronUp.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ChevronUpIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ChevronUpIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/CircledPlay.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CircledPlayIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default CircledPlayIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Clipboard.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ClipboardIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ClipboardIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Close.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CloseIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default CloseIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Code.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CodeIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default CodeIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Copy.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const CopyIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default CopyIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Delete.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const DeleteIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default DeleteIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/DeleteRow.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const DeleteRowIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default DeleteRowIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/DownloadsFolder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const DownloadsFolderIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default DownloadsFolderIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/ErrorCircle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ErrorCircleIcon = (props: SVGProps) => (
4 |
13 |
18 |
19 | )
20 | export default ErrorCircleIcon
21 |
--------------------------------------------------------------------------------
/src/icons/compiled/ExclamationWarn.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ExclamationWarnIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ExclamationWarnIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ExternalLinkIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ExternalLinkIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Eye.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const EyeIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default EyeIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Folder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const FolderIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default FolderIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/FrownFace.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const FrownFaceIcon = (props: SVGProps) => (
4 |
12 |
18 |
19 | )
20 | export default FrownFaceIcon
21 |
--------------------------------------------------------------------------------
/src/icons/compiled/Google.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const GoogleIcon = (props: SVGProps) => (
4 |
11 |
15 |
19 |
23 |
27 |
28 | )
29 | export default GoogleIcon
30 |
--------------------------------------------------------------------------------
/src/icons/compiled/IconSortUp.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const IconSortUpIcon = (props: SVGProps) => (
4 |
10 |
11 |
12 | )
13 | export default IconSortUpIcon
14 |
--------------------------------------------------------------------------------
/src/icons/compiled/Image.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ImageIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ImageIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Info.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const InfoIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default InfoIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Lock.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const LockIcon = (props: SVGProps) => (
4 |
11 |
12 |
23 |
30 |
39 |
40 |
41 | )
42 | export default LockIcon
43 |
--------------------------------------------------------------------------------
/src/icons/compiled/Menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const MenuIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default MenuIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/MinusMini.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const MinusMiniIcon = (props: SVGProps) => (
4 |
12 |
13 |
14 | )
15 | export default MinusMiniIcon
16 |
--------------------------------------------------------------------------------
/src/icons/compiled/More.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const MoreIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default MoreIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Ok.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const OkIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default OkIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Play.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const PlayIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default PlayIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/PlusMini.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const PlusMiniIcon = (props: SVGProps) => (
4 |
12 |
13 |
14 | )
15 | export default PlusMiniIcon
16 |
--------------------------------------------------------------------------------
/src/icons/compiled/QuoteLeft.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const QuoteLeftIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default QuoteLeftIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Redirect.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const RedirectIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default RedirectIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Redo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const RedoIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default RedoIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Refresh.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const RefreshIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default RefreshIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/RightArrow.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const RightArrowIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default RightArrowIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Schedule.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ScheduleIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ScheduleIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Search.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const SearchIcon = (props: SVGProps) => (
4 |
12 |
17 |
18 | )
19 | export default SearchIcon
20 |
--------------------------------------------------------------------------------
/src/icons/compiled/Settings.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const SettingsIcon = (props: SVGProps) => (
4 |
11 |
18 |
19 | )
20 | export default SettingsIcon
21 |
--------------------------------------------------------------------------------
/src/icons/compiled/SortDown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const SortDownIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default SortDownIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/SortUp.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const SortUpIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default SortUpIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Subtract.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const SubtractIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default SubtractIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/TailwindChevronDown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const TailwindChevronDownIcon = (props: SVGProps) => (
4 |
11 |
18 |
19 | )
20 | export default TailwindChevronDownIcon
21 |
--------------------------------------------------------------------------------
/src/icons/compiled/ThumbsDown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ThumbsDownIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ThumbsDownIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/ThumbsUp.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const ThumbsUpIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default ThumbsUpIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/Undo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const UndoIcon = (props: SVGProps) => (
4 |
11 |
12 |
13 | )
14 | export default UndoIcon
15 |
--------------------------------------------------------------------------------
/src/icons/compiled/User.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const UserIcon = (props: SVGProps) => (
4 |
11 |
12 |
21 |
28 |
29 |
30 | )
31 | export default UserIcon
32 |
--------------------------------------------------------------------------------
/src/icons/compiled/XCircle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SVGProps } from 'react'
3 | const XCircleIcon = (props: SVGProps) => (
4 |
11 |
18 |
19 | )
20 | export default XCircleIcon
21 |
--------------------------------------------------------------------------------
/src/icons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "icons": {
3 | "icons8-copy.svg": "Clipboard",
4 | "icons8-secured-letter.svg": "Mail",
5 | "icons8-done.svg": "Check",
6 | "icons8-insert-row-above.svg": "AddRowAbove",
7 | "icons8-high-priority.svg": "ExclamationWarn",
8 | "icons8-medium-icons.svg": "Actions",
9 | "icons8-delivery-time.svg": "Schedule"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/icons/src/actions.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/add-row-above.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/add-row.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/bulleted-list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/cancel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/caret-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/check-circle-outline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/check-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/circled-play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/clipboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/delete-row.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/downloads-folder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/error-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/exclamation-warn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/external-link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/folder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/frown-face.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/google.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/icons/src/image.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/lock.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/menu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/minus-mini.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/ok.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/src/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/plus-mini.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/quote-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/redirect.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/src/redo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/schedule.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/settings.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/slack.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/src/sort-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/icons/src/sort-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/icons/src/sparkling.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/subtract.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/tailwind-chevron-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/src/thumbs-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/thumbs-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/twitter-circled.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/undo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/uploads-folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/src/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/src/x-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/icons/svgr-template.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const template = (variables, { tpl }) => {
4 | // SvgSettings -> SettingsIcon
5 | variables.componentName = `${variables.componentName.slice(3)}Icon`
6 | variables.exports[0].declaration.name = variables.componentName
7 |
8 | return tpl`
9 | ${variables.imports};
10 | ${variables.interfaces};
11 | const ${variables.componentName} = (${variables.props}) => (
12 | ${variables.jsx}
13 | );
14 |
15 | ${variables.exports};
16 | `
17 | }
18 |
19 | module.exports = template
20 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | // import viteSSR from 'vite-ssr/react'
2 | import React from 'react'
3 | import { createRoot } from 'react-dom/client'
4 | import { BrowserRouter } from 'react-router-dom'
5 | import { HelmetProvider } from 'react-helmet-async'
6 | import App from './App'
7 | // import { routes } from './routes'
8 | import './styles/globals.css'
9 | import 'react-loading-skeleton/dist/skeleton.css'
10 | import 'form-request-submit-polyfill'
11 |
12 | declare global {
13 | interface Window {
14 | gtag?: any
15 | }
16 | }
17 |
18 | const container = document.getElementById('root')
19 | if (!container) throw new Error('Missing root element')
20 |
21 | const root = createRoot(container)
22 |
23 | root.render(
24 |
25 |
26 |
27 |
28 |
29 | )
30 |
--------------------------------------------------------------------------------
/src/pages/authentication-confirmed.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export default function AuthenticationConfirmedPage() {
4 | useEffect(() => {
5 | setTimeout(window.close, 1000)
6 | }, [])
7 |
8 | return (
9 |
10 |
11 |
Authentication confirmed
12 |
This page should close automatically momentarily.
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/dashboard/[orgSlug]/[...fallback].tsx:
--------------------------------------------------------------------------------
1 | import NotFound from '~/components/NotFound'
2 |
3 | export default function OrganizationDashboardFallback() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/dashboard/[orgSlug]/account.tsx:
--------------------------------------------------------------------------------
1 | import PageHeading from '~/components/PageHeading'
2 | import AccountSettings from '~/components/AccountSettings'
3 |
4 | export default function AccountSettingsPage() {
5 | return (
6 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/pages/dashboard/[orgSlug]/develop/console/index.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom'
2 | import { useOrgParams } from '~/utils/organization'
3 |
4 | export default function ConsoleRedirect() {
5 | const { orgEnvSlug } = useOrgParams()
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/dashboard/[orgSlug]/develop/index.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom'
2 | import { useHasPermission } from '~/components/DashboardContext'
3 | import { atom } from 'recoil'
4 | import IVSpinner from '~/components/IVSpinner'
5 |
6 | export const hasRecentlyConnectedToConsole = atom({
7 | key: 'hasRecentlyConnectedToConsole',
8 | default: false,
9 | })
10 |
11 | export default function DevelopIndexPage() {
12 | const canDevelop = useHasPermission('RUN_DEV_ACTIONS')
13 | const canCreateKeys = useHasPermission('CREATE_PROD_API_KEYS')
14 |
15 | if (canDevelop === undefined || canCreateKeys === undefined) {
16 | return
17 | }
18 |
19 | return (
20 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/dashboard/[orgSlug]/organization/index.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom'
2 |
3 | export default function OrganizationIndexPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/dashboard/[orgSlug]/transactions/index.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom'
2 | import { useOrgParams } from '~/utils/organization'
3 |
4 | export default function ConsoleRedirect() {
5 | const { orgEnvSlug } = useOrgParams()
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import DashboardLayout from '~/components/DashboardLayout'
2 |
3 | export default function IndexPage() {
4 | // Redirect to default organization handled by DashboardLayout -> DashboardContext
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/develop/GhostModeConsoleLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useEffect } from 'react'
2 | import {
3 | OrganizationProvider,
4 | useOrganization,
5 | } from '~/components/DashboardContext'
6 |
7 | function OrgIdSetter() {
8 | const org = useOrganization()
9 |
10 | const orgId = org.id
11 | useEffect(() => {
12 | if (orgId) {
13 | window.__INTERVAL_ORGANIZATION_ID = orgId
14 | }
15 |
16 | return () => {
17 | window.__INTERVAL_ORGANIZATION_ID = undefined
18 | }
19 | }, [orgId])
20 |
21 | return null
22 | }
23 |
24 | export default function GhostModeConsoleLayout({
25 | children,
26 | }: {
27 | children: ReactNode
28 | }) {
29 | return (
30 |
31 | <>
32 | {children}
33 |
34 | >
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom'
2 |
3 | export default function IndexPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/not-found.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFoundPage() {
2 | return (
3 |
4 |
5 | 404 - Page not found
6 |
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/server/__test__/auth.test.ts:
--------------------------------------------------------------------------------
1 | import { generateKey } from '../auth'
2 |
3 | describe('generateKey', () => {
4 | const user = {
5 | firstName: 'Something-complicated and long',
6 | }
7 |
8 | test('environments', () => {
9 | expect(generateKey(user, 'DEVELOPMENT')).toMatch(/_dev_/)
10 |
11 | expect(generateKey(user, 'PRODUCTION')).toMatch(/^live_/)
12 | })
13 |
14 | test('name prefix cleans name', () => {
15 | expect(generateKey(user, 'DEVELOPMENT')).toMatch(
16 | /somethingcomplicatedandlong_dev_/
17 | )
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/src/server/__test__/user.test.ts:
--------------------------------------------------------------------------------
1 | import { isSlugValid } from '~/utils/validate'
2 | import { generateSlug, getCollisionSafeSlug } from '~/server/utils/slugs'
3 |
4 | describe('generateSlug', () => {
5 | test('strips invalid chars', () => {
6 | const inputs = ['foo', 'foo bar', 'foo~!$/bar']
7 |
8 | for (const input of inputs) {
9 | expect(isSlugValid(generateSlug(input))).toBe(true)
10 | }
11 | })
12 |
13 | test('makes unique', () => {
14 | expect(
15 | getCollisionSafeSlug(generateSlug('slug exists'), [
16 | 'slug-exists',
17 | 'slug-exists-as-prefix',
18 | 'existing',
19 | ])
20 | ).toBe('slug-exists-3')
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/server/api/auth/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import ghostRouter from './ghost'
3 | import loginRoute from './login'
4 | import logoutRoute from './logout'
5 | import resetPasswordRoute from './reset'
6 | import sessionRoute from './session'
7 | import ssoRouter from './sso'
8 | import oauthRouter from './oauth'
9 |
10 | const router = express.Router()
11 |
12 | router.post('/login', loginRoute)
13 | router.get('/session', sessionRoute)
14 | router.post('/reset', resetPasswordRoute)
15 | router.post('/logout', logoutRoute)
16 | router.use('/sso', ssoRouter)
17 | router.use('/oauth', oauthRouter)
18 | router.use('/ghost', ghostRouter)
19 |
20 | export default router
21 |
--------------------------------------------------------------------------------
/src/server/api/auth/logout.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from 'express'
2 | import { logoutSession } from '~/server/auth'
3 |
4 | export default async function logoutRoute(req: Request, res: Response) {
5 | if (req.session.session) {
6 | await logoutSession(req.session.session.id)
7 | }
8 | req.session.destroy()
9 | res.status(200).send(true)
10 | }
11 |
--------------------------------------------------------------------------------
/src/server/api/auth/oauth/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import slack from './slack'
3 |
4 | const router = express.Router()
5 |
6 | router.get('/slack', slack)
7 |
8 | export default router
9 |
--------------------------------------------------------------------------------
/src/server/api/auth/sso/auth.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from 'express'
2 | import env from '~/env'
3 | import { workos, isWorkOSEnabled, REDIRECT_URI } from '.'
4 | import { logger } from '~/server/utils/logger'
5 |
6 | export default async function auth(req: Request, res: Response) {
7 | if (!isWorkOSEnabled || !workos || !env.WORKOS_CLIENT_ID) {
8 | logger.error('WorkOS credentials not found, aborting', {
9 | path: req.path,
10 | })
11 | return res.sendStatus(501)
12 | }
13 |
14 | const { workosOrganizationId, transactionId } = req.query
15 |
16 | if (!workosOrganizationId || typeof workosOrganizationId !== 'string') {
17 | res.status(400).end()
18 | return
19 | }
20 |
21 | const authorizationURL = workos.sso.getAuthorizationURL({
22 | redirectURI: REDIRECT_URI,
23 | clientID: env.WORKOS_CLIENT_ID,
24 | organization: workosOrganizationId,
25 | state: transactionId
26 | ? JSON.stringify({ transactionId: String(transactionId) })
27 | : undefined,
28 | })
29 |
30 | res.redirect(authorizationURL)
31 | return
32 | }
33 |
--------------------------------------------------------------------------------
/src/server/api/auth/sso/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import auth from './auth'
3 | import callback from './callback'
4 | import signInWithGoogle from './sign-in-with-google'
5 | import env from '~/env'
6 |
7 | export { workos, isWorkOSEnabled } from '~/server/auth'
8 | export const REDIRECT_URI = `${env.APP_URL}/api/auth/sso/callback`
9 |
10 | const router = express.Router()
11 |
12 | router.get('/sign-in-with-google', signInWithGoogle)
13 | router.get('/callback', callback)
14 | router.get('/auth', auth)
15 |
16 | export default router
17 |
--------------------------------------------------------------------------------
/src/server/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 | import { logger } from './utils/logger'
3 | import env from '~/env'
4 |
5 | declare global {
6 | // eslint-disable-next-line no-var
7 | var prisma: PrismaClient | undefined
8 | }
9 |
10 | const prisma =
11 | global.prisma ??
12 | new PrismaClient({
13 | datasources: {
14 | db: {
15 | url: env.DATABASE_URL,
16 | },
17 | },
18 | })
19 |
20 | try {
21 | //@ts-ignore undocumented prisma field
22 | const dbUrlString: string = prisma._engine.config.overrideDatasources.db.url
23 | const dbUrl = new URL(dbUrlString)
24 | logger.info(`[Prisma] Connecting to database as user: ${dbUrl.username}`)
25 | if (dbUrl.host.includes('interval2-prod-do-user-860008')) {
26 | logger.info(`[Prisma] 🚨 Connecting to prod DB 🚨`)
27 | }
28 | } catch (e) {
29 | logger.info('Failed to determine Prisma URL')
30 | }
31 |
32 | if (process.env.NODE_ENV !== 'production') {
33 | global.prisma = prisma
34 | }
35 |
36 | export default prisma
37 |
--------------------------------------------------------------------------------
/src/server/proxyServerForTesting.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This emulates the development proxy setup to facilitate testing the
3 | * compiled JavaScript code in a non-production environment.
4 | */
5 |
6 | import http from 'http'
7 | import httpProxy from 'http-proxy'
8 | import '.'
9 | import '../wss'
10 | import { logger } from './utils/logger'
11 |
12 | if (process.env.NODE_ENV !== 'production') {
13 | const proxy = httpProxy.createProxyServer({})
14 |
15 | const server = http.createServer((req, res) => {
16 | if (!req.url) return
17 |
18 | proxy.web(
19 | req,
20 | res,
21 | {
22 | target: 'http://localhost:3001',
23 | },
24 | err => {
25 | logger.error('Failed proxying', err)
26 | res.end()
27 | }
28 | )
29 | })
30 |
31 | server.on('upgrade', (req, socket, head) => {
32 | proxy.ws(req, socket, head, {
33 | target: 'ws://localhost:3002',
34 | })
35 | })
36 |
37 | server.listen(3000)
38 | }
39 |
--------------------------------------------------------------------------------
/src/server/trpc/actionGroup.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 | import {
3 | createRouter,
4 | authenticatedMiddleware,
5 | organizationMiddleware,
6 | } from './util'
7 | import { TRPCError } from '@trpc/server'
8 |
9 | export const actionGroupRouter = createRouter()
10 | .middleware(authenticatedMiddleware)
11 | .middleware(organizationMiddleware)
12 | .query('one', {
13 | input: z.object({
14 | groupSlug: z.string(),
15 | mode: z.enum(['live', 'console']).default('live'),
16 | }),
17 | async resolve({
18 | ctx: { prisma, user, organizationId, organizationEnvironmentId },
19 | input: { groupSlug, mode },
20 | }) {
21 | const app = await prisma.actionGroup.findFirst({
22 | where: {
23 | slug: groupSlug,
24 | developerId: mode === 'console' ? user.id : null,
25 | organizationId,
26 | organizationEnvironmentId,
27 | },
28 | })
29 |
30 | if (!app) {
31 | throw new TRPCError({ code: 'NOT_FOUND' })
32 | }
33 |
34 | return app
35 | },
36 | })
37 |
--------------------------------------------------------------------------------
/src/server/utils/actionSchedule.ts:
--------------------------------------------------------------------------------
1 | import * as cron from 'node-cron'
2 | import { ActionSchedule } from '@prisma/client'
3 | import {
4 | CronSchedule,
5 | cronScheduleToString,
6 | toCronSchedule,
7 | ScheduleInput,
8 | } from '~/utils/actionSchedule'
9 | import { ActionWithPossibleMetadata } from '~/utils/types'
10 | import { makeApiCall } from './wss'
11 |
12 | export function isInputValid(input: ScheduleInput): boolean {
13 | const schedule = toCronSchedule(input)
14 | if (!schedule) return false
15 |
16 | return cron.validate(cronScheduleToString(schedule))
17 | }
18 |
19 | export function isValid(schedule: CronSchedule): boolean {
20 | return cron.validate(cronScheduleToString(schedule))
21 | }
22 |
23 | export async function syncActionSchedules(
24 | action: ActionWithPossibleMetadata & { schedules?: ActionSchedule[] },
25 | inputs: ScheduleInput[]
26 | ) {
27 | return makeApiCall(
28 | '/api/action-schedules/sync',
29 | JSON.stringify({
30 | actionId: action.id,
31 | inputs,
32 | })
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/server/utils/email.ts:
--------------------------------------------------------------------------------
1 | import env from "env";
2 |
3 | export const isEmailEnabled = () => !!env.POSTMARK_API_KEY && !!env.EMAIL_FROM
4 |
--------------------------------------------------------------------------------
/src/server/utils/featureFlags.ts:
--------------------------------------------------------------------------------
1 | import { ConfiguredFeatureFlag } from '@prisma/client'
2 | import {
3 | isFeatureFlagEnabled,
4 | OrganizationWithFlags,
5 | } from '~/utils/featureFlags'
6 | import prisma from '../prisma'
7 |
8 | export async function isFlagEnabled(
9 | flagToCheck: ConfiguredFeatureFlag,
10 | organizationId?: string
11 | ): Promise {
12 | const globalFeatureFlags = await prisma.globalFeatureFlag.findMany({
13 | where: {
14 | enabled: true,
15 | },
16 | })
17 |
18 | let organization: OrganizationWithFlags | undefined | null
19 |
20 | if (organizationId) {
21 | organization = await prisma.organization.findUnique({
22 | where: {
23 | id: organizationId,
24 | },
25 | include: {
26 | featureFlags: true,
27 | },
28 | })
29 | }
30 |
31 | return isFeatureFlagEnabled(flagToCheck, {
32 | globalFeatureFlags,
33 | organization,
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/src/server/utils/hash.ts:
--------------------------------------------------------------------------------
1 | import { BinaryToTextEncoding, createHash } from 'node:crypto'
2 |
3 | /**
4 | * Should go without saying, but don't use this for anything security related.
5 | */
6 | export function shaHash(
7 | input: string,
8 | encoding: BinaryToTextEncoding = 'hex'
9 | ): string {
10 | return createHash('sha256').update(input).digest(encoding)
11 | }
12 |
--------------------------------------------------------------------------------
/src/server/utils/routes.ts:
--------------------------------------------------------------------------------
1 | import glob from 'glob'
2 | import { getDashboardL1Paths } from '~/utils/routes'
3 |
4 | // Should keep these in sync with those in `src/App.tsx`; they must be literals there.
5 | export const ROUTES_GLOB = 'src/pages/**/[a-z[]*.{tsx,mdx}'
6 | export const DASHBOARD_ROUTES_GLOB =
7 | 'src/pages/dashboard/\\[orgSlug\\]/*/**/[a-z]*.{tsx,mdx}'
8 |
9 | const DASHBOARD_ROUTES = glob.sync(DASHBOARD_ROUTES_GLOB)
10 | export const dashboardL1Paths = getDashboardL1Paths(DASHBOARD_ROUTES)
11 |
--------------------------------------------------------------------------------
/src/server/utils/sdkAlerts.ts:
--------------------------------------------------------------------------------
1 | import prisma from '~/server/prisma'
2 | import { SdkAlert } from '@prisma/client'
3 |
4 | export async function getSdkAlert(
5 | sdkName: string,
6 | sdkVersion: string
7 | ): Promise {
8 | return prisma.sdkAlert.findFirst({
9 | where: {
10 | sdkName,
11 | minSdkVersion: {
12 | gt: sdkVersion,
13 | },
14 | },
15 | orderBy: [
16 | {
17 | severity: 'desc',
18 | },
19 | {
20 | minSdkVersion: 'desc',
21 | },
22 | ],
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/server/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
2 |
3 | export default sleep
4 |
--------------------------------------------------------------------------------
/src/server/utils/wss.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'cross-fetch'
2 |
3 | import env from '~/env'
4 | import { encryptPassword } from '../auth'
5 | import { port } from '~/wss/consts'
6 |
7 | export async function makeApiCall(
8 | path: string,
9 | body: string
10 | ): Promise {
11 | // TODO: Use correct URL if not on same server
12 | const url = new URL('http://localhost')
13 | url.port = port.toString()
14 | url.pathname = path
15 |
16 | return fetch(`${url.toString()}`, {
17 | method: 'POST',
18 | headers: {
19 | 'Content-Type': 'application/json',
20 | Authorization: `Bearer ${encryptPassword(env.WSS_API_SECRET)}`,
21 | },
22 | body,
23 | }).then(response => {
24 | if (!response.ok) {
25 | // TODO: Make this better
26 | throw new Error(response.status.toString())
27 | }
28 |
29 | return response
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/IOComponentError.ts:
--------------------------------------------------------------------------------
1 | export class IOComponentError extends Error {
2 | constructor(message?: string) {
3 | super(message ?? 'This field is required.')
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/__test__/slug.test.ts:
--------------------------------------------------------------------------------
1 | import { generateSlug } from '~/server/utils/slugs'
2 |
3 | describe('generateSlug', () => {
4 | const cases = {
5 | Word: 'word',
6 | 'Basic words': 'basic-words',
7 | 'Double -- hyphen': 'double--hyphen',
8 | 'has "double quotes"': 'has-double-quotes',
9 | "has 'single quotes'": 'has-single-quotes',
10 | '- Starts with hyphen': 'starts-with-hyphen',
11 | 'Ends-with-hyphen-': 'ends-with-hyphen',
12 | 'More + Advanced thing :)': 'more-advanced-thing',
13 | }
14 |
15 | for (const [start, end] of Object.entries(cases)) {
16 | test(start, () => {
17 | expect(generateSlug(start)).toBe(end)
18 | })
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/utils/__test__/validate.test.ts:
--------------------------------------------------------------------------------
1 | import { isSlugValid } from '../validate'
2 |
3 | describe('isSlugValid', () => {
4 | const valid = [
5 | 'CamelCase',
6 | 'camelCase',
7 | 'snake_case',
8 | 'SCREAMING_SNAKE_CASE',
9 | 'kebab-case',
10 | 'Camel-Kebab-Case',
11 | '__dunder__separated__',
12 | 'period.separated',
13 | 'Period.Separated',
14 | ]
15 |
16 | const invalid = [
17 | 'with spaces',
18 | 'with+plus',
19 | 'with:colon',
20 | 'with_exclamation!',
21 | ]
22 |
23 | for (const slug of valid) {
24 | test(slug, () => {
25 | expect(isSlugValid(slug)).toBe(true)
26 | })
27 | }
28 |
29 | for (const slug of invalid) {
30 | test(slug, () => {
31 | expect(isSlugValid(slug)).toBe(false)
32 | })
33 | }
34 | })
35 |
--------------------------------------------------------------------------------
/src/utils/currency.ts:
--------------------------------------------------------------------------------
1 | import { CURRENCIES, CurrencyCode } from '@interval/sdk/dist/ioSchema'
2 |
3 | export { CURRENCIES }
4 | export type { CurrencyCode }
5 |
6 | const CURRENCY_SYMBOLS: Record, string> = {
7 | USD: '$',
8 | CAD: '$',
9 | AUD: '$',
10 | EUR: '€',
11 | GBP: '£',
12 | CNY: '¥',
13 | JPY: '¥',
14 | }
15 |
16 | export function getCurrencySymbol(code: CurrencyCode): string | undefined {
17 | return CURRENCY_SYMBOLS[code]
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/deserialize.ts:
--------------------------------------------------------------------------------
1 | import { UsageEnvironment } from '@prisma/client'
2 | export * from '@interval/sdk/dist/utils/deserialize'
3 |
4 | export function getUsageEnvironment(
5 | env: string | undefined | null
6 | ): UsageEnvironment {
7 | return env?.toUpperCase() === 'DEVELOPMENT'
8 | ? 'DEVELOPMENT'
9 | : ('PRODUCTION' as UsageEnvironment)
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/email.ts:
--------------------------------------------------------------------------------
1 | export { isEmail } from './validate'
2 |
3 | export function getDomain(email: string): string | undefined {
4 | const index = email.indexOf('@')
5 | if (index < 0) return undefined
6 |
7 | return email.substring(index + 1)
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/examples.ts:
--------------------------------------------------------------------------------
1 | // Duplicated from docs/examplesSidebar.json
2 | export const examples = [
3 | {
4 | id: 'refund-charges',
5 | label: 'Refund tool',
6 | },
7 | {
8 | id: 'user-settings',
9 | label: 'User settings form',
10 | },
11 | {
12 | id: 'qr-codes',
13 | label: 'QR code generator',
14 | },
15 | {
16 | id: 'account-migration',
17 | label: 'Account migration tool',
18 | },
19 | {
20 | id: 'github-issue-editor',
21 | label: 'Bulk GitHub issue editor',
22 | },
23 | {
24 | id: 'metrics-notifier',
25 | label: 'Metrics notifier',
26 | },
27 | {
28 | id: 'web-screenshot-comparison',
29 | label: 'Compare web pages',
30 | },
31 | ]
32 |
--------------------------------------------------------------------------------
/src/utils/extractOrgSlug.ts:
--------------------------------------------------------------------------------
1 | export function extractOrgSlug({ orgSlug: slug }: { orgSlug?: string }): {
2 | orgSlug?: string
3 | envSlug?: string
4 | orgEnvSlug?: string
5 | isDevMode?: boolean
6 | } {
7 | if (!slug) return {}
8 |
9 | const orgEnvSlug = slug
10 |
11 | const isDevMode = location.pathname.startsWith(
12 | `/dashboard/${orgEnvSlug}/develop/actions`
13 | )
14 |
15 | if (slug.includes('+')) {
16 | const [orgSlug, envSlug] = slug.split('+')
17 |
18 | return {
19 | orgSlug,
20 | envSlug,
21 | isDevMode,
22 | orgEnvSlug,
23 | }
24 | }
25 |
26 | return {
27 | orgSlug: slug,
28 | orgEnvSlug,
29 | isDevMode,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/getQueuedActionId.ts:
--------------------------------------------------------------------------------
1 | import type { Location } from 'react-router-dom'
2 |
3 | /**
4 | * Use history state to get queuedActionId to not clobber any
5 | * user-set search params.
6 | */
7 | export function getQueuedActionId(location: Location): string | undefined {
8 | const state = location.state
9 | if (
10 | state &&
11 | typeof state === 'object' &&
12 | !Array.isArray(state) &&
13 | 'queuedActionId' in state
14 | ) {
15 | const newState = state as { queuedActionId: unknown }
16 | if (typeof newState.queuedActionId === 'string') {
17 | return newState.queuedActionId
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/getTextWidth.ts:
--------------------------------------------------------------------------------
1 | // based on https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript
2 | function getStyle(element: HTMLElement, prop: string) {
3 | return window.getComputedStyle(element, null).getPropertyValue(prop)
4 | }
5 |
6 | function getCanvasFontSize(el = document.body) {
7 | const fontWeight = getStyle(el, 'font-weight') || 'normal'
8 | const fontSize = getStyle(el, 'font-size') || '16px'
9 | const fontFamily = getStyle(el, 'font-family') || 'Helvetica'
10 |
11 | return `${fontWeight} ${fontSize} ${fontFamily}`
12 | }
13 |
14 | export default function getTextWidth(text: string, el?: HTMLElement) {
15 | const canvas = document.createElement('canvas')
16 | const context = canvas.getContext('2d')
17 | if (!context) return 0
18 | context.font = getCanvasFontSize(el || document.body)
19 | const metrics = context.measureText(text)
20 | return metrics.width
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/isomorphicConsts.ts:
--------------------------------------------------------------------------------
1 | // consts that can safely be imported in the browser or server (eg. not secrets)
2 | export const AUTH_COOKIE_NAME = 'interval_auth_cookie'
3 | export const REFERRAL_LOCAL_STORAGE_KEY = '__INTERVAL_REFERRAL_INFO'
4 | export const ME_LOCAL_STORAGE_KEY = '__INTERVAL_ME'
5 | export const INTERVAL_USAGE_ENVIRONMENT = '__INTERVAL_USAGE_ENVIRONMENT'
6 | export const TRANSACTION_ID_SEARCH_PARAM_KEY = '__INTERVAL_TRANSACTION_ID'
7 | export const SLACK_OAUTH_SCOPES =
8 | 'im:write,chat:write,channels:read,groups:read,users:read,users:read.email'
9 | export const CLIENT_ISOCKET_ID_SEARCH_PARAM_KEY = '__INTERVAL_CLIENT_ISOCKET_ID'
10 |
11 | export const NODE_SDK_NAME = '@interval/sdk'
12 | export const PYTHON_SDK_NAME = 'interval-py'
13 |
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | import logger from 'loglevel'
2 |
3 | logger.setDefaultLevel('warn')
4 |
5 | export { logger }
6 |
--------------------------------------------------------------------------------
/src/utils/notify.ts:
--------------------------------------------------------------------------------
1 | import { TransactionResultStatus } from '@prisma/client'
2 |
3 | export function completionTitle(resultStatus: TransactionResultStatus): string {
4 | switch (resultStatus) {
5 | case 'SUCCESS':
6 | case 'REDIRECTED':
7 | return 'Transaction completed: Success ✅'
8 | case 'FAILURE':
9 | return 'Transaction completed: Failure ❌'
10 | case 'CANCELED':
11 | return 'Transaction canceled.'
12 | }
13 | }
14 |
15 | export function completionMessage(
16 | resultStatus: TransactionResultStatus,
17 | actionName = 'An action'
18 | ): string {
19 | switch (resultStatus) {
20 | case 'SUCCESS':
21 | case 'REDIRECTED':
22 | return `${actionName} has completed successfully, see the transaction history for more information.`
23 | case 'FAILURE':
24 | return `${actionName} has failed, see the transaction history for more information.`
25 | case 'CANCELED':
26 | return `A transaction for ${actionName} has been canceled.`
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/number.ts:
--------------------------------------------------------------------------------
1 | import { ImageSize } from '@interval/sdk/dist/ioSchema'
2 |
3 | // Keep in sync with tailwind.config.js
4 | export function imageSizeToPx(
5 | imageSize: ImageSize,
6 | context?: 'dropdown'
7 | ): number {
8 | if (context === 'dropdown') {
9 | switch (imageSize) {
10 | case 'thumbnail':
11 | return 32
12 | case 'small':
13 | return 48
14 | case 'medium':
15 | return 80
16 | case 'large':
17 | return 128
18 | }
19 | }
20 |
21 | switch (imageSize) {
22 | case 'thumbnail':
23 | return 64
24 | case 'small':
25 | return 128
26 | case 'medium':
27 | return 256
28 | case 'large':
29 | return 512
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/organization.ts:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router-dom'
2 | import { extractOrgSlug } from './extractOrgSlug'
3 |
4 | /**
5 | * A thin wrapper around useParams that extracts orgSlug + orgEnvSlug if provided, requires that orgSlug exists in URL
6 | */
7 | export function useOrgParams() {
8 | const params = useParams()
9 |
10 | const { orgSlug, envSlug, orgEnvSlug, isDevMode } = extractOrgSlug(params)
11 |
12 | if (!orgSlug)
13 | throw new Error('useOrgParams: could not find orgSlug in params')
14 |
15 | if (!orgEnvSlug)
16 | throw new Error('useOrgParams: could not find orgEnvSlug in params')
17 |
18 | const basePath = `/dashboard/${orgEnvSlug}/${
19 | isDevMode ? 'develop/actions' : 'actions'
20 | }`
21 |
22 | return {
23 | ...params,
24 | orgSlug,
25 | envSlug,
26 | orgEnvSlug,
27 | basePath,
28 | isDevMode,
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/parseActionResult.ts:
--------------------------------------------------------------------------------
1 | import { ParsedActionResultSchema } from '@interval/sdk/dist/ioSchema'
2 |
3 | /**
4 | * Parses the JSON result of a transaction into a more structured format.
5 | */
6 | export function parseActionResult(
7 | res: string | undefined
8 | ): ParsedActionResultSchema {
9 | if (!res) {
10 | return {
11 | schemaVersion: 0,
12 | status: 'SUCCESS',
13 | data: null,
14 | meta: null,
15 | }
16 | }
17 |
18 | return JSON.parse(res)
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/preventDefaultInputEnter.ts:
--------------------------------------------------------------------------------
1 | export function preventDefaultInputEnterKey(event: React.KeyboardEvent) {
2 | if (event.key === 'Enter') {
3 | event.preventDefault()
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/queuedActions.ts:
--------------------------------------------------------------------------------
1 | import { DeserializableRecord } from '@interval/sdk/dist/ioSchema'
2 | import { Prisma } from '@prisma/client'
3 |
4 | export function getQueuedActionParams(
5 | params: Prisma.JsonValue | undefined | null
6 | ): DeserializableRecord | undefined {
7 | if (!params || typeof params !== 'object' || Array.isArray(params)) return
8 |
9 | const record: DeserializableRecord = {}
10 |
11 | for (const [key, val] of Object.entries(params as Prisma.JsonObject)) {
12 | if (typeof key === 'string' && (!val || typeof val !== 'object')) {
13 | record[key] = val
14 | }
15 | }
16 |
17 | return record
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/referralSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const referralInfoSchema = z
4 | .object({
5 | referrer: z.string().nullish(),
6 | utmSource: z.string().nullish(),
7 | utmMedium: z.string().nullish(),
8 | utmCampaign: z.string().nullish(),
9 | utmTerm: z.string().nullish(),
10 | utmContent: z.string().nullish(),
11 | })
12 | .optional()
13 |
14 | export type ReferralInfo = z.infer
15 |
--------------------------------------------------------------------------------
/src/utils/routes.ts:
--------------------------------------------------------------------------------
1 | function cleanPath(path: string): string {
2 | return path
3 | .replace(/src|.?\/pages|index|\.(tsx|mdx)$/g, '')
4 | .replace(/\[\.{3}.+\]/, '*')
5 | .replace(/\[([A-Za-z_]+)\]/g, ':$1')
6 | }
7 |
8 | export function getDashboardL1Paths(paths: string[]): Set {
9 | return new Set(
10 | paths
11 | .map(route => {
12 | const path = cleanPath(route)
13 | .replace('/dashboard/:orgSlug/', '')
14 | .split('/')[0]
15 |
16 | return path
17 | })
18 | .filter(Boolean)
19 | )
20 | }
21 |
22 | export function getDashboardL0Paths(paths: string[]): Set {
23 | return new Set(
24 | paths
25 | .filter(path => !path.includes('[orgSlug]'))
26 | .map(route => {
27 | const path = cleanPath(route).replace('/dashboard/', '').split('/')[0]
28 |
29 | return path
30 | })
31 | .filter(Boolean)
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/sessionStorage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export function useSessionStorage(
4 | key: string,
5 | defaultValue?: T
6 | ): [string | undefined, (val: T) => void] {
7 | const [value, setValue] = useState(defaultValue)
8 |
9 | useEffect(() => {
10 | if (typeof window !== 'undefined') {
11 | const val = window.sessionStorage.getItem(key)
12 |
13 | if (val) {
14 | setValue(val)
15 | } else {
16 | setValue(defaultValue)
17 | }
18 | }
19 | }, [key, defaultValue])
20 |
21 | useEffect(() => {
22 | if (typeof window !== 'undefined') {
23 | if (value) {
24 | window.sessionStorage.setItem(key, value)
25 | } else {
26 | window.sessionStorage.removeItem(key)
27 | }
28 | }
29 | }, [key, value])
30 |
31 | return [value, setValue]
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/stringify.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Mostly a wrapper around JSON.stringify that excludes wrapping quotes from strings.
3 | */
4 | export default function stringify(
5 | value:
6 | | string
7 | | number
8 | | boolean
9 | | null
10 | | undefined
11 | | Record
12 | | bigint
13 | | Date
14 | ): string {
15 | if (typeof value === 'string') {
16 | return value
17 | }
18 |
19 | if (typeof value === 'bigint') {
20 | return `${value}n`
21 | }
22 |
23 | if (value instanceof Date) {
24 | return value.toISOString()
25 | }
26 |
27 | return JSON.stringify(value)
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/superjson.ts:
--------------------------------------------------------------------------------
1 | import superjson from 'superjson'
2 |
3 | // Custom transformer for python time strings (HH:MM:SS)
4 | superjson.registerCustom(
5 | {
6 | isApplicable: (v: string): v is string =>
7 | typeof v === 'string' && /^\d{2}:\d{2}:\d{2}$/.test(v),
8 | serialize: v => String(v),
9 | deserialize: v => String(v),
10 | },
11 | 'time'
12 | )
13 |
14 | export default superjson
15 |
--------------------------------------------------------------------------------
/src/utils/throttle.ts:
--------------------------------------------------------------------------------
1 | export default function throttle(
2 | callback: (...args: Args) => R,
3 | timeoutMs: number
4 | ): (...args: Args) => R {
5 | let inThrottle = false
6 | let prevValue: R | undefined
7 |
8 | return (...args: Args) => {
9 | if (inThrottle) return prevValue as R
10 |
11 | inThrottle = true
12 | prevValue = callback(...args)
13 | setTimeout(() => {
14 | inThrottle = false
15 | }, timeoutMs)
16 | return prevValue as R
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/trpc/utils.ts:
--------------------------------------------------------------------------------
1 | import superjson from '~/utils/superjson'
2 | import devalue from 'devalue'
3 |
4 | // Use superjson for client -> server because it's safe
5 | // Use devalue for server -> client because it's fast
6 | // https://trpc.io/docs/data-transformers#different-transformers-for-upload-and-download
7 | export const transformer = {
8 | input: superjson,
9 | output: {
10 | serialize: d => devalue(d),
11 | deserialize: d => eval(`(${d})`),
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/uniqueId.ts:
--------------------------------------------------------------------------------
1 | // create basic globally unique IDs - helpful for autogenerated inputs, etc.
2 | // inspired by https://github.com/jamiebuilds/gud
3 |
4 | const key = '__iv_global_unique_id__'
5 | const globalObject = typeof window !== 'undefined' ? window : global
6 |
7 | export default function uniqueId() {
8 | return (globalObject[key] = (globalObject[key] || 0) + 1)
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/uploads.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { trpc } from './trpc'
3 |
4 | export function useUploadUrlFetcher({
5 | transactionId,
6 | inputGroupKey,
7 | }: {
8 | transactionId?: string
9 | inputGroupKey?: string
10 | }) {
11 | const { mutateAsync: getUrls } = trpc.useMutation(['uploads.io.urls'])
12 |
13 | const getUploadUrls = useCallback(
14 | async ({ objectKeys }: { objectKeys: string[] }) => {
15 | if (!transactionId || !inputGroupKey) {
16 | return
17 | }
18 |
19 | return getUrls({
20 | objectKeys,
21 | transactionId,
22 | inputGroupKey,
23 | })
24 | },
25 | [getUrls, inputGroupKey, transactionId]
26 | )
27 |
28 | return getUploadUrls
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | import urlRegex from 'url-regex-safe'
2 | import tlds from 'tlds'
3 |
4 | export function getURLsFromString(str: string) {
5 | return str.match(
6 | urlRegex({
7 | // URLs must start with a valid protocol or www
8 | strict: true,
9 | tlds: [...tlds, 'test'],
10 | })
11 | )
12 | }
13 |
14 | export function isUrl(str: string): boolean {
15 | return urlRegex({ exact: true, tlds: [...tlds, 'test'] }).test(str)
16 | }
17 |
18 | export function getBackPath(url: string): string {
19 | if (url.endsWith('/')) {
20 | url = url.substring(0, url.length - 1)
21 | }
22 |
23 | return url.substring(0, url.lastIndexOf('/'))
24 | }
25 |
26 | export function getCurrentPath() {
27 | if (typeof window === 'undefined') return undefined
28 |
29 | let path = window.location.pathname
30 |
31 | if (window.location.search) {
32 | path += window.location.search
33 | }
34 |
35 | if (window.location.hash) {
36 | path += window.location.hash
37 | }
38 |
39 | return path
40 | }
41 |
--------------------------------------------------------------------------------
/src/utils/useActionUrlBuilder.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { useOrgParams } from './organization'
3 | import type { ActionMode } from './types'
4 | import { getActionUrl } from './actions'
5 |
6 | export function useActionUrlBuilder(mode: ActionMode | 'anon-console') {
7 | const { orgEnvSlug } = useOrgParams()
8 |
9 | const actionUrlBuilder = useCallback(
10 | params => {
11 | return getActionUrl({
12 | ...params,
13 | orgEnvSlug,
14 | mode,
15 | })
16 | },
17 | [orgEnvSlug, mode]
18 | )
19 |
20 | return actionUrlBuilder
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/useAnchorScroll.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from 'react'
2 | import { useLocation } from 'react-router-dom'
3 |
4 | export default function useAnchorScroll() {
5 | const location = useLocation()
6 |
7 | useLayoutEffect(() => {
8 | const element = document.getElementById(location.hash.replace('#', ''))
9 |
10 | if (!element) return
11 |
12 | window.scrollTo({ top: element.offsetTop })
13 | }, [location])
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/useBackPath.ts:
--------------------------------------------------------------------------------
1 | import { useLocation } from 'react-router-dom'
2 | import { getBackPath } from './url'
3 |
4 | export default function useBackPath() {
5 | const location = useLocation()
6 | if (
7 | location.state &&
8 | typeof location.state === 'object' &&
9 | 'backPath' in location.state &&
10 | typeof location.state['backPath'] === 'string'
11 | ) {
12 | return location.state['backPath']
13 | }
14 |
15 | return getBackPath(location.pathname)
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | export default function useDebounce(value: T, delay: number) {
4 | const [debouncedValue, setDebouncedValue] = useState(value)
5 |
6 | useEffect(() => {
7 | const handler = window.setTimeout(() => {
8 | setDebouncedValue(value)
9 | }, delay)
10 |
11 | return () => {
12 | window.clearTimeout(handler)
13 | }
14 | }, [value, delay])
15 |
16 | return debouncedValue
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/useDisableSelectionWithShiftKey.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | export default function useDisableSelectionWithShiftKey() {
4 | const [isSelectingRange, setIsSelectingRange] = useState(false)
5 |
6 | useEffect(() => {
7 | function toggleIsSelectingRange(e: KeyboardEvent) {
8 | setIsSelectingRange(e.shiftKey)
9 | }
10 |
11 | document.addEventListener('keydown', toggleIsSelectingRange)
12 | document.addEventListener('keyup', toggleIsSelectingRange)
13 |
14 | return () => {
15 | document.removeEventListener('keydown', toggleIsSelectingRange)
16 | document.removeEventListener('keyup', toggleIsSelectingRange)
17 | }
18 | }, [])
19 |
20 | return isSelectingRange
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/useImageLoaded.ts:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react'
2 |
3 | export default function useImageLoaded() {
4 | const [imageLoaded, setImageLoaded] = useState(false)
5 | const imgRef = useRef(null)
6 |
7 | useEffect(() => {
8 | const img = imgRef.current
9 |
10 | if (!img) return
11 |
12 | const handleLoad = () => setImageLoaded(true)
13 | img.addEventListener('load', handleLoad)
14 | return () => img.removeEventListener('load', handleLoad)
15 | }, [])
16 |
17 | return { imageLoaded, imgRef }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/useInterval.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react'
2 |
3 | type CallbackFn = () => void
4 |
5 | export default function useInterval(callback: CallbackFn, delay: number) {
6 | const savedCallback = useRef()
7 |
8 | useEffect(() => {
9 | savedCallback.current = callback
10 | })
11 |
12 | useEffect(() => {
13 | function tick() {
14 | if (savedCallback.current !== undefined) {
15 | savedCallback.current()
16 | }
17 | }
18 |
19 | const id = setInterval(tick, delay)
20 |
21 | // run immediately
22 | tick()
23 |
24 | return () => clearInterval(id)
25 | }, [delay])
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/useIsFeatureEnabled.ts:
--------------------------------------------------------------------------------
1 | import { ConfiguredFeatureFlag } from '@prisma/client'
2 | import {
3 | useDashboardOptional,
4 | useOrganizationOptional,
5 | } from '~/components/DashboardContext'
6 | import { isFeatureFlagEnabled } from './featureFlags'
7 | import { trpc } from './trpc'
8 |
9 | export function useIsFeatureEnabled(flag: ConfiguredFeatureFlag) {
10 | const dashboard = useDashboardOptional()
11 | const organization = useOrganizationOptional()
12 | const globalFeatureFlags = trpc.useQuery(['dashboard.global-feature-flags'], {
13 | enabled: !dashboard,
14 | })
15 |
16 | return isFeatureFlagEnabled(flag, {
17 | globalFeatureFlags:
18 | dashboard?.globalFeatureFlags ?? globalFeatureFlags.data,
19 | organization,
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/useIsomorphicLocation.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useLocation as useReactRouterLocation,
3 | Link as ReactRouterLink,
4 | } from 'react-router-dom'
5 |
6 | // Will refactor away this file in a future PR
7 |
8 | export function useIsomorphicLink() {
9 | return ReactRouterLink
10 | }
11 |
12 | export default function useIsomorphicLocation() {
13 | return useReactRouterLocation
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/usePageVisibility.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export default function usePageVisibility() {
4 | const [isVisible, setIsVisible] = useState(true)
5 |
6 | useEffect(() => {
7 | const handleVisibilityChange = () => {
8 | setIsVisible(document.visibilityState !== 'hidden')
9 | }
10 |
11 | const handleFocusChange = event => {
12 | setIsVisible(event.type === 'focus')
13 | }
14 |
15 | document.addEventListener('visibilitychange', handleVisibilityChange)
16 |
17 | window.addEventListener('blur', handleFocusChange)
18 | window.addEventListener('focus', handleFocusChange)
19 |
20 | return () => {
21 | document.removeEventListener(
22 | 'visibilitychange',
23 | handleVisibilityChange,
24 | false
25 | )
26 |
27 | window.removeEventListener('blur', handleFocusChange)
28 | window.removeEventListener('focus', handleFocusChange)
29 | }
30 | }, [])
31 |
32 | return isVisible
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/usePlatform.ts:
--------------------------------------------------------------------------------
1 | /* While navigator.platform is deprecated, there is no better way to detect this right now */
2 | export function reportsAsAppleDevice() {
3 | if (typeof window !== 'undefined') {
4 | const re = /Mac|iPhone/gi
5 | return window.navigator?.platform && re.test(window.navigator.platform)
6 | }
7 |
8 | return false
9 | }
10 |
11 | export interface ShortcutMap {
12 | mac?: string
13 | pc?: string
14 | }
15 |
16 | export function getShortcuts(
17 | shortcuts: string | ShortcutMap | undefined
18 | ): string | undefined {
19 | if (!shortcuts) return undefined
20 | if (typeof shortcuts === 'string') return shortcuts
21 |
22 | const device = reportsAsAppleDevice() ? 'mac' : 'pc'
23 | return shortcuts?.[device]
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import { MutableRefObject, useEffect, useRef } from 'react'
2 |
3 | export default function usePrevious(
4 | value: T
5 | ): MutableRefObject['current'] {
6 | const ref = useRef()
7 | useEffect(() => {
8 | ref.current = value
9 | }, [value])
10 | return ref.current
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/useScrollToTop.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from 'react'
2 | import { useLocation } from 'react-router-dom'
3 | import usePrevious from './usePrevious'
4 |
5 | /**
6 | * Scroll to top when navigating to a new page.
7 | * (this is not the default behavior in React Router v5.)
8 | */
9 | export default function useScrollToTop() {
10 | const { pathname, hash } = useLocation()
11 | const prevPathname = usePrevious(pathname)
12 |
13 | useLayoutEffect(() => {
14 | if (prevPathname !== pathname) {
15 | if (hash) {
16 | const element = document.querySelector(hash)
17 | if (element) {
18 | element.scrollIntoView()
19 | }
20 | } else {
21 | window.scrollTo(0, 0)
22 | }
23 | }
24 | }, [pathname, prevPathname, hash])
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/useTransactionAutoFocus.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export default function useTransactionAutoFocus({
4 | enabled,
5 | ref,
6 | }: {
7 | enabled: boolean
8 | ref: React.RefObject
9 | }) {
10 | useEffect(() => {
11 | let timeout: NodeJS.Timeout | null = null
12 |
13 | if (!enabled) return
14 |
15 | // wait until the scroll is finished before focusing the next input.
16 | // this prevents the scroll from being interrupted by the focus.
17 | timeout = setTimeout(() => {
18 | const autofocusTarget = ref.current?.querySelector(
19 | '[data-autofocus-target]'
20 | )
21 |
22 | if (autofocusTarget instanceof HTMLElement) {
23 | autofocusTarget.focus()
24 | }
25 | }, 200)
26 |
27 | return () => {
28 | if (timeout) clearTimeout(timeout)
29 | }
30 | }, [enabled, ref])
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/useTransactionAutoScroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export default function useTransactionAutoScroll({
4 | enabled,
5 | ref,
6 | }: {
7 | enabled: boolean
8 | ref: React.RefObject
9 | }) {
10 | useEffect(() => {
11 | if (!enabled) return
12 |
13 | const focusedEl = document.activeElement
14 |
15 | if (focusedEl && focusedEl instanceof HTMLElement) {
16 | // don't blur if the focused element is inside a dialog (e.g. io.confirm)
17 | // this will not interfere with scroll behavior
18 | if (focusedEl.closest('[role="dialog"]')) return
19 |
20 | focusedEl.blur()
21 | }
22 |
23 | const timeoutHandle = setTimeout(() => {
24 | ref.current?.scrollIntoView({
25 | behavior: 'smooth',
26 | })
27 | }, 100)
28 |
29 | return () => {
30 | clearTimeout(timeoutHandle)
31 | }
32 | }, [enabled, ref])
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/useWindowSize.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | function getWindowSize() {
4 | return {
5 | width: window.innerWidth,
6 | height: window.innerHeight,
7 | }
8 | }
9 |
10 | export default function useWindowSize() {
11 | const [windowSize, setWindowSize] = useState(getWindowSize())
12 |
13 | useEffect(() => {
14 | function handleResize() {
15 | setWindowSize(getWindowSize())
16 | }
17 |
18 | window.addEventListener('resize', handleResize)
19 | return () => window.removeEventListener('resize', handleResize)
20 | }, [])
21 |
22 | return windowSize
23 | }
24 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/wss/consts.ts:
--------------------------------------------------------------------------------
1 | export const port = 3033
2 |
--------------------------------------------------------------------------------
/svgr.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | /** @type {import('@svgr/core').Config} */
4 | module.exports = {
5 | svgoConfig: {
6 | plugins: ['removeDimensions'],
7 | },
8 | template: require('./src/icons/svgr-template.js'),
9 | typescript: true,
10 | expandProps: true,
11 | replaceAttrValues: {
12 | black: 'currentColor',
13 | '#000': 'currentColor',
14 | '#000000': 'currentColor',
15 | },
16 | outDir: 'src/icons/compiled',
17 | }
18 |
--------------------------------------------------------------------------------
/test/_fixtures.ts:
--------------------------------------------------------------------------------
1 | import { test as base } from '@playwright/test'
2 | import { Signup } from './classes/Signup'
3 | import { Transaction } from './classes/Transaction'
4 |
5 | interface IntervalFixtures {
6 | transactions: Transaction
7 | signup: Signup
8 | }
9 |
10 | export const test = base.extend({
11 | transactions: async ({ page }, use) => {
12 | await use(new Transaction(page))
13 | },
14 | signup: async ({ page }, use) => {
15 | await use(new Signup(page))
16 | },
17 | })
18 |
--------------------------------------------------------------------------------
/test/app/console.test.ts:
--------------------------------------------------------------------------------
1 | import { consoleUrl } from '../_setup'
2 | import { test } from '../_fixtures'
3 |
4 | test.describe.parallel('Console tests', () => {
5 | test('New transaction', async ({ page, transactions }) => {
6 | await page.goto(await consoleUrl())
7 | await transactions.run('io.input.text')
8 |
9 | await page.click('text=First name')
10 | await page.fill('input[type="text"]', 'Interval')
11 | await transactions.continue()
12 | await transactions.expectSuccess()
13 | await page.click('text=New transaction')
14 |
15 | await page.click('text=First name')
16 | await page.fill('input[type="text"]', 'Interval')
17 | await transactions.continue()
18 | await transactions.expectSuccess()
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/test/data/canyon.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/interval/server/e83ae439fb87cbc68fc0c765c4a7719dcfc16be9/test/data/canyon.mp4
--------------------------------------------------------------------------------
/test/data/fail.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/interval/server/e83ae439fb87cbc68fc0c765c4a7719dcfc16be9/test/data/fail.gif
--------------------------------------------------------------------------------
/test/data/mockDb.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker'
2 |
3 | faker.seed(0)
4 |
5 | export interface User {
6 | id: string
7 | firstName: string
8 | lastName: string
9 | email: string
10 | createdAt: Date
11 | }
12 |
13 | const allUsers = Array.from({ length: 313 }, () => {
14 | return {
15 | id: faker.datatype.uuid(),
16 | firstName: faker.name.firstName(),
17 | lastName: faker.name.lastName(),
18 | email: faker.internet.email(),
19 | createdAt: faker.date.recent(30),
20 | }
21 | }).sort((a, b) => {
22 | return b.createdAt.getTime() - a.createdAt.getTime()
23 | })
24 |
25 | export function getUsers() {
26 | return allUsers
27 | }
28 |
29 | export function getUser(id: string): User | null {
30 | return allUsers.find(user => user.id === id) ?? null
31 | }
32 |
33 | export function findUser(query: string): User[] {
34 | const re = RegExp(query, 'i')
35 | return allUsers.filter(
36 | user =>
37 | re.test(user.firstName) || re.test(user.lastName) || re.test(user.email)
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/test/data/spreadsheet.csv:
--------------------------------------------------------------------------------
1 | string,optionalString,number,boolean
2 | A string,,23,1
3 | Another string,A third string,3,0
4 |
--------------------------------------------------------------------------------
/test/index.ts:
--------------------------------------------------------------------------------
1 | import child_process from 'child_process'
2 | import { versions } from './releases'
3 |
4 | function runAll() {
5 | let command = 'yarn test'
6 | // Allow passing runtime command-line arguments to below playwright call
7 | // eg `PLAYWRIGHT_ARGS='--workers 1' yarn test:all`
8 | const { PLAYWRIGHT_ARGS } = process.env
9 | if (PLAYWRIGHT_ARGS) {
10 | command += ` ${PLAYWRIGHT_ARGS}`
11 | }
12 |
13 | for (const version of Object.keys(versions)) {
14 | const commandString = `SDK_VERSION=${version} ${command}`
15 |
16 | console.log(`Running: ${commandString}`)
17 | child_process.execSync(command, {
18 | stdio: 'inherit',
19 | env: {
20 | // Necessary to use same `node` as current process, eg for nvm
21 | // Derived from https://github.com/yarnpkg/yarn/blob/6db39cf0ff684ce4e7de29669046afb8103fce3d/src/util/execute-lifecycle-script.js
22 | NODE: process.execPath,
23 | ...process.env,
24 | npm_node_execpath: process.env.NODE ?? process.execPath,
25 | SDK_VERSION: version,
26 | },
27 | })
28 | }
29 | }
30 |
31 | runAll()
32 |
--------------------------------------------------------------------------------
/test/releases/index.ts:
--------------------------------------------------------------------------------
1 | import main, { localConfig } from './main/host'
2 |
3 | export const versions = {
4 | main,
5 | }
6 |
7 | export const localConfigs = {
8 | main: localConfig,
9 | }
10 |
11 | export { main }
12 |
13 | export default async function setupHost() {
14 | const { SDK_VERSION = 'main' } = process.env
15 |
16 | return versions[SDK_VERSION]()
17 | }
18 |
19 | export function getLocalConfig() {
20 | const { SDK_VERSION = 'main' } = process.env
21 |
22 | return localConfigs[SDK_VERSION]
23 | }
24 |
--------------------------------------------------------------------------------
/test/utils/uploads.ts:
--------------------------------------------------------------------------------
1 | import env from '~/env'
2 | import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
3 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
4 |
5 | export async function generateS3Urls(key: string) {
6 | if (!env.S3_KEY_ID || !env.S3_KEY_SECRET || !env.S3_REGION) {
7 | throw new Error('Missing S3 credentials for generateUploadUrl')
8 | }
9 |
10 | const s3Client = new S3Client({
11 | region: env.S3_REGION,
12 | credentials: {
13 | accessKeyId: env.S3_KEY_ID,
14 | secretAccessKey: env.S3_KEY_SECRET,
15 | },
16 | })
17 |
18 | const command = new PutObjectCommand({
19 | // always use this bucket for test uploads.
20 | // it has public access configured so upload contents can be validated.
21 | Bucket: 'interval-io-uploads-dev',
22 | Key: key,
23 | })
24 |
25 | const uploadUrl = await getSignedUrl(s3Client, command, {
26 | expiresIn: 3600, // 1 hour
27 | })
28 |
29 | const url = new URL(uploadUrl)
30 | const downloadUrl = url.origin + url.pathname
31 |
32 | return { uploadUrl, downloadUrl }
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "strictNullChecks": true,
9 | "noEmit": true,
10 | "noImplicitAny": false,
11 | "moduleResolution": "node",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "types": ["@types/jest", "mdx"],
16 | "baseUrl": ".",
17 | "forceConsistentCasingInFileNames": true,
18 | "esModuleInterop": true,
19 | "module": "ES2020",
20 | "paths": {
21 | "env": ["src/env.ts"],
22 | "~/*": ["./src/*"],
23 | "server/*": ["./src/server"]
24 | }
25 | },
26 | "include": ["interval.d.ts", "**/*.ts", "**/*.tsx", "**/*.mdx"],
27 | "exclude": ["node_modules"],
28 | "ts-node": {
29 | "compilerOptions": {
30 | "module": "CommonJS"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist/src",
5 | "module": "commonjs",
6 | "noEmit": false,
7 | "types": ["node", "jest"]
8 | },
9 | "include": ["src/server/**/*", "src/entry.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.wss.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist/src",
5 | "module": "commonjs",
6 | "noEmit": false,
7 | "types": ["node", "jest"]
8 | },
9 | "include": ["src/wss/**/*"]
10 | }
11 |
--------------------------------------------------------------------------------