Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0fe3fbe
fix: make dropdown menus scrollable
imdeaconu Sep 6, 2024
4ca1a44
fix: truncate overflowing table columns
imdeaconu Sep 6, 2024
921aa13
Merge branch 'commitglobal:main' into main
imdeaconu Sep 6, 2024
e5a2869
Merge branch 'commitglobal:main' into main
imdeaconu Sep 6, 2024
7866a67
Merge branch 'commitglobal:main' into main
imdeaconu Sep 9, 2024
9ea6c42
Merge branch 'commitglobal:main' into main
imdeaconu Sep 10, 2024
1bd449d
Merge branch 'commitglobal:main' into main
imdeaconu Sep 11, 2024
c9874d1
Squashed commit of the following:
imdeaconu Sep 11, 2024
b7715f3
Merge branch 'commitglobal:main' into main
imdeaconu Sep 12, 2024
0facf65
Squashed commit of the following:
imdeaconu Sep 13, 2024
67f681d
chore: remove unused import
imdeaconu Sep 13, 2024
8d73252
chore: delete duplicated / unused classes
imdeaconu Sep 16, 2024
63b21b9
Merge branch 'commitglobal:main' into main
imdeaconu Sep 17, 2024
1892e6e
Merge branch 'commitglobal:main' into main
imdeaconu Sep 18, 2024
abb7c01
feature: add searching to MonitoringObserversTagFilter
imdeaconu Sep 19, 2024
9d0b8ae
Merge branch 'commitglobal:main' into main
imdeaconu Sep 20, 2024
c9fcd3e
chore: update config files
imdeaconu Sep 20, 2024
333ba49
Revert "[NGO Admin] Rewrite the tag selector component (#675)"
imdeaconu Sep 23, 2024
580b68e
Merge branch 'main' of https://github.com/commitglobal/votemonitor
imdeaconu Sep 23, 2024
ba2dad9
Merge branch 'commitglobal:main' into main
imdeaconu Sep 25, 2024
eea4faa
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Sep 26, 2024
29b8163
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Sep 26, 2024
68a44ee
Merge branch 'commitglobal-main'
imdeaconu Sep 26, 2024
7cf3244
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Oct 1, 2024
b6abee7
Merge branch 'commitglobal-main-s1'
imdeaconu Oct 1, 2024
cc71856
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
e45ea22
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
50d15b6
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
1ed8e99
Merge branch 'commitglobal:main' into main
imdeaconu Oct 3, 2024
c2f1395
Merge branch 'commitglobal:main' into main
imdeaconu Oct 7, 2024
66acf47
chore: update / install tiptap deps
imdeaconu Oct 7, 2024
3efb5ab
feature: add rich text editor
imdeaconu Oct 7, 2024
5da3a2f
chore: add deps for handling headings
imdeaconu Oct 7, 2024
7f70636
add headings selector
imdeaconu Oct 7, 2024
16ec946
render html notifications
imdeaconu Oct 7, 2024
7d22441
invalidate query after sending a push message
imdeaconu Oct 7, 2024
e64465e
add rich text editor to guide form
imdeaconu Oct 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
Expand All @@ -42,14 +43,20 @@
"@tanstack/react-query-devtools": "^5.28.6",
"@tanstack/react-router": "^1.22.0",
"@tanstack/react-table": "^8.9.3",
"@tiptap/extension-bold": "^2.5.8",
"@tiptap/extension-character-count": "^2.5.8",
"@tiptap/extension-document": "^2.5.8",
"@tiptap/extension-link": "^2.5.8",
"@tiptap/extension-paragraph": "^2.5.8",
"@tiptap/extension-text": "^2.5.8",
"@tiptap/react": "^2.5.8",
"@tiptap/starter-kit": "^2.5.8",
"@tiptap/core": "^2.8.0",
"@tiptap/extension-bold": "^2.8.0",
"@tiptap/extension-character-count": "^2.8.0",
"@tiptap/extension-document": "^2.8.0",
"@tiptap/extension-heading": "^2.8.0",
"@tiptap/extension-link": "^2.8.0",
"@tiptap/extension-paragraph": "^2.8.0",
"@tiptap/extension-placeholder": "^2.8.0",
"@tiptap/extension-text": "^2.8.0",
"@tiptap/extension-text-style": "^2.8.0",
"@tiptap/extension-typography": "^2.8.0",
"@tiptap/pm": "^2.8.0",
"@tiptap/react": "^2.8.0",
"@tiptap/starter-kit": "^2.8.0",
"@types/lodash": "^4.17.7",
"@uidotdev/usehooks": "^2.4.1",
"axios": "^1.6.2",
Expand Down Expand Up @@ -91,6 +98,7 @@
"@heroicons/react": "^2.0.18",
"@hookform/devtools": "^4.3.1",
"@playwright/test": "^1.36.2",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-query": "^5.28.6",
"@tanstack/react-table-devtools": "^8.7.6",
"@tanstack/router-devtools": "^1.22.0",
Expand Down
585 changes: 349 additions & 236 deletions web/pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Editor } from '@tiptap/core';
import { BubbleMenu } from '@tiptap/react';
import { useState } from 'react';
import { LinkProps, ShouldShowProps } from '../../types';
import { setLink } from '../../utils';
import { LinkEditBlock } from '../link/link-edit-block';
import { LinkPopoverBlock } from '../link/link-popover-block';

const LinkBubbleMenu = ({ editor }: { editor: Editor }) => {
const [showEdit, setShowEdit] = useState(false);
const shouldShow = ({ editor, from, to }: ShouldShowProps) => {
if (from === to) {
return false;
}

const link = editor.getAttributes('link');

if (link['href']) {
return true;
}

return false;
};

const unSetLink = () => {
editor.chain().extendMarkRange('link').unsetLink().focus().run();
setShowEdit(false);
};

function onSetLink(props: LinkProps) {
setLink(editor, props);
setShowEdit(false);
}

return (
<BubbleMenu
editor={editor}
shouldShow={shouldShow}
tippyOptions={{
placement: 'bottom-start',
onHidden: () => {
setShowEdit(false);
},
}}>
{showEdit ? (
<LinkEditBlock
onSetLink={onSetLink}
editor={editor}
className='w-full min-w-80 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none'
/>
) : (
<LinkPopoverBlock onClear={unSetLink} link={editor.getAttributes('link')} onEdit={() => setShowEdit(true)} />
)}
</BubbleMenu>
);
};

export { LinkBubbleMenu };
118 changes: 118 additions & 0 deletions web/src/components/rich-text-editor/components/headings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type { toggleVariants } from '@/components/ui/toggle';
import { cn } from '@/lib/utils';
import { CaretDownIcon, LetterCaseCapitalizeIcon } from '@radix-ui/react-icons';
import type { Level } from '@tiptap/extension-heading';
import type { Editor } from '@tiptap/react';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';
import type { FormatAction } from '../types';
import { ToolbarButton } from './toolbar-button';

interface TextStyle extends Omit<FormatAction, 'value' | 'icon' | 'action' | 'isActive' | 'canExecute'> {
element: keyof JSX.IntrinsicElements;
level?: Level;
className: string;
}

const formatActions: TextStyle[] = [
{
label: 'Normal Text',
element: 'span',
className: 'grow',
shortcuts: ['mod', 'alt', '0'],
},
{
label: 'Heading 1',
element: 'h1',
level: 1,
className: 'm-0 grow text-3xl font-extrabold',
shortcuts: ['mod', 'alt', '1'],
},
{
label: 'Heading 2',
element: 'h2',
level: 2,
className: 'm-0 grow text-xl font-bold',
shortcuts: ['mod', 'alt', '2'],
},
{
label: 'Heading 3',
element: 'h3',
level: 3,
className: 'm-0 grow text-lg font-semibold',
shortcuts: ['mod', 'alt', '3'],
},

];

interface HeadingsProps extends VariantProps<typeof toggleVariants> {
editor: Editor;
activeLevels?: Level[];
}

export const Headings: React.FC<HeadingsProps> = React.memo(
({ editor, activeLevels = [1, 2, 3, 4, 5, 6], size, variant }) => {
const filteredActions = React.useMemo(
() => formatActions.filter((action) => !action.level || activeLevels.includes(action.level)),
[activeLevels]
);

const handleStyleChange = React.useCallback(
(level?: Level) => {
if (level) {
editor.chain().focus().toggleHeading({ level }).run();
} else {
editor.chain().focus().setParagraph().run();
}
},
[editor]
);

const renderMenuItem = React.useCallback(
({ label, element: Element, level, className, shortcuts }: TextStyle) => (
<DropdownMenuItem
key={label}
onClick={() => handleStyleChange(level)}
className={cn('flex flex-row items-center justify-between gap-4', {
'bg-accent': level ? editor.isActive('heading', { level }) : editor.isActive('paragraph'),
})}
aria-label={label}>
<Element className={className}>{label}</Element>
</DropdownMenuItem>
),
[editor, handleStyleChange]
);

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ToolbarButton
isActive={editor.isActive('heading')}
tooltip='Text styles'
aria-label='Text styles'
pressed={editor.isActive('heading')}
className='w-12'
disabled={editor.isActive('codeBlock')}
size={size}
variant={variant}>
<LetterCaseCapitalizeIcon className='size-5' />
<CaretDownIcon className='size-5' />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent align='start' className='w-full'>
{filteredActions.map(renderMenuItem)}
</DropdownMenuContent>
</DropdownMenu>
);
}
);

Headings.displayName = 'Headings';

export default Headings;
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { Editor } from '@tiptap/core'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { LinkProps, LinkType } from '../../types'
import { cn } from '@/lib/utils'
import { Toggle } from '@/components/ui/toggle'
import { Link2Icon, MobileIcon, EnvelopeClosedIcon } from '@radix-ui/react-icons'

interface LinkEditBlockProps extends React.HTMLAttributes<HTMLDivElement> {
editor: Editor
onSetLink: ({ url, text, type }: LinkProps) => void
close?: () => void
}

const LinkEditBlock = ({ editor, onSetLink, close, className, ...props }: LinkEditBlockProps) => {
const formRef = React.useRef<HTMLDivElement>(null)

const [field, setField] = React.useState<LinkProps>({
url: '',
text: '',
type: LinkType.URL
})

const data = React.useMemo(() => {
const { href } = editor.getAttributes('link')
const { from, to } = editor.state.selection
const text = editor.state.doc.textBetween(from, to, ' ')

const isTelephone = href?.startsWith('tel:');
const isEmail = href?.startsWith('mailto:');

let url: string = href;

if (isTelephone) {
url = url.replace('tel:', '')
}

if (isEmail) {
url = url.replace('mailto:', '')
}

return {
url,
text,
type: isTelephone ? LinkType.TELEPHONE : isEmail ? LinkType.EMAIL : LinkType.URL
}
}, [editor])

React.useEffect(() => {
console.log('effect');
setField(data)
}, [data])

const handleClick = (e: React.FormEvent) => {
e.preventDefault()

if (formRef.current) {
const isValid = Array.from(formRef.current.querySelectorAll('input')).every(input => input.checkValidity())

if (isValid) {
onSetLink(field)
close?.()
} else {
formRef.current.querySelectorAll('input').forEach(input => {
if (!input.checkValidity()) {
input.reportValidity()
}
})
}
}
}

return (
<div ref={formRef}>
<div className={cn('space-y-4', className)} {...props}>
<div>
<Toggle aria-label="Url" onPressedChange={() => setField({ ...field, type: LinkType.URL })} pressed={field.type === LinkType.URL}>
<Link2Icon className="h-4 w-4" />
</Toggle>
<Toggle aria-label="Email" onPressedChange={() => setField({ ...field, type: LinkType.EMAIL })} pressed={field.type === LinkType.EMAIL} >
<EnvelopeClosedIcon className="h-4 w-4" />
</Toggle>
<Toggle aria-label="Telephone" onPressedChange={() => setField({ ...field, type: LinkType.TELEPHONE })} pressed={field.type === LinkType.TELEPHONE}>
<MobileIcon className="h-4 w-4" />
</Toggle>
</div>
<div className="space-y-1">
<Label>{field.type === LinkType.EMAIL ? 'Email' : field.type === LinkType.URL ? 'Link' : 'Telephone'}</Label>
<Input
type={field.type === LinkType.EMAIL ? 'email' : field.type === LinkType.URL ? 'url' : 'tel'}
required
placeholder={field.type === LinkType.EMAIL ? 'Paste an email' : field.type === LinkType.URL ? 'Paste a link' : 'Paste a phone number'}
value={field.url ?? ''}
onChange={e => setField({ ...field, url: e.target.value })}
/>
</div>

<div className="space-y-1">
<Label>Display text (optional)</Label>
<Input
type="text"
placeholder="Text to display"
value={field.text ?? ''}
onChange={e => setField({ ...field, text: e.target.value })}
/>
</div>

<div className="flex justify-end space-x-2">
{close && (
<Button variant="ghost" type="button" onClick={close}>
Cancel
</Button>
)}

<Button type="button" onClick={handleClick}>
Insert
</Button>
</div>
</div>
</div>
)
}

export { LinkEditBlock }
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Editor } from '@tiptap/core'
import * as React from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Link2Icon } from '@radix-ui/react-icons'
import { ToolbarButton } from '../toolbar-button'
import { LinkEditBlock } from './link-edit-block'
import { LinkProps } from '../../types'
import { setLink } from '../../utils'

const LinkEditPopover = ({ editor }: { editor: Editor }) => {
const [open, setOpen] = React.useState(false)

const onSetLink = (props: LinkProps) => {
setLink(editor, props)
editor.commands.enter()
}

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<ToolbarButton
isActive={editor.isActive('link')}
tooltip="Link"
aria-label="Insert link"
disabled={editor.isActive('codeBlock')}
>
<Link2Icon className="size-5" />
</ToolbarButton>
</PopoverTrigger>
<PopoverContent className="w-full min-w-80" align="start" side="bottom">
<LinkEditBlock editor={editor} close={() => setOpen(false)} onSetLink={onSetLink} />
</PopoverContent>
</Popover>
)
}

export { LinkEditPopover }
Loading