diff --git a/apps/sensenet/src/application-paths.ts b/apps/sensenet/src/application-paths.ts index caae61c9e..0d640e2ba 100644 --- a/apps/sensenet/src/application-paths.ts +++ b/apps/sensenet/src/application-paths.ts @@ -13,11 +13,11 @@ export const PATHS = { content: { appPath: '/content/:browseType/:action?', snPath: '/Root/Content' }, contentTemplates: { appPath: '/content-templates/:browseType/:action?', snPath: '/Root/ContentTemplates' }, custom: { appPath: '/custom/:browseType/:path/:action?' }, - configuration: { appPath: '/settings/configuration/:action?', snPath: '/Root/System/Settings' }, - localization: { appPath: '/settings/localization/:action?', snPath: '/Root/Localization' }, - webhooks: { appPath: '/settings/webhooks/:action?', snPath: '/Root/System/WebHooks' }, - settings: { appPath: '/settings/:submenu?' }, - apiKeys: { appPath: '/settings/apikeys' }, + configuration: { appPath: '/system/settings/:action?', snPath: '/Root/System/Settings' }, + localization: { appPath: '/system/localization/:action?', snPath: '/Root/Localization' }, + webhooks: { appPath: '/system/webhooks/:action?', snPath: '/Root/System/WebHooks' }, + settings: { appPath: '/system/:submenu?' }, + apiKeys: { appPath: '/system/apikeys' }, } as const type SettingsItemType = 'stats' | 'apikeys' | 'webhooks' | 'adminui' @@ -30,28 +30,28 @@ type RoutesWithContentBrowser = keyof Pick< type RoutesWithActionParam = keyof Pick type Options = - | { path: (typeof PATHS)['events']['appPath']; params?: { eventGuid: string; [index: string]: string } } + | { path: (typeof PATHS)['events']['appPath']; params?: { eventGuid: string;[index: string]: string } } | { - path: (typeof PATHS)[RoutesWithContentBrowser]['appPath'] - params: { browseType: (typeof BrowseType)[number]; action?: string; [index: string]: string | undefined } - } + path: (typeof PATHS)[RoutesWithContentBrowser]['appPath'] + params: { browseType: (typeof BrowseType)[number]; action?: string;[index: string]: string | undefined } + } | { - path: (typeof PATHS)['custom']['appPath'] - params: { - browseType: (typeof BrowseType)[number] - path: string - action?: string - [index: string]: string | undefined - } + path: (typeof PATHS)['custom']['appPath'] + params: { + browseType: (typeof BrowseType)[number] + path: string + action?: string + [index: string]: string | undefined } + } | { - path: (typeof PATHS)[RoutesWithActionParam]['appPath'] - params?: { action: string; [index: string]: string } - } + path: (typeof PATHS)[RoutesWithActionParam]['appPath'] + params?: { action: string;[index: string]: string } + } | { - path: (typeof PATHS)['settings']['appPath'] - params?: { submenu: SettingsItemType; [index: string]: string | SettingsItemType } - } + path: (typeof PATHS)['settings']['appPath'] + params?: { submenu: SettingsItemType;[index: string]: string | SettingsItemType } + } export const resolvePathParams = ({ path, params }: Options) => { let currentPath: string = path diff --git a/apps/sensenet/src/components/react-control-mapper.ts b/apps/sensenet/src/components/react-control-mapper.ts index eb73c9419..c1213d2d5 100644 --- a/apps/sensenet/src/components/react-control-mapper.ts +++ b/apps/sensenet/src/components/react-control-mapper.ts @@ -39,6 +39,9 @@ export const reactControlMapper = (repository: Repository) => { return FieldControls.WebhookPayload case 'sn:HtmlEditor': return FieldControls.HtmlEditor + case 'sn:RichText': + case 'sn:TipTapEditor': + return FieldControls.RichTextEditor default: } diff --git a/apps/sensenet/src/components/settings/index.tsx b/apps/sensenet/src/components/settings/index.tsx index 22d42ea8f..ca0cb12b9 100644 --- a/apps/sensenet/src/components/settings/index.tsx +++ b/apps/sensenet/src/components/settings/index.tsx @@ -53,8 +53,8 @@ export const Settings: React.FunctionComponent = () => { const settingsItems = [ { - name: 'configuration', - displayName: localizationDrawer.titles.Configuration, + name: 'settings', + displayName: localizationDrawer.titles.Settings, url: resolvePathParams({ path: PATHS.configuration.appPath }), }, { @@ -83,7 +83,7 @@ export const Settings: React.FunctionComponent = () => { switch (routeMatch.params.submenu) { case 'localization': return - case 'configuration': + case 'settings': return case 'adminui': return @@ -117,7 +117,7 @@ export const Settings: React.FunctionComponent = () => { return (
- {localizationDrawer.titles.Settings} + {localizationDrawer.titles.System}
diff --git a/apps/sensenet/src/components/settings/setup.tsx b/apps/sensenet/src/components/settings/setup.tsx index c3a88df3b..495e78e93 100644 --- a/apps/sensenet/src/components/settings/setup.tsx +++ b/apps/sensenet/src/components/settings/setup.tsx @@ -128,7 +128,7 @@ const Setup = () => { return (
- {localizationDrawerTitles.Configuration} + {localizationDrawerTitles.Settings}
{renderContent()}
diff --git a/apps/sensenet/src/components/view-controls/permission-view.tsx b/apps/sensenet/src/components/view-controls/permission-view.tsx index c04fac43f..a451abb8f 100644 --- a/apps/sensenet/src/components/view-controls/permission-view.tsx +++ b/apps/sensenet/src/components/view-controls/permission-view.tsx @@ -292,7 +292,7 @@ export const PermissionView: React.FC = (props) => { - {inheritedEntry.identity.displayName} + {inheritedEntry.identity.displayName} ({inheritedEntry.identity.path}) = (props) => { - {setOnThisEntry.identity.displayName} + {setOnThisEntry.identity.displayName} ({setOnThisEntry.identity.path}) {!setOnThisEntry.propagates && ( diff --git a/apps/sensenet/src/hooks/use-drawer-items.tsx b/apps/sensenet/src/hooks/use-drawer-items.tsx index 558ca0978..b53e80ab9 100644 --- a/apps/sensenet/src/hooks/use-drawer-items.tsx +++ b/apps/sensenet/src/hooks/use-drawer-items.tsx @@ -89,7 +89,7 @@ export const useDrawerItems = () => { systemItem: true, }, { - itemType: 'Settings', + itemType: 'System', systemItem: true, }, ], @@ -131,7 +131,7 @@ export const useDrawerItems = () => { return case 'CustomContent': return item.settings?.icon ? : - case 'Settings': + case 'System': return // no default } @@ -174,7 +174,7 @@ export const useDrawerItems = () => { path: (item as CustomContentDrawerItem).settings?.appPath || '', }, }) - case 'Settings': + case 'System': return resolvePathParams({ path: PATHS.settings.appPath, params: { submenu: 'stats' } }) default: return '/' @@ -194,32 +194,32 @@ export const useDrawerItems = () => { return drawerItem } - ;[...settings.drawer.items, ...builtInDrawerItems] - .filterAsync(async (item) => { - if (!item.permissions?.length) { - return true - } + ;[...settings.drawer.items, ...builtInDrawerItems] + .filterAsync(async (item) => { + if (!item.permissions?.length) { + return true + } - try { - for (const permission of item.permissions) { - const actions = await repo.getActions({ idOrPath: permission.path }) - const actionIndex = actions.d.results.findIndex((action) => action.Name === permission.action) - if (actionIndex === -1 || actions.d.results[actionIndex].Forbidden) { - return false + try { + for (const permission of item.permissions) { + const actions = await repo.getActions({ idOrPath: permission.path }) + const actionIndex = actions.d.results.findIndex((action) => action.Name === permission.action) + if (actionIndex === -1 || actions.d.results[actionIndex].Forbidden) { + return false + } } + } catch (error) { + logger.debug({ + message: error.message, + data: { + error, + }, + }) + return false } - } catch (error) { - logger.debug({ - message: error.message, - data: { - error, - }, - }) - return false - } - return true - }) - .then((items) => setDrawerItems(items.map(getItemFromSettings))) + return true + }) + .then((items) => setDrawerItems(items.map(getItemFromSettings))) }, [ localization.descriptions, localization.titles, diff --git a/apps/sensenet/src/localization/default.ts b/apps/sensenet/src/localization/default.ts index 9dd777283..8450598fd 100644 --- a/apps/sensenet/src/localization/default.ts +++ b/apps/sensenet/src/localization/default.ts @@ -128,6 +128,7 @@ const values = { Settings: 'Settings', Configuration: 'Configuration', Stats: 'Stats', + System: 'System', ApiAndSecurity: 'Api and Security', Webhooks: 'Webhooks', AdminUiCustomization: 'Admin-ui customization', @@ -143,6 +144,7 @@ const values = { UsersAndGroups: 'Manage users and groups, roles and identities', CustomContent: 'Explore and manage your content from the configured path', Settings: 'Configure the sensenet system', + System: 'Configure the sensenet system', ContentTemplates: 'Manage content templates', }, personalSettingsTitle: 'Edit personal settings', diff --git a/apps/sensenet/src/services/PersonalSettings.ts b/apps/sensenet/src/services/PersonalSettings.ts index 855b1d1c1..759424f03 100644 --- a/apps/sensenet/src/services/PersonalSettings.ts +++ b/apps/sensenet/src/services/PersonalSettings.ts @@ -29,7 +29,7 @@ export const BuiltInDrawerItemType = tuple( 'SavedQueries', 'Trash', 'UsersAndGroups', - 'Settings', + 'System', 'ContentTemplates', ) @@ -153,19 +153,19 @@ export const defaultSettings: PersonalSettingsType = { export class PersonalSettings { private checkDrawerItems(settings: Partial): Partial { if (settings.default?.drawer?.items?.find((i) => typeof i === 'string')) { - ;(settings.default.drawer.items as any) = undefined + ; (settings.default.drawer.items as any) = undefined } if (settings.desktop?.drawer?.items?.find((i) => typeof i === 'string')) { - ;(settings.desktop.drawer.items as any) = undefined + ; (settings.desktop.drawer.items as any) = undefined } if (settings.tablet?.drawer?.items?.find((i) => typeof i === 'string')) { - ;(settings.tablet.drawer.items as any) = undefined + ; (settings.tablet.drawer.items as any) = undefined } if (settings.mobile?.drawer?.items?.find((i) => typeof i === 'string')) { - ;(settings.mobile.drawer.items as any) = undefined + ; (settings.mobile.drawer.items as any) = undefined } return settings diff --git a/packages/sn-controls-react/src/fieldcontrols/rich-text-editor.tsx b/packages/sn-controls-react/src/fieldcontrols/rich-text-editor.tsx index 402ca15e5..a19c2fbc5 100644 --- a/packages/sn-controls-react/src/fieldcontrols/rich-text-editor.tsx +++ b/packages/sn-controls-react/src/fieldcontrols/rich-text-editor.tsx @@ -80,6 +80,11 @@ export const RichTextEditor: React.FC< readOnly={props.settings.ReadOnly} localization={props.localization?.richTextEditor} onChange={({ editor }) => { + if (props.settings.ControlHint === 'sn:RichText' || props.settings.ControlHint === 'sn:TipTapEditor') { + props.fieldOnChange?.(props.settings.Name, editor.getHTML()) + return + } + props.fieldOnChange?.(props.settings.Name, { text: editor.getHTML(), editor: JSON.stringify(editor.getJSON()), diff --git a/packages/sn-editor-react/src/components/controls/html-editor-control.tsx b/packages/sn-editor-react/src/components/controls/html-editor-control.tsx new file mode 100644 index 000000000..a410f737a --- /dev/null +++ b/packages/sn-editor-react/src/components/controls/html-editor-control.tsx @@ -0,0 +1,87 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + IconButtonProps, + Tooltip, +} from '@material-ui/core' +import CodeIcon from '@material-ui/icons/Code' +import { Editor } from '@tiptap/react' +import React, { FC, useState } from 'react' +import { useLocalization } from '../../hooks' +import { HtmlEditor } from '../html-editor' + +interface HTMLEditorControlProps { + editor: Editor + buttonProps?: Partial +} + +export const HTMLEditorControl: FC = ({ editor, buttonProps }) => { + const [open, setOpen] = useState(false) + const [html, setHtml] = useState(editor.getHTML()) + const localization = useLocalization() + + const handleClickOpen = () => { + setOpen(true) + } + + const handleClose = () => { + if (editor.getHTML() === html) { + setOpen(false) + return + } + + const confirmResult = window.confirm(localization.HTMLEditorControl.confirm) + if (!confirmResult) { + return + } + editor.commands.setContent(html) + + setOpen(false) + } + + return ( + <> + + handleClickOpen()} + color={editor.isActive('code') ? 'primary' : 'default'} + {...buttonProps}> + + html + + + { }}> + {localization.HTMLEditorControl.title} + + + + + + + + + + ) +} diff --git a/packages/sn-editor-react/src/components/controls/index.ts b/packages/sn-editor-react/src/components/controls/index.ts index e012cdc90..77af3fb13 100644 --- a/packages/sn-editor-react/src/components/controls/index.ts +++ b/packages/sn-editor-react/src/components/controls/index.ts @@ -2,3 +2,4 @@ export * from './image-control' export * from './link-control' export * from './table-control' export * from './typography-control' +export * from './html-editor-control' diff --git a/packages/sn-editor-react/src/components/html-editor.tsx b/packages/sn-editor-react/src/components/html-editor.tsx new file mode 100644 index 000000000..fb40a177d --- /dev/null +++ b/packages/sn-editor-react/src/components/html-editor.tsx @@ -0,0 +1,83 @@ +// import { InputLabel, Theme } from '@material-ui/core' +import React, { useEffect, useRef, useState } from 'react' +import MonacoEditor from 'react-monaco-editor' + +/** + * Field control that represents a HTMLEDITOr. + */ +export const HtmlEditor: React.FC<{ + initialState?: string + fieldOnChange?: (value: string) => void +}> = (props) => { + const [value, setValue] = useState(props.initialState) + + const editorRef = useRef(null) + const containerRef = useRef(null) + + useEffect(() => { + if (!editorRef.current) { + return + } + editorRef.current.editor!.onDidContentSizeChange(() => { + containerRef.current!.style.height = `${400}px` + }) + }, [editorRef]) + + const editorChangeHandler = (newValue: string) => { + setValue(newValue) + props.fieldOnChange?.(newValue) + } + + return ( +
+ { + monaco.editor.defineTheme('admin-ui-dark', { + base: 'vs-dark', + inherit: true, + rules: [], + colors: { + 'editor.background': '#121212', + }, + }) + }} + /> +
+ ) +} diff --git a/packages/sn-editor-react/src/components/menu-bar.tsx b/packages/sn-editor-react/src/components/menu-bar.tsx index 58fbf74c3..af6c52d68 100644 --- a/packages/sn-editor-react/src/components/menu-bar.tsx +++ b/packages/sn-editor-react/src/components/menu-bar.tsx @@ -15,7 +15,7 @@ import { Editor } from '@tiptap/react' import React, { FC } from 'react' import { useLocalization } from '../hooks' import { getCommonStyles } from '../styles' -import { ImageControl, LinkControl, TableControl, TypographyControl } from './controls' +import { HTMLEditorControl, ImageControl, LinkControl, TableControl, TypographyControl } from './controls' const useStyles = makeStyles((theme) => { const commonStyles = getCommonStyles(theme) @@ -201,6 +201,10 @@ export const MenuBar: FC = ({ editor }) => {
+
) } diff --git a/packages/sn-editor-react/src/context/localization-context.tsx b/packages/sn-editor-react/src/context/localization-context.tsx index afba7a666..4222d985c 100644 --- a/packages/sn-editor-react/src/context/localization-context.tsx +++ b/packages/sn-editor-react/src/context/localization-context.tsx @@ -23,6 +23,7 @@ export const defaultLocalization = { clearFormat: 'Clear format', undo: 'Undo', redo: 'Redo', + EditHtml: 'Edit HTML', }, bubbleMenu: { removeImage: 'remove image', @@ -51,6 +52,10 @@ export const defaultLocalization = { openInNewTab: 'Open link in a new tab', submit: 'Insert', }, + HTMLEditorControl: { + title: 'Edit HTML', + confirm: 'Would you like to save the changes?', + }, tableControl: { title: 'Insert table', rows: 'Rows', diff --git a/packages/sn-editor-react/test/bubble-menu.test.tsx b/packages/sn-editor-react/test/bubble-menu.test.tsx index 3f0601617..85acffb50 100644 --- a/packages/sn-editor-react/test/bubble-menu.test.tsx +++ b/packages/sn-editor-react/test/bubble-menu.test.tsx @@ -7,6 +7,17 @@ import { Editor } from '../src/components/editor' import { BubbleMenu } from './../src/components/bubble-menu' import { mockInstance } from './__mocks__/mokInstance' +jest.mock('react-monaco-editor', () => + jest.fn((props) => { + console.log(props) + return ( +
+ {props.value} +
+ ) + }), +) + describe('BubbleMenu', () => { it('should be rendered BubbleMenu', () => { const wrapper = mount() diff --git a/packages/sn-editor-react/test/editor.test.tsx b/packages/sn-editor-react/test/editor.test.tsx index 9fac49803..e78b27dea 100644 --- a/packages/sn-editor-react/test/editor.test.tsx +++ b/packages/sn-editor-react/test/editor.test.tsx @@ -8,6 +8,17 @@ import { Editor } from '../src/components/editor' import { MenuBar } from '../src/components/menu-bar' import { defaultLocalization } from '../src/context' +jest.mock('react-monaco-editor', () => + jest.fn((props) => { + console.log(props) + return ( +
+ {props.value} +
+ ) + }), +) + describe('editor', () => { it('should be rendered properly', () => { const wrapper = shallow() diff --git a/packages/sn-editor-react/test/image-control.test.tsx b/packages/sn-editor-react/test/image-control.test.tsx index d928f1428..59d84a8de 100644 --- a/packages/sn-editor-react/test/image-control.test.tsx +++ b/packages/sn-editor-react/test/image-control.test.tsx @@ -8,6 +8,15 @@ import { defaultLocalization } from '../src/context' import { createExtensions } from '../src/extension-list' import { FileReaderMock } from './__mocks__/file-reader' +jest.mock('react-monaco-editor', () => + jest.fn((props) => { + return ( +
+ {props.value} +
+ ) + }), +) describe('image control', () => { const onChange = jest.fn(({ editor }) => editor.getHTML()) const editor = new TiptapEditor({ diff --git a/packages/sn-editor-react/test/link-control.test.tsx b/packages/sn-editor-react/test/link-control.test.tsx index 8c017437d..5b72c1e05 100644 --- a/packages/sn-editor-react/test/link-control.test.tsx +++ b/packages/sn-editor-react/test/link-control.test.tsx @@ -7,6 +7,16 @@ import { LinkControl } from '../src/components/controls' import { defaultLocalization } from '../src/context' import { createExtensions } from '../src/extension-list' +jest.mock('react-monaco-editor', () => + jest.fn((props) => { + return ( +
+ {props.value} +
+ ) + }), +) + describe('link control', () => { const onChange = jest.fn(({ editor }) => editor.getHTML()) const editor = new TiptapEditor({ diff --git a/packages/sn-editor-react/test/table-control.test.tsx b/packages/sn-editor-react/test/table-control.test.tsx index a4e78b136..e5cf9d3c8 100644 --- a/packages/sn-editor-react/test/table-control.test.tsx +++ b/packages/sn-editor-react/test/table-control.test.tsx @@ -7,6 +7,17 @@ import { TableControl } from '../src/components/controls' import { defaultLocalization } from '../src/context' import { createExtensions } from '../src/extension-list' +jest.mock('react-monaco-editor', () => + jest.fn((props) => { + console.log(props) + return ( +
+ {props.value} +
+ ) + }), +) + describe('table control', () => { const onChange = jest.fn(({ editor }) => editor.getHTML()) const editor = new TiptapEditor({ diff --git a/packages/sn-editor-react/test/typography-control.test.tsx b/packages/sn-editor-react/test/typography-control.test.tsx index 6a20d4bf5..d6ba998e2 100644 --- a/packages/sn-editor-react/test/typography-control.test.tsx +++ b/packages/sn-editor-react/test/typography-control.test.tsx @@ -6,6 +6,17 @@ import { TypographyControl } from '../src/components/controls' import { defaultLocalization } from '../src/context' import { createExtensions } from '../src/extension-list' +jest.mock('react-monaco-editor', () => + jest.fn((props) => { + console.log(props) + return ( +
+ {props.value} +
+ ) + }), +) + describe('typography control', () => { const editor = new TiptapEditor({ content: '
Hello

world

', diff --git a/packages/sn-hooks-react/src/hooks/use-download.ts b/packages/sn-hooks-react/src/hooks/use-download.ts index a5a05019d..7bd80b564 100644 --- a/packages/sn-hooks-react/src/hooks/use-download.ts +++ b/packages/sn-hooks-react/src/hooks/use-download.ts @@ -10,6 +10,7 @@ export const fakeClick = (obj: EventTarget) => { export const downloadFile = (name: string, repositoryUrl: string) => { const saveLink = document.createElement('a') saveLink.href = `${repositoryUrl}${name}?download` + saveLink.target = '_blank'; fakeClick(saveLink) }