mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-11-03 20:19:31 +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 AppRouter from './components/AppRouter/AppRouter';
 | 
			
		||||
import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider';
 | 
			
		||||
import DirectionProvider from './components/DirectionProvider/DirectionProvider';
 | 
			
		||||
import DomainProvider from './components/Domain/DomainProvider/DomainProvider';
 | 
			
		||||
import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component';
 | 
			
		||||
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
 | 
			
		||||
@ -43,27 +44,29 @@ const App: FC<AppProps> = ({ routeElements }) => {
 | 
			
		||||
        <Router history={history}>
 | 
			
		||||
          <I18nextProvider i18n={i18n}>
 | 
			
		||||
            <ErrorBoundary>
 | 
			
		||||
              <ApplicationConfigProvider routeElements={routeElements}>
 | 
			
		||||
                <AuthProvider childComponentType={AppRouter}>
 | 
			
		||||
                  <TourProvider>
 | 
			
		||||
                    <HelmetProvider>
 | 
			
		||||
                      <WebAnalyticsProvider>
 | 
			
		||||
                        <PermissionProvider>
 | 
			
		||||
                          <WebSocketProvider>
 | 
			
		||||
                            <GlobalSearchProvider>
 | 
			
		||||
                              <DomainProvider>
 | 
			
		||||
                                <EntityExportModalProvider>
 | 
			
		||||
                                  <AppRouter />
 | 
			
		||||
                                </EntityExportModalProvider>
 | 
			
		||||
                              </DomainProvider>
 | 
			
		||||
                            </GlobalSearchProvider>
 | 
			
		||||
                          </WebSocketProvider>
 | 
			
		||||
                        </PermissionProvider>
 | 
			
		||||
                      </WebAnalyticsProvider>
 | 
			
		||||
                    </HelmetProvider>
 | 
			
		||||
                  </TourProvider>
 | 
			
		||||
                </AuthProvider>
 | 
			
		||||
              </ApplicationConfigProvider>
 | 
			
		||||
              <DirectionProvider>
 | 
			
		||||
                <ApplicationConfigProvider routeElements={routeElements}>
 | 
			
		||||
                  <AuthProvider childComponentType={AppRouter}>
 | 
			
		||||
                    <TourProvider>
 | 
			
		||||
                      <HelmetProvider>
 | 
			
		||||
                        <WebAnalyticsProvider>
 | 
			
		||||
                          <PermissionProvider>
 | 
			
		||||
                            <WebSocketProvider>
 | 
			
		||||
                              <GlobalSearchProvider>
 | 
			
		||||
                                <DomainProvider>
 | 
			
		||||
                                  <EntityExportModalProvider>
 | 
			
		||||
                                    <AppRouter />
 | 
			
		||||
                                  </EntityExportModalProvider>
 | 
			
		||||
                                </DomainProvider>
 | 
			
		||||
                              </GlobalSearchProvider>
 | 
			
		||||
                            </WebSocketProvider>
 | 
			
		||||
                          </PermissionProvider>
 | 
			
		||||
                        </WebAnalyticsProvider>
 | 
			
		||||
                      </HelmetProvider>
 | 
			
		||||
                    </TourProvider>
 | 
			
		||||
                  </AuthProvider>
 | 
			
		||||
                </ApplicationConfigProvider>
 | 
			
		||||
              </DirectionProvider>
 | 
			
		||||
            </ErrorBoundary>
 | 
			
		||||
          </I18nextProvider>
 | 
			
		||||
        </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,
 | 
			
		||||
  useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { EDITOR_OPTIONS } from '../../constants/BlockEditor.constants';
 | 
			
		||||
import { formatContent } from '../../utils/BlockEditorUtils';
 | 
			
		||||
import './block-editor.less';
 | 
			
		||||
@ -37,6 +38,7 @@ export interface BlockEditorProps {
 | 
			
		||||
 | 
			
		||||
const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>(
 | 
			
		||||
  ({ content = '', editable = true, onChange }, ref) => {
 | 
			
		||||
    const { i18n } = useTranslation();
 | 
			
		||||
    const editorSlots = useRef<EditorSlotsRef>(null);
 | 
			
		||||
 | 
			
		||||
    const editor = useCustomEditor({
 | 
			
		||||
@ -89,6 +91,20 @@ const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>(
 | 
			
		||||
      setTimeout(() => editor.setEditable(editable));
 | 
			
		||||
    }, [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 (
 | 
			
		||||
      <div className="block-editor-wrapper" id="block-editor-wrapper">
 | 
			
		||||
        <EditorContent
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ import { NodeSelection, Plugin } from '@tiptap/pm/state';
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { EditorView, __serializeForClipboard } from '@tiptap/pm/view';
 | 
			
		||||
import { isUndefined } from 'lodash';
 | 
			
		||||
import i18n from '../../../../utils/i18next/LocalUtil';
 | 
			
		||||
import { BlockAndDragHandleOptions } from './BlockAndDragDrop';
 | 
			
		||||
import { absoluteRect, nodeDOMAtCoords, nodePosAtDOM } from './helpers';
 | 
			
		||||
 | 
			
		||||
@ -128,7 +129,11 @@ export const BlockAndDragHandle = (options: BlockAndDragHandleOptions) => {
 | 
			
		||||
      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`;
 | 
			
		||||
    showDragHandle();
 | 
			
		||||
  };
 | 
			
		||||
@ -177,9 +182,15 @@ export const BlockAndDragHandle = (options: BlockAndDragHandleOptions) => {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    blockHandleElement.style.left = `${
 | 
			
		||||
      rect.left - rect.width - options.blockHandleWidth
 | 
			
		||||
    }px`;
 | 
			
		||||
    if (i18n.dir() === 'rtl') {
 | 
			
		||||
      blockHandleElement.style.right = `${
 | 
			
		||||
        rect.right - rect.width - options.blockHandleWidth
 | 
			
		||||
      }px`;
 | 
			
		||||
    } else {
 | 
			
		||||
      blockHandleElement.style.left = `${
 | 
			
		||||
        rect.left - rect.width - options.blockHandleWidth
 | 
			
		||||
      }px`;
 | 
			
		||||
    }
 | 
			
		||||
    blockHandleElement.style.top = `${rect.top}px`;
 | 
			
		||||
    showBlockHandle();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ export const absoluteRect = (node: Element) => {
 | 
			
		||||
    top: data.top,
 | 
			
		||||
    left: data.left,
 | 
			
		||||
    width: data.width,
 | 
			
		||||
    right: data.right,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -451,3 +451,9 @@
 | 
			
		||||
    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 { EntityReference } from '../../../generated/entity/type';
 | 
			
		||||
import { PageType } from '../../../generated/system/ui/page';
 | 
			
		||||
import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection';
 | 
			
		||||
import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface';
 | 
			
		||||
import '../../../pages/MyDataPage/my-data.less';
 | 
			
		||||
import { getUserById } from '../../../rest/userAPI';
 | 
			
		||||
@ -249,6 +250,9 @@ function CustomizeMyData({
 | 
			
		||||
    fetchMyData();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  // call the hook to set the direction of the grid layout
 | 
			
		||||
  useGridLayoutDirection();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ActivityFeedProvider>
 | 
			
		||||
      <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 { debounce } from 'lodash';
 | 
			
		||||
import { debounce, isNil } from 'lodash';
 | 
			
		||||
import Emoji from 'quill-emoji';
 | 
			
		||||
import 'quill-emoji/dist/quill-emoji.css';
 | 
			
		||||
import 'quill-mention';
 | 
			
		||||
@ -42,11 +42,7 @@ import {
 | 
			
		||||
  userMentionItemWithAvatar,
 | 
			
		||||
} from '../../utils/FeedUtils';
 | 
			
		||||
import { LinkBlot } from '../../utils/QuillLink/QuillLink';
 | 
			
		||||
import {
 | 
			
		||||
  directionHandler,
 | 
			
		||||
  insertMention,
 | 
			
		||||
  insertRef,
 | 
			
		||||
} from '../../utils/QuillUtils';
 | 
			
		||||
import { insertMention, insertRef } from '../../utils/QuillUtils';
 | 
			
		||||
import { getEntityIcon } from '../../utils/TableUtils';
 | 
			
		||||
import { useApplicationConfigContext } from '../ApplicationConfigProvider/ApplicationConfigProvider';
 | 
			
		||||
import { editorRef } from '../common/RichTextEditor/RichTextEditor.interface';
 | 
			
		||||
@ -74,7 +70,7 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
 | 
			
		||||
    }: FeedEditorProp,
 | 
			
		||||
    ref
 | 
			
		||||
  ) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { t, i18n } = useTranslation();
 | 
			
		||||
    const editorRef = useRef<ReactQuill>(null);
 | 
			
		||||
    const [value, setValue] = useState<string>(defaultValue ?? '');
 | 
			
		||||
    const [isMentionListOpen, toggleMentionList] = useState(false);
 | 
			
		||||
@ -180,7 +176,6 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
 | 
			
		||||
          handlers: {
 | 
			
		||||
            insertMention: insertMention,
 | 
			
		||||
            insertRef: insertRef,
 | 
			
		||||
            direction: directionHandler,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        'emoji-toolbar': true,
 | 
			
		||||
@ -281,6 +276,29 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
 | 
			
		||||
      }
 | 
			
		||||
    }, [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 (
 | 
			
		||||
      <div
 | 
			
		||||
        className={className}
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,6 @@
 | 
			
		||||
 *  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 i18n from '../../../utils/i18next/LocalUtil';
 | 
			
		||||
 | 
			
		||||
@ -43,55 +41,6 @@ const markdownButton = (): HTMLButtonElement => {
 | 
			
		||||
  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 = [
 | 
			
		||||
  'heading',
 | 
			
		||||
  'bold',
 | 
			
		||||
@ -104,12 +53,6 @@ export const EDITOR_TOOLBAR_ITEMS = [
 | 
			
		||||
  'quote',
 | 
			
		||||
  'code',
 | 
			
		||||
  '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'),
 | 
			
		||||
    el: markdownButton(),
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ import React, {
 | 
			
		||||
  useImperativeHandle,
 | 
			
		||||
  useState,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import i18n from '../../../utils/i18next/LocalUtil';
 | 
			
		||||
import { EDITOR_TOOLBAR_ITEMS } from './EditorToolBar';
 | 
			
		||||
import './rich-text-editor.less';
 | 
			
		||||
import { editorRef, RichTextEditorProp } from './RichTextEditor.interface';
 | 
			
		||||
@ -72,10 +73,34 @@ const RichTextEditor = forwardRef<editorRef, RichTextEditorProp>(
 | 
			
		||||
      setEditorValue(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 (
 | 
			
		||||
      <div className={classNames(className)} style={style}>
 | 
			
		||||
        {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
 | 
			
		||||
              extendedAutolinks={extendedAutolinks}
 | 
			
		||||
              initialValue={editorValue}
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ const RichTextEditorPreviewer = ({
 | 
			
		||||
  showReadMoreBtn = true,
 | 
			
		||||
  maxLength = DESCRIPTION_MAX_PREVIEW_CHARACTERS,
 | 
			
		||||
}: PreviewerProp) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { t, i18n } = useTranslation();
 | 
			
		||||
  const [content, setContent] = useState<string>('');
 | 
			
		||||
 | 
			
		||||
  // initially read more will be false
 | 
			
		||||
@ -97,8 +97,11 @@ const RichTextEditorPreviewer = ({
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames('rich-text-editor-container', className)}
 | 
			
		||||
      data-testid="viewer-container">
 | 
			
		||||
      className={classNames('rich-text-editor-container', className, {
 | 
			
		||||
        'text-right': i18n.dir() === 'rtl',
 | 
			
		||||
      })}
 | 
			
		||||
      data-testid="viewer-container"
 | 
			
		||||
      dir={i18n.dir()}>
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames('markdown-parser', textVariant)}
 | 
			
		||||
        data-testid="markdown-parser">
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,6 @@ export const TOOLBAR_ITEMS = [
 | 
			
		||||
  [{ list: 'ordered' }, { list: 'bullet' }],
 | 
			
		||||
  ['link'],
 | 
			
		||||
  ['insertMention', 'insertRef', 'emoji'],
 | 
			
		||||
  [{ direction: 'rtl' }],
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
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 { PageType } from '../../generated/system/ui/page';
 | 
			
		||||
import { EntityReference } from '../../generated/type/entityReference';
 | 
			
		||||
import { useGridLayoutDirection } from '../../hooks/useGridLayoutDirection';
 | 
			
		||||
import { getDocumentByFQN } from '../../rest/DocStoreAPI';
 | 
			
		||||
import { getActiveAnnouncement } from '../../rest/feedsAPI';
 | 
			
		||||
import { getUserById } from '../../rest/userAPI';
 | 
			
		||||
@ -190,6 +191,9 @@ const MyDataPage = () => {
 | 
			
		||||
    fetchAnnouncements();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  // call the hook to set the direction of the grid layout
 | 
			
		||||
  useGridLayoutDirection(isLoading);
 | 
			
		||||
 | 
			
		||||
  if (showWelcomeScreen) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="bg-white full-height">
 | 
			
		||||
 | 
			
		||||
@ -89,6 +89,7 @@ jest.mock('utils/i18next/LocalUtil', () => ({
 | 
			
		||||
    t: (key) => key,
 | 
			
		||||
  }),
 | 
			
		||||
  t: (key) => key,
 | 
			
		||||
  dir: jest.fn().mockReturnValue('ltr'),
 | 
			
		||||
}));
 | 
			
		||||
/**
 | 
			
		||||
 * mock react-i18next
 | 
			
		||||
@ -97,7 +98,7 @@ jest.mock('react-i18next', () => ({
 | 
			
		||||
  ...jest.requireActual('react-i18next'),
 | 
			
		||||
  useTranslation: jest.fn().mockReturnValue({
 | 
			
		||||
    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');
 | 
			
		||||
  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 esES from '../../locale/languages/es-es.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 nlNL from '../../locale/languages/nl-nl.json';
 | 
			
		||||
import ptBR from '../../locale/languages/pt-br.json';
 | 
			
		||||
@ -32,6 +33,7 @@ export enum SupportedLocales {
 | 
			
		||||
  Español = 'es-ES',
 | 
			
		||||
  Русский = 'ru-RU',
 | 
			
		||||
  Deutsh = 'de-DE',
 | 
			
		||||
  Hebrew = 'he-HE',
 | 
			
		||||
  Nederlands = 'nl-NL',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -53,6 +55,7 @@ export const getInitOptions = (): InitOptions => {
 | 
			
		||||
      'es-ES': { translation: esES },
 | 
			
		||||
      'ru-RU': { translation: ruRU },
 | 
			
		||||
      'de-DE': { translation: deDe },
 | 
			
		||||
      'he-HE': { translation: heHE },
 | 
			
		||||
      'nl-NL': { translation: nlNL },
 | 
			
		||||
    },
 | 
			
		||||
    fallbackLng: ['en-US'],
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user