diff --git a/src/frontend/apps/impress/next.config.js b/src/frontend/apps/impress/next.config.js index 16d2bbaae..6c98ce81d 100644 --- a/src/frontend/apps/impress/next.config.js +++ b/src/frontend/apps/impress/next.config.js @@ -1,4 +1,5 @@ const crypto = require('crypto'); +const path = require('path'); const { InjectManifest } = require('workbox-webpack-plugin'); @@ -11,6 +12,9 @@ const nextConfig = { images: { unoptimized: true, }, + sassOptions: { + includePaths: [path.join(__dirname, 'src')], + }, compiler: { // Enables the styled-components SWC transform styledComponents: true, diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index 66740a915..122e3ccef 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -33,6 +33,7 @@ "react-dom": "*", "react-i18next": "15.0.3", "react-select": "5.8.1", + "sass": "1.80.4", "styled-components": "6.1.13", "y-protocols": "1.0.6", "yjs": "*", diff --git a/src/frontend/apps/impress/src/components/DropButton.tsx b/src/frontend/apps/impress/src/components/DropButton.tsx index f79b78e61..2fa78e887 100644 --- a/src/frontend/apps/impress/src/components/DropButton.tsx +++ b/src/frontend/apps/impress/src/components/DropButton.tsx @@ -19,6 +19,8 @@ const StyledPopover = styled(Popover)` const StyledButton = styled(Button)` cursor: pointer; + display: flex; + border: none; background: none; outline: none; @@ -29,7 +31,7 @@ const StyledButton = styled(Button)` text-wrap: nowrap; `; -interface DropButtonProps { +export interface DropButtonProps { button: ReactNode; isOpen?: boolean; onOpenChange?: (isOpen: boolean) => void; diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx new file mode 100644 index 000000000..4a1229229 --- /dev/null +++ b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx @@ -0,0 +1,70 @@ +import { Button } from '@openfun/cunningham-react'; +import * as React from 'react'; +import { PropsWithChildren } from 'react'; + +import { Box, DropButton, DropButtonProps } from '@/components'; +import { Icon } from '@/components/icons/Icon'; + +export type DropdownMenuOption = { + icon?: string; + label: string; + callback?: () => void | Promise; + danger?: boolean; +}; + +export type DropdownMenuProps = Omit & { + options: DropdownMenuOption[]; + showArrow?: boolean; + arrowClassname?: string; +}; + +export const DropdownMenu = ({ + options, + children, + showArrow = false, + arrowClassname, + ...dropButtonProps +}: PropsWithChildren) => { + const getButton = () => { + if (!showArrow) { + return children; + } + + return ( +
+
{children}
+ +
+ ); + }; + + return ( + + + {options.map((option) => ( + + ))} + + + ); +}; diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/dropdown-menu.module.scss b/src/frontend/apps/impress/src/components/dropdown-menu/dropdown-menu.module.scss new file mode 100644 index 000000000..96f267ed7 --- /dev/null +++ b/src/frontend/apps/impress/src/components/dropdown-menu/dropdown-menu.module.scss @@ -0,0 +1,13 @@ +.simpleContent { + display: flex; + align-items: center; + gap: var(--c--theme--spacings--st); +} + + +.optionsContainer { + display: flex; + flex-direction: column; + + align-items: flex-start; +} \ No newline at end of file diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/useDropdownMenu.tsx b/src/frontend/apps/impress/src/components/dropdown-menu/useDropdownMenu.tsx new file mode 100644 index 000000000..eb11e619b --- /dev/null +++ b/src/frontend/apps/impress/src/components/dropdown-menu/useDropdownMenu.tsx @@ -0,0 +1,14 @@ +import { useState } from 'react'; + +export const useDropdownMenu = () => { + const [isOpen, setIsOpen] = useState(false); + + const onOpenChange = (isOpen: boolean) => { + setIsOpen(isOpen); + }; + + return { + isOpen, + onOpenChange, + }; +}; diff --git a/src/frontend/apps/impress/src/components/icons/Icon.tsx b/src/frontend/apps/impress/src/components/icons/Icon.tsx new file mode 100644 index 000000000..2da5975d5 --- /dev/null +++ b/src/frontend/apps/impress/src/components/icons/Icon.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +type Props = { + icon: string; + className?: string; +}; +export const Icon = ({ icon, className }: Props) => { + return {icon}; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 2ef15e5a3..32a1800a0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -10,6 +10,8 @@ import { useTranslation } from 'react-i18next'; import { Box, TextErrors } from '@/components'; import { mediaUrl } from '@/core'; import { useAuthStore } from '@/core/auth'; +import { blockNoteSchema } from '@/features/docs'; +import { SuggestionMenuEditor } from '@/features/docs/doc-editor/components/custom-blocks/suggestion-menu/SuggestionMenuEditor'; import { Doc } from '@/features/docs/doc-management'; import { Version } from '@/features/docs/doc-versioning/'; @@ -135,6 +137,7 @@ export const BlockNoteContent = ({ const editor = useCreateBlockNote( { + schema: blockNoteSchema, collaboration: { provider, fragment: provider.document.getXmlFragment('document-store'), @@ -165,26 +168,32 @@ export const BlockNoteContent = ({ }; }, [editor, resetHeadings, setHeadings]); - return ( - - {isErrorAttachment && ( - - - - )} + const goodEditor = storedEditor ?? editor; - - - - + return ( + <> + + {isErrorAttachment && ( + + + + )} + + + + + + + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/AlertBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/AlertBlock.tsx new file mode 100644 index 000000000..f51286b6a --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/AlertBlock.tsx @@ -0,0 +1,81 @@ +import { defaultProps } from '@blocknote/core'; +import { createReactBlockSpec } from '@blocknote/react'; +import React from 'react'; + +import { + DropdownMenu, + DropdownMenuOption, +} from '@/components/dropdown-menu/DropdownMenu'; +import { useDropdownMenu } from '@/components/dropdown-menu/useDropdownMenu'; + +import style from './alert-block.module.scss'; + +const alertTypes = [ + { value: 'default', icon: 'info' }, + { value: 'warning', icon: 'warning' }, + { value: 'info', icon: 'info' }, + { value: 'error', icon: 'cancel' }, + { value: 'success', icon: 'check_circle' }, +]; + +export const Alert = createReactBlockSpec( + { + type: 'alert', + propSchema: { + textAlignment: defaultProps.textAlignment, + textColor: defaultProps.textColor, + type: { + default: 'default', + values: alertTypes.map((value) => value.value), + }, + }, + content: 'inline', + }, + { + render: (props) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const dropdown = useDropdownMenu(); + const aa: DropdownMenuOption[] = alertTypes.map((type) => { + return { + label: type.value, + icon: type.icon, + callback: () => + void props.editor.updateBlock(props.block, { + type: 'alert', + props: { type: type.value }, + }), + }; + }); + + const getIcon = () => { + const index = alertTypes.findIndex( + (type) => type.value === props.block.props.type, + ); + + if (index >= 0) { + return alertTypes[index].icon; + } + return 'info'; + }; + + return ( +
+
+ + + {getIcon()} + + +
+ +
+
+ ); + }, + }, +); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/alert-block.module.scss b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/alert-block.module.scss new file mode 100644 index 000000000..aa4ec3e92 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/alert-block.module.scss @@ -0,0 +1,61 @@ +.alertContainer { + display: flex; + border-radius: 4px; + justify-content: flex-start; + align-items: center; + padding: var(--c--theme--spacings--s); + gap: var(--c--theme--spacings--t); + width: 100%; + + &.default { + background: #f2f1ee; // To keep default coolor of BlockNote but maybe make a custom theme + } + + &.warning { + background: #FEF7DA + } + + &.error { + background: #FEE9E5 + + } + + &.info { + background: #e6ebff + } + + &.success { + background: #E3FDEB + } + +} + +.alertIcon { + &[data-alert-icon-type="warning"] { + color: #FBE769 + } + &[data-alert-icon-type="error"] { + color: #E4794A + } + &[data-alert-icon-type="info"] { + color: #507aff + } + &[data-alert-icon-type="success"] { + color: #6FE49D + } +} + + + + +.icon { + display: flex; + align-items: center; + justify-content: center; +} +.content { + flex-grow: 1; +} + + + diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/insertMenuAlertBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/insertMenuAlertBlock.tsx new file mode 100644 index 000000000..0fb558889 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/alert/insertMenuAlertBlock.tsx @@ -0,0 +1,25 @@ +import { insertOrUpdateBlock } from '@blocknote/core'; + +import { blockNoteSchema } from '@/features/docs'; + +export const insertMenuAlertBlock = ( + editor: typeof blockNoteSchema.BlockNoteEditor, +) => ({ + title: 'Alert', + onItemClick: () => { + insertOrUpdateBlock(editor, { + type: 'alert', + }); + }, + aliases: [ + 'alert', + 'notification', + 'emphasize', + 'warning', + 'error', + 'info', + 'success', + ], + group: 'Other', + icon: infos, +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/suggestion-menu/SuggestionMenuEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/suggestion-menu/SuggestionMenuEditor.tsx new file mode 100644 index 000000000..192c60864 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/suggestion-menu/SuggestionMenuEditor.tsx @@ -0,0 +1,28 @@ +import { filterSuggestionItems } from '@blocknote/core'; +import { + SuggestionMenuController, + getDefaultReactSlashMenuItems, +} from '@blocknote/react'; +import * as React from 'react'; + +import { DocsEditor } from '@/features/docs'; +import { insertMenuAlertBlock } from '@/features/docs/doc-editor/components/custom-blocks/alert/insertMenuAlertBlock'; + +type Props = { + editor: DocsEditor; +}; +export const SuggestionMenuEditor = ({ editor }: Props) => { + const getItem = async (query: string): Promise => { + return new Promise((resolve) => { + const result = filterSuggestionItems( + [ + ...getDefaultReactSlashMenuItems(editor), + insertMenuAlertBlock(editor), + ], + query, + ); + resolve(result as never[]); + }); + }; + return ; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx index c75fe5ef4..f1052863b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useDocStore.tsx @@ -1,16 +1,16 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { HocuspocusProvider } from '@hocuspocus/provider'; import * as Y from 'yjs'; import { create } from 'zustand'; import { providerUrl } from '@/core'; +import { DocsEditor } from '@/features/docs'; import { Base64, Doc } from '@/features/docs/doc-management'; import { blocksToYDoc } from '../utils'; interface DocStore { provider: HocuspocusProvider; - editor?: BlockNoteEditor; + editor?: DocsEditor; } export interface UseDocStore { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx index ac9b8a4b3..fe2e634e9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx @@ -1,7 +1,6 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { create } from 'zustand'; -import { HeadingBlock } from '../types'; +import { DocsEditor, HeadingBlock } from '../types'; const recursiveTextContent = (content: HeadingBlock['content']): string => { if (!content) { @@ -21,7 +20,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => { export interface UseHeadingStore { headings: HeadingBlock[]; - setHeadings: (editor: BlockNoteEditor) => void; + setHeadings: (editor: DocsEditor) => void; resetHeadings: () => void; } diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx index 19094b6c5..fe932b7ff 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx @@ -1,3 +1,7 @@ +import { BlockNoteSchema, defaultBlockSpecs } from '@blocknote/core'; + +import { Alert } from '@/features/docs/doc-editor/components/custom-blocks/alert/AlertBlock'; + export interface DocAttachment { file: string; } @@ -12,3 +16,14 @@ export type HeadingBlock = { level: number; }; }; + +export const blockNoteSchema = BlockNoteSchema.create({ + blockSpecs: { + // Adds all default blocks. + ...defaultBlockSpecs, + // Adds the Alert block. + alert: Alert, + }, +}); + +export type DocsEditor = typeof blockNoteSchema.BlockNoteEditor; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index 2ce583e98..87562f9fd 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -129,7 +129,6 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => { icon={summarize} size="small" > - {t('Table of contents')}