Skip to content

Commit

Permalink
✨ Allow managing communication templates from ozone (#225)
Browse files Browse the repository at this point in the history
* ✨ Allow managing communication templates from ozone

* 💄 Make buttons and links better

* ✨ Drive tabs through URL

* ✨ Add email action for account

* 🧹 Cleanup keys

* ✨ Adjust field from content -> contentMarkdown

* ✨ Adjust typings and field names to match lexicons
  • Loading branch information
foysalit authored Jan 21, 2024
1 parent 36af3e6 commit cc49787
Show file tree
Hide file tree
Showing 18 changed files with 656 additions and 109 deletions.
14 changes: 14 additions & 0 deletions app/communication-template/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import { CommunicationTemplateForm } from 'components/communication-template/form'

export default function CommunicationTemplateEditPage({ params: { id } }) {
return (
<div className="w-5/6 md:w-2/3 lg:w-1/2 mx-auto">
<h2 className="text-gray-600 font-semibold mb-3 pb-2 mt-4 border-b border-gray-300">
Edit Template #{id}
</h2>
<CommunicationTemplateForm templateId={id} />
</div>
)
}
14 changes: 14 additions & 0 deletions app/communication-template/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import { CommunicationTemplateForm } from 'components/communication-template/form'

export default function CommunicationTemplateCreatePage() {
return (
<div className="w-5/6 md:w-2/3 lg:w-1/2 mx-auto">
<h2 className="text-gray-600 font-semibold mb-3 pb-2 mt-4 border-b border-gray-300">
Create New Template
</h2>
<CommunicationTemplateForm />
</div>
)
}
94 changes: 94 additions & 0 deletions app/communication-template/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use client'

import format from 'date-fns/format'
import { useState } from 'react'
import Link from 'next/link'
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/20/solid'

import { LabelChip } from '@/common/labels'
import { Loading, LoadingFailed } from '@/common/Loader'
import { useCommunicationTemplateList } from 'components/communication-template/hooks'
import { CommunicationTemplateDeleteConfirmationModal } from 'components/communication-template/delete-confirmation-modal'
import { ActionButton, LinkButton } from '@/common/buttons'

export default function CommunicationTemplatePage() {
const { data, error, isLoading } = useCommunicationTemplateList({})
const [deletingTemplateId, setDeletingTemplateId] = useState<
string | undefined
>()

if (isLoading) {
return <Loading message="Loading templates" />
}

if (error) {
return <LoadingFailed error={error} />
}

return (
<div className="w-5/6 md:w-2/3 lg:w-1/2 mx-auto">
<div className="flex flex-row justify-between items-center">
<h2 className="font-semibold text-gray-600 mb-3 mt-4">
Communication Templates
</h2>
<LinkButton
href="/communication-template/create"
appearance="primary"
size="sm"
>
<PlusIcon className="h-4 w-4 mr-1" />
New Template
</LinkButton>
</div>
<CommunicationTemplateDeleteConfirmationModal
templateId={deletingTemplateId}
setIsDialogOpen={() => setDeletingTemplateId(undefined)}
/>
<ul>
{!data?.length && (
<div className="shadow bg-white rounded-sm p-5 text-gray-700 mb-3 text-center">
<p>No templates found</p>
<p className="text-sm text-gray-900">
Create a new template to send emails to users.
</p>
</div>
)}
{data?.map((template) => (
<li
key={template.id}
className="shadow bg-white rounded-sm p-3 text-gray-700 mb-3"
>
<p className="flex flex-row justify-between">
<span className="text-sm text-gray-900">{template.name}</span>
{template.disabled && (
<LabelChip className="bg-red-200">Disabled</LabelChip>
)}
</p>
<p className="text-sm">Subject: {template.subject}</p>
<div className="text-sm flex flex-row justify-between">
<span>
Last Updated{' '}
{format(new Date(template.updatedAt), 'do MMM yyyy')}
</span>
<div className="flex flex-row">
<Link
href={`/communication-template/${template.id}/edit`}
className="flex flex-row items-center border border-gray-400 rounded-sm px-2 hover:bg-gray-100 mx-1"
>
<PencilIcon className="h-3 w-3 mr-1" />
Edit
</Link>
<ActionButton
appearance="outlined"
onClick={() => setDeletingTemplateId(template.id)}
>
<TrashIcon className="h-3 w-3" />
</ActionButton>
</div>
</div>
</li>
))}
</ul>
</div>
)
}
60 changes: 60 additions & 0 deletions app/repositories/[id]/page-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client'
import { AccountView } from '@/repositories/AccountView'
import { createReport } from '@/repositories/createReport'
import { useRepoAndProfile } from '@/repositories/useRepoAndProfile'
import { ComAtprotoAdminEmitModerationEvent } from '@atproto/api'
import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { emitEvent } from '@/mod-event/helpers/emitEvent'

export function RepositoryViewPageContent({ id }: { id: string }) {
const {
error,
data: { repo, profile } = {},
refetch,
isLoading: isInitialLoading,
} = useRepoAndProfile({ id })
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const quickOpenParam = searchParams.get('quickOpen') ?? ''
const setQuickActionPanelSubject = (subject: string) => {
const newParams = new URLSearchParams(document.location.search)
if (!subject) {
newParams.delete('quickOpen')
} else {
newParams.set('quickOpen', subject)
}
router.push((pathname ?? '') + '?' + newParams.toString())
}

return (
<>
<ModActionPanelQuick
open={!!quickOpenParam}
onClose={() => setQuickActionPanelSubject('')}
setSubject={setQuickActionPanelSubject}
subject={quickOpenParam} // select first subject if there are multiple
subjectOptions={[quickOpenParam]}
isInitialLoading={isInitialLoading}
onSubmit={async (
vals: ComAtprotoAdminEmitModerationEvent.InputSchema,
) => {
await emitEvent(vals)
refetch()
}}
/>
<AccountView
repo={repo}
profile={profile}
onSubmit={async (vals) => {
await createReport(vals)
refetch()
}}
onShowActionPanel={(subject) => setQuickActionPanelSubject(subject)}
error={error}
id={id}
/>
</>
)
}
61 changes: 6 additions & 55 deletions app/repositories/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,13 @@
'use client'
import { AccountView } from '@/repositories/AccountView'
import { createReport } from '@/repositories/createReport'
import { useRepoAndProfile } from '@/repositories/useRepoAndProfile'
import { ComAtprotoAdminEmitModerationEvent } from '@atproto/api'
import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { emitEvent } from '@/mod-event/helpers/emitEvent'
import { Suspense } from 'react'
import { RepositoryViewPageContent } from './page-content'

export default function Repository({ params }: { params: { id: string } }) {
export default function RepositoryViewPage({ params }: { params: { id: string } }) {
const { id: rawId } = params
const id = decodeURIComponent(rawId)
const {
error,
data: { repo, profile } = {},
refetch,
isLoading: isInitialLoading,
} = useRepoAndProfile({ id })
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const quickOpenParam = searchParams.get('quickOpen') ?? ''
const setQuickActionPanelSubject = (subject: string) => {
const newParams = new URLSearchParams(document.location.search)
if (!subject) {
newParams.delete('quickOpen')
} else {
newParams.set('quickOpen', subject)
}
router.push((pathname ?? '') + '?' + newParams.toString())
}

return (
<>
<ModActionPanelQuick
open={!!quickOpenParam}
onClose={() => setQuickActionPanelSubject('')}
setSubject={setQuickActionPanelSubject}
subject={quickOpenParam} // select first subject if there are multiple
subjectOptions={[quickOpenParam]}
isInitialLoading={isInitialLoading}
onSubmit={async (
vals: ComAtprotoAdminEmitModerationEvent.InputSchema,
) => {
await emitEvent(vals)
refetch()
}}
/>
<AccountView
repo={repo}
profile={profile}
onSubmit={async (vals) => {
await createReport(vals)
refetch()
}}
onShowActionPanel={(subject) => setQuickActionPanelSubject(subject)}
error={error}
id={id}
/>
</>
<Suspense fallback={<div></div>}>
<RepositoryViewPageContent id={id} />
</Suspense>
)
}
6 changes: 3 additions & 3 deletions components/common/Loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { ComponentProps } from 'react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'

export function Loading(
props: { noPadding?: boolean } & ComponentProps<'div'>,
props: { noPadding?: boolean; message?: string } & ComponentProps<'div'>,
) {
const { className = '', noPadding, ...others } = props
const { className = '', noPadding, message = 'Loading...', ...others } = props
return (
<div
className={`${className} text-center ${noPadding ? '' : 'p-10'}`}
Expand All @@ -15,7 +15,7 @@ export function Loading(
role="status"
aria-label="loading"
>
<span className="sr-only">Loading...</span>
<span className="sr-only">{message}</span>
</div>
</div>
)
Expand Down
23 changes: 21 additions & 2 deletions components/common/buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ComponentProps, forwardRef, LegacyRef, ReactElement } from 'react'
import Link from 'next/link'

import { classNames } from '../../lib/util'

Expand Down Expand Up @@ -42,19 +43,37 @@ const appearanceClassNames = {
primary:
'bg-indigo-600 disabled:bg-gray-400 text-white hover:bg-indigo-700 focus:ring-indigo-500 border-transparent',
}
const sizeClassNames = {
xs: 'px-1 py-1 text-xs font-light',
sm: 'px-2 py-1 text-sm font-light',
md: 'px-4 py-2 text-base font-medium',
}

export const ActionButton = forwardRef(function ActionButton(
props: ComponentProps<'button'> & ActionButtonProps,
ref: LegacyRef<HTMLButtonElement>,
) {
const { className = '', appearance, ...others } = props
const { className = '', appearance, size, ...others } = props
const appearanceClassName =
appearanceClassNames[appearance] || appearanceClassNames.primary
const classNames = `inline-flex items-center rounded border px-4 py-2 text-base font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 ${className} ${appearanceClassName}`
const sizeClassName = (size && sizeClassNames[size]) || sizeClassNames.md
const classNames = `inline-flex items-center rounded border text-base font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 ${className} ${appearanceClassName} ${sizeClassName}`

return <button ref={ref} type="button" className={classNames} {...others} />
})

export const LinkButton = (
props: ComponentProps<typeof Link> & ActionButtonProps,
) => {
const { className = '', appearance, size, ...others } = props
const appearanceClassName =
appearanceClassNames[appearance] || appearanceClassNames.primary
const sizeClassName = (size && sizeClassNames[size]) || sizeClassNames.md
const classNames = `inline-flex items-center rounded border shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 ${className} ${appearanceClassName} ${sizeClassName}`

return <Link className={classNames} {...others} />
}

type ButtonGroupItem = ComponentProps<'button'> & {
Icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>
text: string
Expand Down
21 changes: 14 additions & 7 deletions components/common/forms/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,22 @@ export function FormLabel(
)
}

export const Checkbox = ({
label,
required,
className,
...rest
}: LabelProps & ComponentProps<'input'> & { className?: string }) => {
export const Checkbox = forwardRef<
HTMLInputElement,
LabelProps & ComponentProps<'input'>
>(function CheckboxElement(
{
label,
required,
className,
...rest
}: LabelProps & ComponentProps<'input'> & { className?: string },
ref,
) {
return (
<div className={className}>
<input
ref={ref}
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 mr-1"
{...rest}
Expand All @@ -160,4 +167,4 @@ export const Checkbox = ({
</label>
</div>
)
}
})
Loading

0 comments on commit cc49787

Please sign in to comment.