Skip to content

Commit

Permalink
❇️ Template language (#172)
Browse files Browse the repository at this point in the history
* ✨ Show detailed error message when template saving fails

* ✨ Add language picker to comm templates

* 💄 Improved UI changes

* ✨ Better empty message

* ♻️ Refactor language name getter

* 🧹 Cleanup

* ⬆️ Upgrade @atproto/api version
  • Loading branch information
foysalit authored Sep 5, 2024
1 parent 24d7fe7 commit dfb6c66
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 108 deletions.
58 changes: 41 additions & 17 deletions app/communication-template/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ import { useCommunicationTemplateList } from 'components/communication-template/
import { CommunicationTemplateDeleteConfirmationModal } from 'components/communication-template/delete-confirmation-modal'
import { ActionButton, LinkButton } from '@/common/buttons'
import { ErrorInfo } from '@/common/ErrorInfo'
import { LanguageSelectorDropdown } from '@/common/LanguagePicker'
import { getLanguageName } from '@/lib/locale/helpers'
import { usePermission } from '@/shell/ConfigurationContext'

export default function CommunicationTemplatePage() {
const { data, error, isLoading } = useCommunicationTemplateList({})
const [selectedLang, setSelectedLang] = useState<string | undefined>()
const [deletingTemplateId, setDeletingTemplateId] = useState<
string | undefined
>()
const templates = data
? [...data].sort((prev, next) => prev.name.localeCompare(next.name))
? [...data]
.filter((tpl) => {
if (!selectedLang) {
return true
}
return tpl.lang === selectedLang
})
.sort((prev, next) => prev.name.localeCompare(next.name))
: []
useTitle(`Communication Templates`)

Expand Down Expand Up @@ -48,14 +58,17 @@ export default function CommunicationTemplatePage() {
<h2 className="font-semibold text-gray-600 dark:text-gray-100 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 className="flex flex-row gap-2">
<LanguageSelectorDropdown {...{ selectedLang, setSelectedLang }} />
<LinkButton
href="/communication-template/create"
appearance="primary"
size="sm"
>
<PlusIcon className="h-4 w-4 mr-1" />
New Template
</LinkButton>
</div>
</div>
<CommunicationTemplateDeleteConfirmationModal
templateId={deletingTemplateId}
Expand All @@ -64,7 +77,11 @@ export default function CommunicationTemplatePage() {
<ul>
{!templates.length && (
<div className="shadow bg-white dark:bg-slate-800 rounded-sm p-5 text-gray-700 dark:text-gray-100 mb-3 text-center">
<p>No templates found</p>
<p>
{selectedLang
? `No ${getLanguageName(selectedLang)} templates found`
: 'No templates found'}
</p>
<p className="text-sm text-gray-900 dark:text-gray-200">
Create a new template to send emails to users.
</p>
Expand All @@ -75,14 +92,21 @@ export default function CommunicationTemplatePage() {
key={template.id}
className="shadow dark:shadow-slate-700 bg-white dark:bg-slate-800 rounded-sm p-3 text-gray-700 dark:text-gray-100 mb-3"
>
<p className="flex flex-row justify-between">
<span className="text-sm text-gray-900 dark:text-gray-200">
<div className="flex flex-row justify-between">
<p className="text-sm text-gray-900 dark:text-gray-200">
{template.name}
</span>
{template.disabled && (
<LabelChip className="bg-red-200">Disabled</LabelChip>
)}
</p>
</p>
<div>
{!!template.disabled && (
<LabelChip className="bg-red-200">Disabled</LabelChip>
)}
{!!template.lang && (
<LabelChip className="dark:bg-slate-600 dark:text-gray-200">
{getLanguageName(template.lang)}
</LabelChip>
)}
</div>
</div>
<p className="text-sm">Subject: {template.subject}</p>
<div className="text-sm flex flex-row justify-between">
<span>
Expand Down
53 changes: 48 additions & 5 deletions components/common/LanguagePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { getLanguageName } from '@/lib/locale/helpers'
import { LANGUAGES_MAP_CODE2 } from '@/lib/locale/languages'
import { Popover, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
import { ActionButton } from './buttons'
import { Dropdown } from './Dropdown'

// Please make sure that any item added here exists in LANGUAGES_MAP_CODE2 or add it there first
const availableLanguageCodes = [
export const availableLanguageCodes = [
'en',
'es',
'fr',
Expand Down Expand Up @@ -146,9 +148,12 @@ export const LanguagePicker: React.FC = () => {
</div>

<p className="py-2 block max-w-xs text-gray-500 dark:text-gray-300 text-xs">
Note: <i>When multiple languages are selected, only subjects that are
tagged with <b>all</b> of those languages will be
included/excluded.</i>
Note:{' '}
<i>
When multiple languages are selected, only subjects that are
tagged with <b>all</b> of those languages will be
included/excluded.
</i>
</p>
{(includedLanguages.length > 0 ||
excludedLanguages.length > 0) && (
Expand Down Expand Up @@ -203,7 +208,7 @@ const LanguageList = ({
onClick={() => !isDisabled && onSelect(code2)}
key={code2}
>
{LANGUAGES_MAP_CODE2[code2].name}
{getLanguageName(code2)}
{selected.includes(code2) && (
<CheckIcon className="h-4 w-4 text-green-700" />
)}
Expand All @@ -214,3 +219,41 @@ const LanguageList = ({
</div>
)
}

export const LanguageSelectorDropdown = ({
selectedLang,
setSelectedLang,
}: {
selectedLang?: string
setSelectedLang: (lang?: string) => void
}) => {
const selectedText = selectedLang
? getLanguageName(selectedLang)
: 'No Specific Language'

return (
<Dropdown
className="inline-flex justify-center rounded-md border border-gray-300 dark:border-teal-500 bg-white dark:bg-slate-800 dark:text-gray-100 dark:focus:border-teal-500 dark px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-slate-700"
items={[
{
id: 'default',
text: 'No Specific Language',
onClick: () => setSelectedLang(),
},
...availableLanguageCodes.map((lang) => ({
id: lang,
text: getLanguageName(lang),
onClick: () => setSelectedLang(lang),
})),
]}
data-cy="lang-selector"
>
{selectedText}

<ChevronDownIcon
className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100"
aria-hidden="true"
/>
</Dropdown>
)
}
12 changes: 11 additions & 1 deletion components/communication-template/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import { commands } from '@uiw/react-md-editor'
import dynamic from 'next/dynamic'
import { DocumentCheckIcon } from '@heroicons/react/24/outline'
import { useState } from 'react'

import { Checkbox, FormLabel, Input } from '@/common/forms'
import { ActionButton } from '@/common/buttons'
import { useCommunicationTemplateEditor } from './hooks'
import { LanguageSelectorDropdown } from '@/common/LanguagePicker'

const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false })

Expand All @@ -24,9 +26,17 @@ export const CommunicationTemplateForm = ({
onSubmit,
isSaving,
} = useCommunicationTemplateEditor(templateId)
const [lang, setLang] = useState<string | undefined>()

return (
<form onSubmit={onSubmit}>
<FormLabel label="Language" htmlFor="lang" className="flex-1 mb-3">
<input type="hidden" name="lang" value={lang} />
<LanguageSelectorDropdown
selectedLang={lang}
setSelectedLang={setLang}
/>
</FormLabel>
<FormLabel label="Name" htmlFor="name" className="flex-1 mb-3">
<Input
type="text"
Expand Down Expand Up @@ -93,4 +103,4 @@ export const CommunicationTemplateForm = ({
</ActionButton>
</form>
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ export const useCommunicationTemplateEditor = (templateId?: string) => {
async ({
contentMarkdown,
name,
lang,
subject,
disabled,
}: {
contentMarkdown: string
name: string
lang?: string
subject: string
disabled: boolean
}) =>
Expand All @@ -80,13 +82,15 @@ export const useCommunicationTemplateEditor = (templateId?: string) => {
contentMarkdown,
subject,
name,
lang,
disabled,
updatedBy: labelerAgent.assertDid,
})
: labelerAgent.api.tools.ozone.communication.createTemplate({
contentMarkdown,
subject,
name,
lang,
createdBy: labelerAgent.assertDid,
}),
[labelerAgent, templateId],
Expand All @@ -97,12 +101,13 @@ export const useCommunicationTemplateEditor = (templateId?: string) => {
const formData = new FormData(e.currentTarget)
const name = formData.get('name')?.toString() ?? ''
const subject = formData.get('subject')?.toString() ?? ''
const lang = formData.get('lang')?.toString() ?? ''
const disabled = formData.get('disabled') === 'true'

setIsSaving(true)
try {
await toast.promise(
saveFunc({ contentMarkdown, name, subject, disabled }),
saveFunc({ contentMarkdown, name, lang, subject, disabled }),
{
pending: 'Saving template...',
success: {
Expand All @@ -111,8 +116,13 @@ export const useCommunicationTemplateEditor = (templateId?: string) => {
},
},
error: {
render() {
return 'Error saving template'
render({ data }: { data?: { message: string } }) {
return (
<div>
<p>Error saving template</p>
{!!data?.message && <p className="text-sm">{data.message}</p>}
</div>
)
},
},
},
Expand Down
46 changes: 44 additions & 2 deletions components/email/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,45 @@ import { useRepoAndProfile } from '@/repositories/useRepoAndProfile'
import { useLabelerAgent } from '@/shell/ConfigurationContext'
import { compileTemplateContent, getTemplate } from './helpers'
import { TemplateSelector } from './template-selector'
import { availableLanguageCodes } from '@/common/LanguagePicker'
import { ToolsOzoneModerationDefs } from '@atproto/api'
import { useEmailComposer } from './useComposer'

const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false })

const getRecipientsLanguages = (
repo?: ToolsOzoneModerationDefs.RepoViewDetail,
) => {
if (!repo) {
return { languages: [], defaultLang: undefined }
}
// If the recipient account is tagged with multiple languages, we can use that to pre-select the non-english language
const recipientsLanguageTags =
repo.moderation.subjectStatus?.tags
?.filter((tag) => {
// there may be non-lang related tags and lang:und is set when we couldn't figure out the language
// this account associates with so no need to consider them
if (!tag.startsWith('lang:') || tag === 'lang:und') {
return false
}
return true
})
.map((tag) => tag.replace('lang:', '')) ?? []

// find out among the accepted languages, if there is any non-english one so that we can default to that
const nonEnglishLang = recipientsLanguageTags.find((lang) => {
return lang !== 'en' && availableLanguageCodes.includes(lang)
})

if (nonEnglishLang) {
return { defaultLang: nonEnglishLang, languages: recipientsLanguageTags }
}
return {
defaultLang: recipientsLanguageTags[0],
languages: recipientsLanguageTags,
}
}

export const EmailComposer = ({ did }: { did: string }) => {
const labelerAgent = useLabelerAgent()
const {
Expand All @@ -34,7 +69,13 @@ export const EmailComposer = ({ did }: { did: string }) => {
const commentField = useRef<HTMLTextAreaElement>(null)

const { data: { repo } = {} } = useRepoAndProfile({ id: did })

const recipientLanguages = getRecipientsLanguages(repo)
let templateLabel = `Template`
if (recipientLanguages.languages.length > 1) {
templateLabel = `Template (account languages: ${recipientLanguages.languages.join(
', ',
)})`
}
const onSubmit = async (e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
Expand Down Expand Up @@ -107,10 +148,11 @@ export const EmailComposer = ({ did }: { did: string }) => {

return (
<form onSubmit={onSubmit}>
<FormLabel label="Template" htmlFor="template" className="mb-3">
<FormLabel label={templateLabel} htmlFor="template" className="mb-3">
<TemplateSelector
communicationTemplates={communicationTemplates}
onSelect={onTemplateSelect}
defaultLang={recipientLanguages.defaultLang}
/>
</FormLabel>
<FormLabel label="Subject" htmlFor="subject" className="mb-3">
Expand Down
Loading

0 comments on commit dfb6c66

Please sign in to comment.