mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-19 20:31:10 +00:00
feat(ui): add RTL support for application (#14701)
* Minor: add RTL support for description * fix: unit test * add direction provider * sync local file * Update he-he.json * add useGridLayoutDirection custom hook to handle the RTL in grid layout * sync local * handle RTL in in feed editor * handle RTL for block editor * remove the RTL toggle button from the editor
This commit is contained in:
parent
d1a1003a9d
commit
3291b07001
@ -20,6 +20,7 @@ import 'react-toastify/dist/ReactToastify.min.css';
|
|||||||
import ApplicationConfigProvider from './components/ApplicationConfigProvider/ApplicationConfigProvider';
|
import ApplicationConfigProvider from './components/ApplicationConfigProvider/ApplicationConfigProvider';
|
||||||
import AppRouter from './components/AppRouter/AppRouter';
|
import AppRouter from './components/AppRouter/AppRouter';
|
||||||
import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider';
|
import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider';
|
||||||
|
import DirectionProvider from './components/DirectionProvider/DirectionProvider';
|
||||||
import DomainProvider from './components/Domain/DomainProvider/DomainProvider';
|
import DomainProvider from './components/Domain/DomainProvider/DomainProvider';
|
||||||
import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component';
|
import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component';
|
||||||
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
|
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
|
||||||
@ -43,27 +44,29 @@ const App: FC<AppProps> = ({ routeElements }) => {
|
|||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ApplicationConfigProvider routeElements={routeElements}>
|
<DirectionProvider>
|
||||||
<AuthProvider childComponentType={AppRouter}>
|
<ApplicationConfigProvider routeElements={routeElements}>
|
||||||
<TourProvider>
|
<AuthProvider childComponentType={AppRouter}>
|
||||||
<HelmetProvider>
|
<TourProvider>
|
||||||
<WebAnalyticsProvider>
|
<HelmetProvider>
|
||||||
<PermissionProvider>
|
<WebAnalyticsProvider>
|
||||||
<WebSocketProvider>
|
<PermissionProvider>
|
||||||
<GlobalSearchProvider>
|
<WebSocketProvider>
|
||||||
<DomainProvider>
|
<GlobalSearchProvider>
|
||||||
<EntityExportModalProvider>
|
<DomainProvider>
|
||||||
<AppRouter />
|
<EntityExportModalProvider>
|
||||||
</EntityExportModalProvider>
|
<AppRouter />
|
||||||
</DomainProvider>
|
</EntityExportModalProvider>
|
||||||
</GlobalSearchProvider>
|
</DomainProvider>
|
||||||
</WebSocketProvider>
|
</GlobalSearchProvider>
|
||||||
</PermissionProvider>
|
</WebSocketProvider>
|
||||||
</WebAnalyticsProvider>
|
</PermissionProvider>
|
||||||
</HelmetProvider>
|
</WebAnalyticsProvider>
|
||||||
</TourProvider>
|
</HelmetProvider>
|
||||||
</AuthProvider>
|
</TourProvider>
|
||||||
</ApplicationConfigProvider>
|
</AuthProvider>
|
||||||
|
</ApplicationConfigProvider>
|
||||||
|
</DirectionProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#555555" viewBox="0 0 18 18"> <polygon points="15 12 13 10 15 8 15 12"></polygon> <line x1="9" x2="5" y1="4" y2="4"></line> <path d="M5,3A3,3,0,0,0,5,9H6V3H5Z"></path> <rect height="11" width="1" x="5" y="4"></rect> <rect height="11" width="1" x="7" y="4"></rect> </svg>
|
|
||||||
|
Before Width: | Height: | Size: 316 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#555555" viewBox="0 0 18 18"> <polygon points="3 11 5 9 3 7 3 11"></polygon> <line x1="15" x2="11" y1="4" y2="4"></line> <path d="M11,3a3,3,0,0,0,0,6h1V3H11Z"></path> <rect height="11" width="1" x="11" y="4"></rect> <rect height="11" width="1" x="13" y="4"></rect> </svg>
|
|
||||||
|
Before Width: | Height: | Size: 317 B |
@ -18,6 +18,7 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EDITOR_OPTIONS } from '../../constants/BlockEditor.constants';
|
import { EDITOR_OPTIONS } from '../../constants/BlockEditor.constants';
|
||||||
import { formatContent } from '../../utils/BlockEditorUtils';
|
import { formatContent } from '../../utils/BlockEditorUtils';
|
||||||
import './block-editor.less';
|
import './block-editor.less';
|
||||||
@ -37,6 +38,7 @@ export interface BlockEditorProps {
|
|||||||
|
|
||||||
const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>(
|
const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>(
|
||||||
({ content = '', editable = true, onChange }, ref) => {
|
({ content = '', editable = true, onChange }, ref) => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
const editorSlots = useRef<EditorSlotsRef>(null);
|
const editorSlots = useRef<EditorSlotsRef>(null);
|
||||||
|
|
||||||
const editor = useCustomEditor({
|
const editor = useCustomEditor({
|
||||||
@ -89,6 +91,20 @@ const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>(
|
|||||||
setTimeout(() => editor.setEditable(editable));
|
setTimeout(() => editor.setEditable(editable));
|
||||||
}, [editable, editor]);
|
}, [editable, editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const editorWrapper = document.getElementById('block-editor-wrapper');
|
||||||
|
if (!editorWrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editorWrapper.setAttribute('dir', i18n.dir());
|
||||||
|
// text align right if rtl
|
||||||
|
if (i18n.dir() === 'rtl') {
|
||||||
|
editorWrapper.style.textAlign = 'right';
|
||||||
|
} else {
|
||||||
|
editorWrapper.style.textAlign = 'left';
|
||||||
|
}
|
||||||
|
}, [i18n]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block-editor-wrapper" id="block-editor-wrapper">
|
<div className="block-editor-wrapper" id="block-editor-wrapper">
|
||||||
<EditorContent
|
<EditorContent
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { NodeSelection, Plugin } from '@tiptap/pm/state';
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { EditorView, __serializeForClipboard } from '@tiptap/pm/view';
|
import { EditorView, __serializeForClipboard } from '@tiptap/pm/view';
|
||||||
import { isUndefined } from 'lodash';
|
import { isUndefined } from 'lodash';
|
||||||
|
import i18n from '../../../../utils/i18next/LocalUtil';
|
||||||
import { BlockAndDragHandleOptions } from './BlockAndDragDrop';
|
import { BlockAndDragHandleOptions } from './BlockAndDragDrop';
|
||||||
import { absoluteRect, nodeDOMAtCoords, nodePosAtDOM } from './helpers';
|
import { absoluteRect, nodeDOMAtCoords, nodePosAtDOM } from './helpers';
|
||||||
|
|
||||||
@ -128,7 +129,11 @@ export const BlockAndDragHandle = (options: BlockAndDragHandleOptions) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
if (i18n.dir() === 'rtl') {
|
||||||
|
dragHandleElement.style.right = `${rect.right - rect.width}px`;
|
||||||
|
} else {
|
||||||
|
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
||||||
|
}
|
||||||
dragHandleElement.style.top = `${rect.top}px`;
|
dragHandleElement.style.top = `${rect.top}px`;
|
||||||
showDragHandle();
|
showDragHandle();
|
||||||
};
|
};
|
||||||
@ -177,9 +182,15 @@ export const BlockAndDragHandle = (options: BlockAndDragHandleOptions) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockHandleElement.style.left = `${
|
if (i18n.dir() === 'rtl') {
|
||||||
rect.left - rect.width - options.blockHandleWidth
|
blockHandleElement.style.right = `${
|
||||||
}px`;
|
rect.right - rect.width - options.blockHandleWidth
|
||||||
|
}px`;
|
||||||
|
} else {
|
||||||
|
blockHandleElement.style.left = `${
|
||||||
|
rect.left - rect.width - options.blockHandleWidth
|
||||||
|
}px`;
|
||||||
|
}
|
||||||
blockHandleElement.style.top = `${rect.top}px`;
|
blockHandleElement.style.top = `${rect.top}px`;
|
||||||
showBlockHandle();
|
showBlockHandle();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const absoluteRect = (node: Element) => {
|
|||||||
top: data.top,
|
top: data.top,
|
||||||
left: data.left,
|
left: data.left,
|
||||||
width: data.width,
|
width: data.width,
|
||||||
|
right: data.right,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -451,3 +451,9 @@
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-editor-wrapper[dir='rtl'] {
|
||||||
|
.tiptap.ProseMirror-focused .is-node-empty.has-focus::before {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { AssetsType } from '../../../enums/entity.enum';
|
|||||||
import { Document } from '../../../generated/entity/docStore/document';
|
import { Document } from '../../../generated/entity/docStore/document';
|
||||||
import { EntityReference } from '../../../generated/entity/type';
|
import { EntityReference } from '../../../generated/entity/type';
|
||||||
import { PageType } from '../../../generated/system/ui/page';
|
import { PageType } from '../../../generated/system/ui/page';
|
||||||
|
import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection';
|
||||||
import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface';
|
import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface';
|
||||||
import '../../../pages/MyDataPage/my-data.less';
|
import '../../../pages/MyDataPage/my-data.less';
|
||||||
import { getUserById } from '../../../rest/userAPI';
|
import { getUserById } from '../../../rest/userAPI';
|
||||||
@ -249,6 +250,9 @@ function CustomizeMyData({
|
|||||||
fetchMyData();
|
fetchMyData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// call the hook to set the direction of the grid layout
|
||||||
|
useGridLayoutDirection();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActivityFeedProvider>
|
<ActivityFeedProvider>
|
||||||
<PageLayoutV1
|
<PageLayoutV1
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import React, { FC, ReactNode } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const DirectionProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
return <ConfigProvider direction={i18n.dir()}>{children}</ConfigProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DirectionProvider;
|
||||||
@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { debounce } from 'lodash';
|
import { debounce, isNil } from 'lodash';
|
||||||
import Emoji from 'quill-emoji';
|
import Emoji from 'quill-emoji';
|
||||||
import 'quill-emoji/dist/quill-emoji.css';
|
import 'quill-emoji/dist/quill-emoji.css';
|
||||||
import 'quill-mention';
|
import 'quill-mention';
|
||||||
@ -42,11 +42,7 @@ import {
|
|||||||
userMentionItemWithAvatar,
|
userMentionItemWithAvatar,
|
||||||
} from '../../utils/FeedUtils';
|
} from '../../utils/FeedUtils';
|
||||||
import { LinkBlot } from '../../utils/QuillLink/QuillLink';
|
import { LinkBlot } from '../../utils/QuillLink/QuillLink';
|
||||||
import {
|
import { insertMention, insertRef } from '../../utils/QuillUtils';
|
||||||
directionHandler,
|
|
||||||
insertMention,
|
|
||||||
insertRef,
|
|
||||||
} from '../../utils/QuillUtils';
|
|
||||||
import { getEntityIcon } from '../../utils/TableUtils';
|
import { getEntityIcon } from '../../utils/TableUtils';
|
||||||
import { useApplicationConfigContext } from '../ApplicationConfigProvider/ApplicationConfigProvider';
|
import { useApplicationConfigContext } from '../ApplicationConfigProvider/ApplicationConfigProvider';
|
||||||
import { editorRef } from '../common/RichTextEditor/RichTextEditor.interface';
|
import { editorRef } from '../common/RichTextEditor/RichTextEditor.interface';
|
||||||
@ -74,7 +70,7 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
|
|||||||
}: FeedEditorProp,
|
}: FeedEditorProp,
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const editorRef = useRef<ReactQuill>(null);
|
const editorRef = useRef<ReactQuill>(null);
|
||||||
const [value, setValue] = useState<string>(defaultValue ?? '');
|
const [value, setValue] = useState<string>(defaultValue ?? '');
|
||||||
const [isMentionListOpen, toggleMentionList] = useState(false);
|
const [isMentionListOpen, toggleMentionList] = useState(false);
|
||||||
@ -180,7 +176,6 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
|
|||||||
handlers: {
|
handlers: {
|
||||||
insertMention: insertMention,
|
insertMention: insertMention,
|
||||||
insertRef: insertRef,
|
insertRef: insertRef,
|
||||||
direction: directionHandler,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'emoji-toolbar': true,
|
'emoji-toolbar': true,
|
||||||
@ -281,6 +276,29 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
|
|||||||
}
|
}
|
||||||
}, [focused, editorRef]);
|
}, [focused, editorRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// get the editor container
|
||||||
|
const container = document.getElementById('om-quill-editor');
|
||||||
|
|
||||||
|
if (container && editorRef.current) {
|
||||||
|
// get the editor instance
|
||||||
|
const editorInstance = editorRef.current.getEditor();
|
||||||
|
const direction = i18n.dir();
|
||||||
|
|
||||||
|
// get the current direction of the editor
|
||||||
|
const { align } = editorInstance.getFormat();
|
||||||
|
|
||||||
|
if (direction === 'rtl' && isNil(align)) {
|
||||||
|
container.setAttribute('data-dir', direction);
|
||||||
|
editorInstance.format('align', 'right', 'user');
|
||||||
|
} else if (align === 'right') {
|
||||||
|
editorInstance.format('align', false, 'user');
|
||||||
|
container.setAttribute('data-dir', 'ltr');
|
||||||
|
}
|
||||||
|
editorInstance.format('direction', direction, 'user');
|
||||||
|
}
|
||||||
|
}, [i18n, editorRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className}
|
className={className}
|
||||||
|
|||||||
@ -11,8 +11,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import LTRIcon from '../../../assets/svg/ic-ltr.svg';
|
|
||||||
import RTLIcon from '../../../assets/svg/ic-rtl.svg';
|
|
||||||
import MarkdownIcon from '../../../assets/svg/markdown.svg';
|
import MarkdownIcon from '../../../assets/svg/markdown.svg';
|
||||||
import i18n from '../../../utils/i18next/LocalUtil';
|
import i18n from '../../../utils/i18next/LocalUtil';
|
||||||
|
|
||||||
@ -43,55 +41,6 @@ const markdownButton = (): HTMLButtonElement => {
|
|||||||
return button;
|
return button;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRTLButtonIcon = (mode: 'rtl' | 'ltr') => `
|
|
||||||
<img
|
|
||||||
alt="rtl-icon"
|
|
||||||
class="svg-icon"
|
|
||||||
height="24px"
|
|
||||||
width="24px"
|
|
||||||
src="${mode === 'rtl' ? RTLIcon : LTRIcon}" />`;
|
|
||||||
|
|
||||||
const toggleEditorDirection = (button: HTMLButtonElement) => {
|
|
||||||
const editorElement = document.querySelector(
|
|
||||||
'.toastui-editor.md-mode.active'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (editorElement) {
|
|
||||||
const editorElementDir = editorElement.getAttribute('dir');
|
|
||||||
const newDir = editorElementDir === 'rtl' ? 'ltr' : 'rtl';
|
|
||||||
const textAlign = newDir === 'rtl' ? 'right' : 'left';
|
|
||||||
|
|
||||||
editorElement.setAttribute('dir', newDir);
|
|
||||||
editorElement.setAttribute('style', `text-align: ${textAlign};`);
|
|
||||||
button.innerHTML = getRTLButtonIcon(newDir === 'rtl' ? 'ltr' : 'rtl');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rtlButton = (): HTMLButtonElement => {
|
|
||||||
const button = document.createElement('button');
|
|
||||||
|
|
||||||
button.onclick = () => toggleEditorDirection(button);
|
|
||||||
|
|
||||||
button.className = 'toastui-editor-toolbar-icons rtl-icon';
|
|
||||||
button.id = 'rtl-button';
|
|
||||||
button.style.cssText = 'background-image: none; margin: 0; margin-top: 4px;';
|
|
||||||
button.type = 'button';
|
|
||||||
button.innerHTML = getRTLButtonIcon('rtl');
|
|
||||||
|
|
||||||
return button;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rtlButtonUpdateHandler = (toolbarState: {
|
|
||||||
active: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
}) => {
|
|
||||||
const rtlButtonElement = document.getElementById('rtl-button');
|
|
||||||
if (rtlButtonElement) {
|
|
||||||
(rtlButtonElement as HTMLButtonElement).disabled =
|
|
||||||
toolbarState.disabled || false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EDITOR_TOOLBAR_ITEMS = [
|
export const EDITOR_TOOLBAR_ITEMS = [
|
||||||
'heading',
|
'heading',
|
||||||
'bold',
|
'bold',
|
||||||
@ -104,12 +53,6 @@ export const EDITOR_TOOLBAR_ITEMS = [
|
|||||||
'quote',
|
'quote',
|
||||||
'code',
|
'code',
|
||||||
'codeblock',
|
'codeblock',
|
||||||
{
|
|
||||||
name: i18n.t('label.rtl-ltr-direction'),
|
|
||||||
el: rtlButton(),
|
|
||||||
tooltip: i18n.t('label.rtl-ltr-direction'),
|
|
||||||
onUpdated: rtlButtonUpdateHandler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: i18n.t('label.markdown-guide'),
|
name: i18n.t('label.markdown-guide'),
|
||||||
el: markdownButton(),
|
el: markdownButton(),
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import i18n from '../../../utils/i18next/LocalUtil';
|
||||||
import { EDITOR_TOOLBAR_ITEMS } from './EditorToolBar';
|
import { EDITOR_TOOLBAR_ITEMS } from './EditorToolBar';
|
||||||
import './rich-text-editor.less';
|
import './rich-text-editor.less';
|
||||||
import { editorRef, RichTextEditorProp } from './RichTextEditor.interface';
|
import { editorRef, RichTextEditorProp } from './RichTextEditor.interface';
|
||||||
@ -72,10 +73,34 @@ const RichTextEditor = forwardRef<editorRef, RichTextEditorProp>(
|
|||||||
setEditorValue(initialValue);
|
setEditorValue(initialValue);
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
|
// handle the direction of the editor
|
||||||
|
useEffect(() => {
|
||||||
|
const dir = i18n.dir();
|
||||||
|
const editorElement = document.querySelector('.toastui-editor.md-mode');
|
||||||
|
const previewElement = document.querySelector(
|
||||||
|
'.toastui-editor-md-preview'
|
||||||
|
);
|
||||||
|
const textAlign = dir === 'rtl' ? 'right' : 'left';
|
||||||
|
if (editorElement) {
|
||||||
|
editorElement.setAttribute('dir', dir);
|
||||||
|
editorElement.setAttribute('style', `text-align: ${textAlign};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewElement) {
|
||||||
|
previewElement.setAttribute('dir', dir);
|
||||||
|
previewElement.setAttribute('style', `text-align: ${textAlign};`);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(className)} style={style}>
|
<div className={classNames(className)} style={style}>
|
||||||
{readonly ? (
|
{readonly ? (
|
||||||
<div className="border p-xs rounded-4" data-testid="viewer">
|
<div
|
||||||
|
className={classNames('border p-xs rounded-4', {
|
||||||
|
'text-right': i18n.dir() === 'rtl',
|
||||||
|
})}
|
||||||
|
data-testid="viewer"
|
||||||
|
dir={i18n.dir()}>
|
||||||
<Viewer
|
<Viewer
|
||||||
extendedAutolinks={extendedAutolinks}
|
extendedAutolinks={extendedAutolinks}
|
||||||
initialValue={editorValue}
|
initialValue={editorValue}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const RichTextEditorPreviewer = ({
|
|||||||
showReadMoreBtn = true,
|
showReadMoreBtn = true,
|
||||||
maxLength = DESCRIPTION_MAX_PREVIEW_CHARACTERS,
|
maxLength = DESCRIPTION_MAX_PREVIEW_CHARACTERS,
|
||||||
}: PreviewerProp) => {
|
}: PreviewerProp) => {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const [content, setContent] = useState<string>('');
|
const [content, setContent] = useState<string>('');
|
||||||
|
|
||||||
// initially read more will be false
|
// initially read more will be false
|
||||||
@ -97,8 +97,11 @@ const RichTextEditorPreviewer = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('rich-text-editor-container', className)}
|
className={classNames('rich-text-editor-container', className, {
|
||||||
data-testid="viewer-container">
|
'text-right': i18n.dir() === 'rtl',
|
||||||
|
})}
|
||||||
|
data-testid="viewer-container"
|
||||||
|
dir={i18n.dir()}>
|
||||||
<div
|
<div
|
||||||
className={classNames('markdown-parser', textVariant)}
|
className={classNames('markdown-parser', textVariant)}
|
||||||
data-testid="markdown-parser">
|
data-testid="markdown-parser">
|
||||||
|
|||||||
@ -45,7 +45,6 @@ export const TOOLBAR_ITEMS = [
|
|||||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||||
['link'],
|
['link'],
|
||||||
['insertMention', 'insertRef', 'emoji'],
|
['insertMention', 'insertRef', 'emoji'],
|
||||||
[{ direction: 'rtl' }],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export enum TaskOperation {
|
export enum TaskOperation {
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGridLayoutDirection } from './useGridLayoutDirection';
|
||||||
|
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: jest.fn().mockImplementation(() => ({
|
||||||
|
i18n: {
|
||||||
|
dir: jest.fn(),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useGridLayoutDirection hook', () => {
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let child: HTMLDivElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'react-grid-layout';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
child = document.createElement('div');
|
||||||
|
child.className = 'react-grid-item';
|
||||||
|
container.appendChild(child);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.removeChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change direction if isLoading is true', () => {
|
||||||
|
renderHook(() => useGridLayoutDirection(true));
|
||||||
|
|
||||||
|
expect(container.getAttribute('dir')).toBeNull();
|
||||||
|
expect(child.getAttribute('dir')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set direction to ltr for container and i18n direction for children if isLoading is false', () => {
|
||||||
|
(useTranslation as jest.Mock).mockImplementationOnce(() => ({
|
||||||
|
i18n: {
|
||||||
|
dir: jest.fn().mockReturnValue('rtl'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
renderHook(() => useGridLayoutDirection(false));
|
||||||
|
|
||||||
|
expect(container.getAttribute('dir')).toBe('ltr');
|
||||||
|
expect(child.getAttribute('dir')).toBe('rtl');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const useGridLayoutDirection = (isLoading = false) => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
const gridLayoutContainer = document.querySelector('.react-grid-layout');
|
||||||
|
const children = document.querySelectorAll('.react-grid-item');
|
||||||
|
|
||||||
|
if (gridLayoutContainer && children) {
|
||||||
|
// parent container should be ltr to avoid RTL issues
|
||||||
|
gridLayoutContainer.setAttribute('dir', 'ltr');
|
||||||
|
|
||||||
|
// children should change direction based on i18n direction
|
||||||
|
children.forEach((child) => child.setAttribute('dir', i18n.dir()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [i18n, isLoading]);
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ import { AssetsType, EntityType } from '../../enums/entity.enum';
|
|||||||
import { Thread } from '../../generated/entity/feed/thread';
|
import { Thread } from '../../generated/entity/feed/thread';
|
||||||
import { PageType } from '../../generated/system/ui/page';
|
import { PageType } from '../../generated/system/ui/page';
|
||||||
import { EntityReference } from '../../generated/type/entityReference';
|
import { EntityReference } from '../../generated/type/entityReference';
|
||||||
|
import { useGridLayoutDirection } from '../../hooks/useGridLayoutDirection';
|
||||||
import { getDocumentByFQN } from '../../rest/DocStoreAPI';
|
import { getDocumentByFQN } from '../../rest/DocStoreAPI';
|
||||||
import { getActiveAnnouncement } from '../../rest/feedsAPI';
|
import { getActiveAnnouncement } from '../../rest/feedsAPI';
|
||||||
import { getUserById } from '../../rest/userAPI';
|
import { getUserById } from '../../rest/userAPI';
|
||||||
@ -190,6 +191,9 @@ const MyDataPage = () => {
|
|||||||
fetchAnnouncements();
|
fetchAnnouncements();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// call the hook to set the direction of the grid layout
|
||||||
|
useGridLayoutDirection(isLoading);
|
||||||
|
|
||||||
if (showWelcomeScreen) {
|
if (showWelcomeScreen) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white full-height">
|
<div className="bg-white full-height">
|
||||||
|
|||||||
@ -89,6 +89,7 @@ jest.mock('utils/i18next/LocalUtil', () => ({
|
|||||||
t: (key) => key,
|
t: (key) => key,
|
||||||
}),
|
}),
|
||||||
t: (key) => key,
|
t: (key) => key,
|
||||||
|
dir: jest.fn().mockReturnValue('ltr'),
|
||||||
}));
|
}));
|
||||||
/**
|
/**
|
||||||
* mock react-i18next
|
* mock react-i18next
|
||||||
@ -97,7 +98,7 @@ jest.mock('react-i18next', () => ({
|
|||||||
...jest.requireActual('react-i18next'),
|
...jest.requireActual('react-i18next'),
|
||||||
useTranslation: jest.fn().mockReturnValue({
|
useTranslation: jest.fn().mockReturnValue({
|
||||||
t: (key) => key,
|
t: (key) => key,
|
||||||
i18n: { language: 'en-US' },
|
i18n: { language: 'en-US', dir: jest.fn().mockReturnValue('ltr') },
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -21,19 +21,3 @@ export function insertRef() {
|
|||||||
const ref = this.quill.getModule('mention');
|
const ref = this.quill.getModule('mention');
|
||||||
ref.openMenu('#');
|
ref.openMenu('#');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function directionHandler(value) {
|
|
||||||
const { align } = this.quill.getFormat();
|
|
||||||
|
|
||||||
// get the editor container
|
|
||||||
const container = document.getElementById('om-quill-editor');
|
|
||||||
|
|
||||||
if (value === 'rtl' && align == null) {
|
|
||||||
container.setAttribute('data-dir', value);
|
|
||||||
this.quill.format('align', 'right', 'user');
|
|
||||||
} else if (!value && align === 'right') {
|
|
||||||
this.quill.format('align', false, 'user');
|
|
||||||
container.setAttribute('data-dir', 'ltr');
|
|
||||||
}
|
|
||||||
this.quill.format('direction', value, 'user');
|
|
||||||
}
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import deDe from '../../locale/languages/de-de.json';
|
|||||||
import enUS from '../../locale/languages/en-us.json';
|
import enUS from '../../locale/languages/en-us.json';
|
||||||
import esES from '../../locale/languages/es-es.json';
|
import esES from '../../locale/languages/es-es.json';
|
||||||
import frFR from '../../locale/languages/fr-fr.json';
|
import frFR from '../../locale/languages/fr-fr.json';
|
||||||
|
import heHE from '../../locale/languages/he-he.json';
|
||||||
import jaJP from '../../locale/languages/ja-jp.json';
|
import jaJP from '../../locale/languages/ja-jp.json';
|
||||||
import nlNL from '../../locale/languages/nl-nl.json';
|
import nlNL from '../../locale/languages/nl-nl.json';
|
||||||
import ptBR from '../../locale/languages/pt-br.json';
|
import ptBR from '../../locale/languages/pt-br.json';
|
||||||
@ -32,6 +33,7 @@ export enum SupportedLocales {
|
|||||||
Español = 'es-ES',
|
Español = 'es-ES',
|
||||||
Русский = 'ru-RU',
|
Русский = 'ru-RU',
|
||||||
Deutsh = 'de-DE',
|
Deutsh = 'de-DE',
|
||||||
|
Hebrew = 'he-HE',
|
||||||
Nederlands = 'nl-NL',
|
Nederlands = 'nl-NL',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ export const getInitOptions = (): InitOptions => {
|
|||||||
'es-ES': { translation: esES },
|
'es-ES': { translation: esES },
|
||||||
'ru-RU': { translation: ruRU },
|
'ru-RU': { translation: ruRU },
|
||||||
'de-DE': { translation: deDe },
|
'de-DE': { translation: deDe },
|
||||||
|
'he-HE': { translation: heHE },
|
||||||
'nl-NL': { translation: nlNL },
|
'nl-NL': { translation: nlNL },
|
||||||
},
|
},
|
||||||
fallbackLng: ['en-US'],
|
fallbackLng: ['en-US'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user