diff --git a/.github/playwright/impact-map.generated.json b/.github/playwright/impact-map.generated.json index a5c2053bdd35..8b2d5e9053e8 100644 --- a/.github/playwright/impact-map.generated.json +++ b/.github/playwright/impact-map.generated.json @@ -3328,6 +3328,7 @@ "playwright/e2e/Features/AdvancedSearch.spec.ts", "playwright/e2e/Features/ContextCenterDashboard.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts", + "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DomainFilterQueryFilter.spec.ts", "playwright/e2e/Features/LandingPageWidgets/LandingPageContextCenter.spec.ts", @@ -8668,6 +8669,7 @@ "openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx" ], "specs": [ + "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/StorageMetadataAgentForm.spec.ts" ] }, @@ -9260,6 +9262,21 @@ "playwright/e2e/Pages/Users.spec.ts" ] }, + { + "sources": [ + "openmetadata-ui/src/main/resources/ui/src/components/common/CommentComposer/CommentComposer.tsx" + ], + "specs": [ + "playwright/e2e/Features/AIMode/InboxActivity.spec.ts", + "playwright/e2e/Features/AIMode/InboxTasks.spec.ts", + "playwright/e2e/Features/ActivityAPI.spec.ts", + "playwright/e2e/Features/ActivityFeed.spec.ts", + "playwright/e2e/Features/ContextCenterArticles.spec.ts", + "playwright/e2e/Features/ContextCenterPermission.spec.ts", + "playwright/e2e/Features/IncidentManager.spec.ts", + "playwright/e2e/Features/Tasks/TaskComments.spec.ts" + ] + }, { "sources": [ "openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertiesWidget/CustomPropertiesWidgetEditor.tsx" @@ -10350,21 +10367,6 @@ "playwright/e2e/Features/AIMode/InboxTasks.spec.ts" ] }, - { - "sources": [ - "openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/InboxCommentComposer.tsx" - ], - "specs": [ - "playwright/e2e/Features/AIMode/InboxActivity.spec.ts", - "playwright/e2e/Features/AIMode/InboxTasks.spec.ts", - "playwright/e2e/Features/ActivityAPI.spec.ts", - "playwright/e2e/Features/ActivityFeed.spec.ts", - "playwright/e2e/Features/ContextCenterArticles.spec.ts", - "playwright/e2e/Features/ContextCenterPermission.spec.ts", - "playwright/e2e/Features/IncidentManager.spec.ts", - "playwright/e2e/Features/Tasks/TaskComments.spec.ts" - ] - }, { "sources": [ "openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/InboxTaskListToolbar.tsx" diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-blockquote.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-blockquote.svg new file mode 100644 index 000000000000..73f0eecd98fe --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-blockquote.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-bold.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-bold.svg new file mode 100644 index 000000000000..e384a9d21b60 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-bulleted-list.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-bulleted-list.svg new file mode 100644 index 000000000000..ccc280bf9eac --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-bulleted-list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-code.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-code.svg new file mode 100644 index 000000000000..583a914ddd8a --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-emoji.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-emoji.svg new file mode 100644 index 000000000000..b0ad7248182b --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-emoji.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-hashtag.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-hashtag.svg new file mode 100644 index 000000000000..49c516fa6b93 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-hashtag.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-italic.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-italic.svg new file mode 100644 index 000000000000..e76fdf2f9c49 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-link.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-link.svg new file mode 100644 index 000000000000..c0d31a578d3d --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-mention.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-mention.svg new file mode 100644 index 000000000000..8a276a293721 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-mention.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-numbered-list.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-numbered-list.svg new file mode 100644 index 000000000000..b3175c92655e --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-numbered-list.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-strikethrough.svg b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-strikethrough.svg new file mode 100644 index 000000000000..891f3fff6770 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/icons/text-editor-strikethrough.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBlockquote.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBlockquote.tsx new file mode 100644 index 000000000000..607d44de8568 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBlockquote.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorBlockquote: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorBlockquote.displayName = 'TextEditorBlockquote'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBold.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBold.tsx new file mode 100644 index 000000000000..5b1983f99a28 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBold.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorBold: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorBold.displayName = 'TextEditorBold'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBulletedList.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBulletedList.tsx new file mode 100644 index 000000000000..a003b14513e4 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorBulletedList.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorBulletedList: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorBulletedList.displayName = 'TextEditorBulletedList'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorCode.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorCode.tsx new file mode 100644 index 000000000000..042f0ecd24f8 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorCode.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorCode: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorCode.displayName = 'TextEditorCode'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorEmoji.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorEmoji.tsx new file mode 100644 index 000000000000..08eba43224b7 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorEmoji.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorEmoji: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorEmoji.displayName = 'TextEditorEmoji'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorHashtag.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorHashtag.tsx new file mode 100644 index 000000000000..ca6172a589e2 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorHashtag.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorHashtag: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorHashtag.displayName = 'TextEditorHashtag'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorItalic.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorItalic.tsx new file mode 100644 index 000000000000..f4bc3d921d1e --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorItalic.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorItalic: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorItalic.displayName = 'TextEditorItalic'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorLink.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorLink.tsx new file mode 100644 index 000000000000..14056c2ccb70 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorLink.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorLink: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorLink.displayName = 'TextEditorLink'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorMention.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorMention.tsx new file mode 100644 index 000000000000..59de7694f307 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorMention.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorMention: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorMention.displayName = 'TextEditorMention'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorNumberedList.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorNumberedList.tsx new file mode 100644 index 000000000000..3dd7c5c8a40f --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorNumberedList.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorNumberedList: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorNumberedList.displayName = 'TextEditorNumberedList'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorStrikethrough.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorStrikethrough.tsx new file mode 100644 index 000000000000..e6afc6910f65 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/TextEditorStrikethrough.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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 * as React from 'react'; +import type { SVGProps, FC } from 'react'; +interface Props extends SVGProps { + color?: string; + size?: number; +} + +export const TextEditorStrikethrough: FC = ({ + size = 24, + color = 'currentColor', + ...props +}) => ( + +); +TextEditorStrikethrough.displayName = 'TextEditorStrikethrough'; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/index.ts b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/index.ts index 6c401952a556..d2cf2338e4c8 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/icons/index.ts +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/icons/index.ts @@ -424,6 +424,17 @@ export { TestCase2 } from './TestCase2'; export { TestCase } from './TestCase'; export { TestDefinition } from './TestDefinition'; export { TestSuite } from './TestSuite'; +export { TextEditorBlockquote } from './TextEditorBlockquote'; +export { TextEditorBold } from './TextEditorBold'; +export { TextEditorBulletedList } from './TextEditorBulletedList'; +export { TextEditorCode } from './TextEditorCode'; +export { TextEditorEmoji } from './TextEditorEmoji'; +export { TextEditorHashtag } from './TextEditorHashtag'; +export { TextEditorItalic } from './TextEditorItalic'; +export { TextEditorLink } from './TextEditorLink'; +export { TextEditorMention } from './TextEditorMention'; +export { TextEditorNumberedList } from './TextEditorNumberedList'; +export { TextEditorStrikethrough } from './TextEditorStrikethrough'; export { ThumbsDown } from './ThumbsDown'; export { ThumbsUp } from './ThumbsUp'; export { Time } from './Time'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/InboxCommentComposer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CommentComposer/CommentComposer.test.tsx similarity index 59% rename from openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/InboxCommentComposer.test.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/common/CommentComposer/CommentComposer.test.tsx index ad6760047149..ad24fe07ffd9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/InboxCommentComposer.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CommentComposer/CommentComposer.test.tsx @@ -59,11 +59,15 @@ jest.mock( onTextChange, placeHolder, editAction, + defaultValue, + className, }: { onSave?: (m: string) => void; onTextChange?: (m: string) => void; placeHolder?: string; editAction?: React.ReactNode; + defaultValue?: string; + className?: string; }, ref: React.Ref ) => { @@ -74,9 +78,22 @@ jest.mock( })); return ( - <> +
{/* quill's format bar, where quill-emoji adds its button. */}
+ +
); } ), @@ -110,14 +128,24 @@ jest.mock('react-i18next', () => ({ })); jest.mock('@openmetadata/ui-core-components', () => ({ - Box: ({ children }: { children: React.ReactNode }) =>
{children}
, + Box: ({ + children, + ...rest + }: { + children: React.ReactNode; + 'data-testid'?: string; + }) =>
{children}
, Button: ({ onClick, + onPress, isDisabled, + children, ...rest }: { onClick?: () => void; + onPress?: () => void; isDisabled?: boolean; + children?: React.ReactNode; 'aria-label'?: string; 'data-testid'?: string; }) => ( @@ -125,12 +153,31 @@ jest.mock('@openmetadata/ui-core-components', () => ({ aria-label={rest['aria-label']} data-testid={rest['data-testid']} disabled={isDisabled} + onClick={onClick ?? onPress}> + {children} + + ), + ButtonUtility: ({ + onClick, + tooltip, + ...rest + }: { + onClick?: () => void; + tooltip?: string; + 'data-testid'?: string; + }) => ( + '; + screen.getByTestId('format-bar').replaceWith(newBar); + + await waitFor(() => + expect(iconIn(newBar.querySelector('.ql-italic'))).toBeTruthy() + ); + }); + }); + // quill-emoji adds its picker to the format bar with no horizontal position // and removes it on close. describe('emoji picker', () => { @@ -285,7 +392,7 @@ describe('InboxCommentComposer', () => { }; it('anchors the picker above its button and marks the button open', async () => { - render(); + render(); const button = screen.getByTestId('emoji-control'); const picker = openPicker('-250px'); @@ -306,7 +413,7 @@ describe('InboxCommentComposer', () => { // The editor loads lazily and quill rebuilds its bar on re-initialising, // so the bar can be a new one by the time the picker opens. it('anchors the picker in a format bar rebuilt after mount', async () => { - render(); + render(); const oldBar = screen.getByTestId('format-bar'); const newBar = document.createElement('div'); newBar.className = 'ql-toolbar'; @@ -328,7 +435,7 @@ describe('InboxCommentComposer', () => { }); it('opens below the bar when quill-emoji chose below', async () => { - render(); + render(); const picker = openPicker(); await waitFor(() => expect(picker.style.top).toBe('100%')); @@ -337,3 +444,55 @@ describe('InboxCommentComposer', () => { }); }); }); + +// An edited comment opens in the same box a new one is written in. +describe('CommentEditForm', () => { + const renderEdit = (onSave = jest.fn(), onCancel = jest.fn()) => + render( + + ); + + it("opens the comment in the composer's editor, ready to send", async () => { + renderEdit(); + + expect(screen.getByTestId('edit-comment-editor')).toBeInTheDocument(); + expect( + screen.getByTestId('feed-editor').getAttribute('data-default-value') + ).toContain('Looking'); + expect(screen.getByTestId('send-button')).toBeEnabled(); + + await waitFor(() => + expect( + screen + .getByTestId('bold-control') + .querySelector('svg.comment-composer-icon') + ).toBeTruthy() + ); + }); + + it('shows the new-line hint beside Cancel', () => { + const onCancel = jest.fn(); + renderEdit(jest.fn(), onCancel); + + expect(screen.getByText(/message.to-add-new-line/)).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('cancel-edit-comment')); + + expect(onCancel).toHaveBeenCalled(); + }); + + it('saves the edited text from the send button', () => { + const onSave = jest.fn().mockResolvedValue(undefined); + mockEditorContent = 'Looking at this tomorrow.'; + renderEdit(onSave); + + fireEvent.click(screen.getByTestId('send-button')); + + expect(onSave).toHaveBeenCalledWith('Looking at this tomorrow.'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CommentComposer/CommentComposer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CommentComposer/CommentComposer.tsx new file mode 100644 index 000000000000..dfb51b75d222 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CommentComposer/CommentComposer.tsx @@ -0,0 +1,482 @@ +/* + * Copyright 2026 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 { + Box, + Button, + ButtonUtility, + Typography, +} from '@openmetadata/ui-core-components'; +import { + ArrowRight, + TextEditorBlockquote, + TextEditorBold, + TextEditorBulletedList, + TextEditorCode, + TextEditorEmoji, + TextEditorHashtag, + TextEditorItalic, + TextEditorLink, + TextEditorMention, + TextEditorNumberedList, + TextEditorStrikethrough, + Type01, +} from '@openmetadata/ui-core-components/icons'; +import classNames from 'classnames'; +import React, { + FC, + RefObject, + SVGProps, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { + getBackendFormat, + getFrontEndFormat, + MarkdownToHTMLConverter, +} from '../../../utils/FeedUtilsPure'; +import ActivityFeedEditorNew from '../../ActivityFeed/ActivityFeedEditor/ActivityFeedEditorNew'; +import ProfilePicture from '../ProfilePicture/ProfilePicture'; +import { EditorContentRef } from '../RichTextEditor/RichTextEditor.interface'; + +// The design's restyle of the OSS ActivityFeedEditorNew, scoped to this +// editor: one input line with the formatting toggle and send button inside it, +// under a format bar of the design's icons. Quill builds this markup itself, so +// these reach it from the editor's root (Tailwind reads `_` in those selectors +// as a space, so no class here has one). `!`: Quill's snow theme and the +// FeedEditor's own styles are unlayered CSS, which beats Tailwind's utilities +// whatever their selector. +const ICON_CLASS = 'comment-composer-icon'; + +// The design's gray-blue border; in dark that scale turns navy, so it stays +// neutral. It also holds over FeedEditor's inline focus border, which all but +// vanishes in dark — and an edit box opens focused. +const EDITOR_BOX_CLASS = [ + 'tw:[&_.editor-container]:border! tw:[&_.editor-container]:border-solid!', + 'tw:[&_.editor-container]:border-utility-gray-blue-100! tw:dark:[&_.editor-container]:border-primary!', + 'tw:[&_.editor-container]:rounded-[10px]! tw:[&_.editor-container]:bg-primary!', + // Quill sets 13px Helvetica; comments read in the page's font at 14px. + 'tw:[&_.ql-container.ql-snow]:border-none! tw:[&_.ql-container.ql-snow]:bg-transparent!', + 'tw:[&_.ql-container.ql-snow]:font-[inherit]! tw:[&_.ql-container.ql-snow]:text-[14px]!', +]; + +// The format bar sits in the same box as the text, split by a hairline. It +// anchors the emoji picker, which the composer places by its button. The +// design's icons, drawn into Quill's buttons, replace Quill's and +// quill-emoji's icons and the @ / # glyphs, at 16px in Quill's own button box. +const FORMAT_BAR_CLASS = [ + 'tw:[&_.ql-toolbar.ql-snow]:relative! tw:[&_.ql-toolbar.ql-snow]:px-2! tw:[&_.ql-toolbar.ql-snow]:py-1!', + 'tw:[&_.ql-toolbar.ql-snow]:border-0! tw:[&_.ql-toolbar.ql-snow]:border-b!', + 'tw:[&_.ql-toolbar.ql-snow]:border-solid! tw:[&_.ql-toolbar.ql-snow]:border-b-secondary!', + 'tw:[&_.ql-toolbar_.ql-formats_button]:text-quaternary!', + 'tw:[&_.ql-toolbar_.ql-formats_button>svg:not(.comment-composer-icon)]:hidden!', + 'tw:[&_.ql-toolbar_.ql-formats_button]:after:content-none!', + // The editor zeroes the @ / # buttons' padding for their glyphs. + 'tw:[&_.ql-toolbar_button.ql-insertMention]:px-[5px]! tw:[&_.ql-toolbar_button.ql-insertMention]:py-[3px]!', + 'tw:[&_.ql-toolbar_button.ql-insertRef]:px-[5px]! tw:[&_.ql-toolbar_button.ql-insertRef]:py-[3px]!', + 'tw:[&_.ql-toolbar_.ql-formats_button:hover]:text-fg-brand-primary!', + 'tw:[&_.ql-toolbar_.ql-formats_button:focus-visible]:text-fg-brand-primary!', + 'tw:[&_.ql-toolbar_.ql-formats_button.ql-active]:text-fg-brand-primary!', + 'tw:[&_.ql-toolbar_.comment-composer-icon]:block!', + 'tw:[&_.ql-toolbar_.comment-composer-icon]:float-none!', + 'tw:[&_.ql-toolbar_.comment-composer-icon]:size-4!', + 'tw:[&_.ql-toolbar_.comment-composer-icon]:mx-auto!', + 'tw:[&_.ql-toolbar_.comment-composer-icon]:my-px!', +]; + +// One line of text, clear of the toggle and send button on the right. +// `.editor-container` outranks the FeedEditor's own 100px minimum. +const TEXT_CLASS = [ + 'tw:[&_.ql-editor]:min-h-11! tw:[&_.ql-editor]:pt-3! tw:[&_.ql-editor]:pb-3!', + 'tw:[&_.ql-editor]:pl-3.5! tw:[&_.ql-editor]:pr-25!', + 'tw:[&_.ql-editor]:bg-transparent! tw:[&_.ql-editor]:rounded-[10px]!', + // A list starts at the text's edge: its items' padding holds the marker. + 'tw:[&_.ql-editor_ol]:pl-0!', + // FeedEditor hard-codes the "Use @mention…" placeholder: the design's copy + // comes from the component as a CSS variable, so it stays translated. + // Quill's own placeholder color is invisible on a dark surface. + 'tw:[&_.ql-editor.ql-blank]:before:content-(--comment-composer-placeholder)!', + 'tw:[&_.ql-editor.ql-blank]:before:not-italic! tw:[&_.ql-editor.ql-blank]:before:left-3.5!', + 'tw:dark:[&_.ql-editor.ql-blank]:before:text-placeholder!', +]; + +// The @ / # suggestion list, in the core Dropdown's style: a padded overlay +// with inset rounded rows. +const MENTION_LIST_CLASS = [ + 'tw:[&_.ql-mention-list-container]:w-62! tw:[&_.ql-mention-list-container]:py-1!', + 'tw:[&_.ql-mention-list-container]:border-none!', + 'tw:[&_.ql-mention-list-container]:rounded-lg! tw:[&_.ql-mention-list-container]:outline-1!', + 'tw:[&_.ql-mention-list-container]:outline-solid!', + 'tw:[&_.ql-mention-list-container]:-outline-offset-1!', + 'tw:[&_.ql-mention-list-container]:outline-subtle!', + 'tw:[&_.ql-mention-list-container]:bg-overlay-surface!', + // The theme's `shadow-lg` is empty here; the token carries the values. + 'tw:[&_.ql-mention-list-container]:[box-shadow:var(--om-shadow-lg)]!', + 'tw:[&_.ql-mention-list-item]:mx-1.5! tw:[&_.ql-mention-list-item]:my-px!', + 'tw:[&_.ql-mention-list-item]:px-2.5! tw:[&_.ql-mention-list-item]:py-2!', + 'tw:[&_.ql-mention-list-item]:rounded-md! tw:[&_.ql-mention-list-item]:text-[14px]!', + 'tw:[&_.ql-mention-list-item]:text-secondary!', + 'tw:[&_.ql-mention-list-item.selected]:bg-primary_hover!', + 'tw:[&_.ql-mention-list-item.disabled]:cursor-default! tw:[&_.ql-mention-list-item.disabled]:text-tertiary!', +]; + +// The link bubble is a fixed 300px with nowrap content, so its chips overflow +// here: a flex row bounded by the editor's width instead. `:not(.ql-hidden)` +// keeps Quill's hide rule winning. Quill's editing mode zeroes the Save chip's +// right padding, leaving its label flush with the chip's edge. Its ::before and +// ::after are reached in the selector, not with `before:`/`after:`, which would +// also reset their `content` ("Enter Url:", "Save"). +const LINK_TOOLTIP_CLASS = [ + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)]:flex!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)]:items-center!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)]:gap-2!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)]:w-[min(420px,calc(100%-16px))]!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)::before]:shrink-0!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_input[type=text]]:flex-auto!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_input[type=text]]:min-w-0!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_input[type=text]]:w-auto!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_a.ql-preview]:flex-auto!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_a.ql-preview]:min-w-0!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_a.ql-preview]:w-auto!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_a.ql-action]:shrink-0!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden)_a.ql-remove]:shrink-0!', + 'tw:[&_.ql-snow_.ql-tooltip:not(.ql-hidden).ql-editing_a.ql-action::after]:pr-2!', +]; + +const EDITOR_CLASS = classNames( + EDITOR_BOX_CLASS, + FORMAT_BAR_CLASS, + TEXT_CLASS, + MENTION_LIST_CLASS, + LINK_TOOLTIP_CLASS +); + +// The format bar shows only while the formatting toggle beside send is on. +const COLLAPSED_CLASS = 'tw:[&_.ql-toolbar.ql-snow]:hidden!'; + +// quill-emoji opens its picker above the editor when the editor sits in the +// lower half of the window, by this inline top. +const EMOJI_PICKER_ABOVE_TOP = '-250px'; + +/** + * The format bar's buttons, by the class Quill or quill-emoji gives each, and + * the design's icon for it. Quill draws its icons from one app-wide registry, + * so the composer draws these into its own buttons instead and hides Quill's. + */ +const FORMAT_BAR_ICONS: ReadonlyArray< + readonly [string, FC>] +> = [ + ['.ql-bold', TextEditorBold], + ['.ql-italic', TextEditorItalic], + ['.ql-strike', TextEditorStrikethrough], + ['.ql-blockquote', TextEditorBlockquote], + ['.ql-code-block', TextEditorCode], + ['.ql-list[value="ordered"]', TextEditorNumberedList], + ['.ql-list[value="bullet"]', TextEditorBulletedList], + ['.ql-link', TextEditorLink], + ['.ql-insertMention', TextEditorMention], + ['.ql-insertRef', TextEditorHashtag], + ['.textarea-emoji-control', TextEditorEmoji], +]; + +const isSameButtons = (a: Array, b: Array) => + a.length === b.length && a.every((button, index) => button === b[index]); + +/** + * Watches the editor's format bar, which loads lazily, is rebuilt when Quill + * re-initialises and gains the emoji button after the rest. Returns its + * buttons in {@link FORMAT_BAR_ICONS} order, and anchors the emoji picker: + * quill-emoji drops it into the bar with no horizontal position, so it lands + * at the composer's far edge. It is pinned to its button, above or below as + * the module chose, and the button is marked open while it shows. + */ +const useFormatBar = (hostRef: RefObject) => { + const [buttons, setButtons] = useState>([]); + + useEffect(() => { + const host = hostRef.current; + if (!host) { + return; + } + // Read afresh each time: the editor loads lazily, and quill rebuilds its + // bar when it re-initialises. + const sync = () => { + const toolbar = host.querySelector('.ql-toolbar'); + const next = FORMAT_BAR_ICONS.map( + ([selector]) => toolbar?.querySelector(selector) ?? null + ); + setButtons((current) => (isSameButtons(current, next) ? current : next)); + const button = toolbar?.querySelector( + '.textarea-emoji-control' + ); + const picker = toolbar?.querySelector('#textarea-emoji'); + button?.classList.toggle('ql-active', Boolean(picker)); + button?.setAttribute('aria-expanded', String(Boolean(picker))); + if (toolbar && button && picker) { + // Read the module's choice once: the first pass clears that top, and + // any later node change (an icon drawn, a palette tab) passes again. + picker.dataset.above ??= String( + picker.style.top === EMOJI_PICKER_ABOVE_TOP + ); + const isAbove = picker.dataset.above === 'true'; + // Kept inside the bar where the button sits near its far edge. + const left = Math.min( + button.offsetLeft, + toolbar.clientWidth - picker.offsetWidth + ); + // `''` drops the module's inline top; quill-emoji's CSS pins `right`. + Object.assign(picker.style, { + left: `${Math.max(left, 0)}px`, + right: 'auto', + top: isAbove ? '' : '100%', + bottom: isAbove ? '100%' : '', + }); + } + }; + // Node changes only: the styles set above never re-trigger it. + const observer = new MutationObserver(sync); + observer.observe(host, { childList: true, subtree: true }); + sync(); + + return () => observer.disconnect(); + }, [hostRef]); + + return buttons; +}; + +export interface CommentEditorProps { + // A rejected promise puts the draft back in the editor. + onSave: (message: string) => void | Promise; + placeHolder?: string; + focused?: boolean; + // The message being edited, as HTML; the send button starts enabled. + defaultValue?: string; +} + +/** + * A comment editor: the OSS {@link ActivityFeedEditorNew} verbatim — + * so mention (@), hashtag (#), markdown and Enter-to-send all keep working — + * restyled through the scoped classes above (EDITOR_CLASS), with the + * design's format-bar icons and its arrow send button, disabled until there is + * something to send. The format bar starts hidden, one line of text; the + * formatting toggle beside send shows it, and stays highlighted while it shows. + * A new comment and an edited one use the same box. + */ +export const CommentEditor: React.FC = ({ + onSave, + placeHolder, + focused, + defaultValue, +}) => { + const { t } = useTranslation(); + const placeholderText = placeHolder ?? t('message.leave-a-comment'); + const editorRef = useRef(null); + const editorHostRef = useRef(null); + const [hasText, setHasText] = useState(Boolean(defaultValue)); + const [isFormatting, setIsFormatting] = useState(false); + const formatBarButtons = useFormatBar(editorHostRef); + + // The editor reports its markdown on every change; whitespace alone is not a + // comment, so it keeps the send button disabled. + const handleTextChange = useCallback((message: string) => { + setHasText(message.trim().length > 0); + }, []); + + // The editor is already empty when this runs, so a save that fails writes + // the draft back rather than losing it. + const submit = useCallback( + (message: string) => { + setHasText(false); + Promise.resolve(onSave(message)).catch(() => { + editorRef.current?.setEditorContent( + MarkdownToHTMLConverter.makeHtml(getFrontEndFormat(message)) + ); + setHasText(true); + }); + }, + [onSave] + ); + + // Mirrors the editor's own Enter-to-send, which clears itself first. + const handleSend = () => { + const content = editorRef.current?.getEditorContent(); + if (content) { + editorRef.current?.clearEditorContent(); + submit(getBackendFormat(content)); + } + }; + + return ( + // FeedEditor hard-codes its Quill placeholder, so feed it the figma copy + // through a CSS variable the scoped style reads. +
+ + setIsFormatting((open) => !open)} + /> + +
+ ); +}; + +export interface CommentEditFormProps { + // The comment being edited, as stored (markdown). + message: string; + onSave: (message: string) => Promise; + onCancel: () => void; + // Names the editor `-editor` and its cancel `cancel-`. + testId: string; +} + +/** + * A comment opened for editing: the same editor a new comment is written in, + * over a row with the new-line hint on the left and Cancel on the right. + */ +export const CommentEditForm: React.FC = ({ + message, + onSave, + onCancel, + testId, +}) => { + const { t } = useTranslation(); + + return ( + + + + + {t('label.shift')} + {t('label.plus-symbol')} {t('label.return')}{' '} + {t('message.to-add-new-line')} + + + + + ); +}; + +export interface CommentComposerProps { + // A rejected promise puts the draft back in the editor. + onSave: (message: string) => void | Promise; + placeHolder?: string; + focused?: boolean; +} + +/** + * A comment composer: the current user's avatar beside the + * {@link CommentEditor}. Used wherever a thread takes a comment: the Inbox's + * activity threads and task detail, an entity's Activity tab, and Collate's + * access-request drawer. + */ +const CommentComposer: React.FC = (props) => { + const { currentUser } = useApplicationStore(); + + return ( + + {/* Centred on the 44px text row, not the toolbar above it. */} + + + + ); +}; + +export default CommentComposer; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.test.tsx index 6b28e6a6ef28..8b40ebcaee3d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.test.tsx @@ -65,25 +65,13 @@ jest.mock('components/common/DeleteModal/DeleteModal', () => ({ ) : null, })); -jest.mock( - 'components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditorNew', - () => ({ - __esModule: true, - default: ({ onSave }: { onSave: (m: string) => void }) => ( - - ), - }) -); - // Has its own suite; here it only passes the author through. jest.mock('./AuthorPopover', () => ({ __esModule: true, default: ({ children }: { children: ReactNode }) => <>{children}, })); -jest.mock('./InboxCommentComposer', () => ({ +jest.mock('../../../../common/CommentComposer/CommentComposer', () => ({ __esModule: true, default: ({ focused, @@ -99,6 +87,24 @@ jest.mock('./InboxCommentComposer', () => ({ composer ), + CommentEditForm: ({ + onSave, + onCancel, + testId, + }: { + onSave: (m: string) => void; + onCancel: () => void; + testId: string; + }) => ( + <> + + + + ), })); jest.mock('components/ActivityFeed/Reactions/Reactions', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.tsx index a35b502bf7aa..e33583a10c42 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/ActivityThread.tsx @@ -13,7 +13,6 @@ import { Box, - Button, ButtonUtility, Skeleton, Typography, @@ -25,7 +24,6 @@ import { compare } from 'fast-json-patch'; import { isEmpty } from 'lodash'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import ActivityFeedEditorNew from '../../../../../components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditorNew'; import Reactions from '../../../../../components/ActivityFeed/Reactions/Reactions'; import DeleteModal from '../../../../../components/common/DeleteModal/DeleteModal'; import ProfilePicture from '../../../../../components/common/ProfilePicture/ProfilePicture'; @@ -46,15 +44,14 @@ import { removeConversationReplyReaction, } from '../../../../../rest/conversationsAPI'; import { getEntityName } from '../../../../../utils/EntityNameUtils'; -import { - getFrontEndFormat, - MarkdownToHTMLConverter, -} from '../../../../../utils/FeedUtilsPure'; +import { getFrontEndFormat } from '../../../../../utils/FeedUtilsPure'; import { showErrorToast } from '../../../../../utils/ToastUtils'; +import CommentComposer, { + CommentEditForm, +} from '../../../../common/CommentComposer/CommentComposer'; import { formatActivityTime, INBOX_MESSAGE_CLASS } from '../inbox.utils'; import { useFeedDeleteAccess } from '../useFeedDeleteAccess'; import AuthorPopover from './AuthorPopover'; -import InboxCommentComposer from './InboxCommentComposer'; // Admins bypass policy evaluation server-side. ConditionalAllow → author // only: exact for the default isOwner() rule; an approximation for other @@ -230,24 +227,12 @@ const ReplyRow = ({ )} {isEditing ? ( - - - - - - + setIsEditing(false)} + onSave={handleEditSave} + /> ) : ( )) )} - ) => { - useEffect(() => { - const host = hostRef.current; - if (!host) { - return; - } - // Read afresh each time: the editor loads lazily, and quill rebuilds its - // bar when it re-initialises. - const sync = () => { - const toolbar = host.querySelector('.ql-toolbar'); - const button = toolbar?.querySelector( - '.textarea-emoji-control' - ); - const picker = toolbar?.querySelector('#textarea-emoji'); - button?.classList.toggle('ql-active', Boolean(picker)); - button?.setAttribute('aria-expanded', String(Boolean(picker))); - if (toolbar && button && picker) { - const isAbove = picker.style.top === EMOJI_PICKER_ABOVE_TOP; - // Kept inside the bar where the button sits near its far edge. - const left = Math.min( - button.offsetLeft, - toolbar.clientWidth - picker.offsetWidth - ); - // `''` drops the module's inline top; quill-emoji's CSS pins `right`. - Object.assign(picker.style, { - left: `${Math.max(left, 0)}px`, - right: 'auto', - top: isAbove ? '' : '100%', - bottom: isAbove ? '100%' : '', - }); - } - }; - // Node changes only: the styles set above never re-trigger it. - const observer = new MutationObserver(sync); - observer.observe(host, { childList: true, subtree: true }); - - return () => observer.disconnect(); - }, [hostRef]); -}; - -export interface InboxCommentComposerProps { - // A rejected promise puts the draft back in the editor. - onSave: (message: string) => void | Promise; - placeHolder?: string; - focused?: boolean; -} - -/** - * Comment composer shared by the Inbox (activity card threads + Task detail). It - * reuses the OSS {@link ActivityFeedEditorNew} verbatim — so mention (@), - * hashtag (#), markdown, the send button and Enter-to-send all keep working — - * and only restyles it via the scoped `inbox-comment-composer__editor` class: - * the format bar over one white input line. The send button is the design's - * arrow button in place of the editor's own, disabled until there is something - * to send; Enter still sends through the editor. The current user's avatar - * sits on the left. - */ -const InboxCommentComposer: React.FC = ({ - onSave, - placeHolder, - focused, -}) => { - const { t } = useTranslation(); - const { currentUser } = useApplicationStore(); - const placeholderText = placeHolder ?? t('message.leave-a-comment'); - const editorRef = useRef(null); - const editorHostRef = useRef(null); - const [hasText, setHasText] = useState(false); - useEmojiPickerAnchor(editorHostRef); - - // The editor reports its markdown on every change; whitespace alone is not a - // comment, so it keeps the send button disabled. - const handleTextChange = useCallback((message: string) => { - setHasText(message.trim().length > 0); - }, []); - - // The editor is already empty when this runs, so a save that fails writes - // the draft back rather than losing it. - const submit = useCallback( - (message: string) => { - setHasText(false); - Promise.resolve(onSave(message)).catch(() => { - editorRef.current?.setEditorContent( - MarkdownToHTMLConverter.makeHtml(getFrontEndFormat(message)) - ); - setHasText(true); - }); - }, - [onSave] - ); - - // Mirrors the editor's own Enter-to-send, which clears itself first. - const handleSend = () => { - const content = editorRef.current?.getEditorContent(); - if (content) { - editorRef.current?.clearEditorContent(); - submit(getBackendFormat(content)); - } - }; - - return ( - - {/* Centred on the 44px text row, not the toolbar above it. */} - - {/* FeedEditor hard-codes its Quill placeholder, so feed it the figma - copy through a CSS variable the scoped style reads. */} -
- } - isDisabled={!hasText} - size="sm" - onClick={handleSend} - /> - } - emptyMentionText={t('message.no-match-found')} - focused={focused} - placeHolder={placeholderText} - ref={editorRef} - onSave={submit} - onTextChange={handleTextChange} - /> -
-
- ); -}; - -export default InboxCommentComposer; +// Moved to components/common/CommentComposer. Collate's access-request drawer +// still imports this path; drop it once Collate imports the new one. +export { default } from '../../../../common/CommentComposer/CommentComposer'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskCommentRow.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskCommentRow.tsx index 9d98e3567856..98c79ccd401f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskCommentRow.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskCommentRow.tsx @@ -14,7 +14,6 @@ import { Badge, Box, - Button, ButtonUtility, Typography, } from '@openmetadata/ui-core-components'; @@ -26,7 +25,6 @@ import { AxiosError } from 'axios'; import classNames from 'classnames'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import ActivityFeedEditorNew from '../../../../../components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditorNew'; import DeleteModal from '../../../../../components/common/DeleteModal/DeleteModal'; import RichTextEditorPreviewerV1 from '../../../../../components/common/RichTextEditor/RichTextEditorPreviewerV1'; import { useApplicationStore } from '../../../../../hooks/useApplicationStore'; @@ -36,12 +34,10 @@ import { TaskComment, } from '../../../../../rest/tasksAPI'; import { getEntityName } from '../../../../../utils/EntityNameUtils'; -import { - getFrontEndFormat, - MarkdownToHTMLConverter, -} from '../../../../../utils/FeedUtilsPure'; +import { getFrontEndFormat } from '../../../../../utils/FeedUtilsPure'; import { resolveCommentPermissions } from '../../../../../utils/TaskCommentUtils'; import { showErrorToast } from '../../../../../utils/ToastUtils'; +import { CommentEditForm } from '../../../../common/CommentComposer/CommentComposer'; import { formatInboxDateTime, INBOX_MESSAGE_CLASS } from '../inbox.utils'; export interface TaskCommentRowProps { @@ -118,28 +114,14 @@ const TaskCommentBody = ({ onCancelEdit, onSave, }: TaskCommentBodyProps) => { - const { t } = useTranslation(); - if (isEditing) { return ( - - - - - - + ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskDetailPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskDetailPanel.tsx index 48b6dc9e058c..b3a8959a8c68 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskDetailPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/InboxPage/components/TaskDetailPanel.tsx @@ -56,6 +56,7 @@ import { getPermissionErrorText } from '../../../../../utils/StringUtils'; import { getResolvedTaskFormSchema } from '../../../../../utils/TaskFormSchemaUtils'; import { getTaskAboutPath } from '../../../../../utils/TaskNavigationUtils'; import { showErrorToast } from '../../../../../utils/ToastUtils'; +import CommentComposer from '../../../../common/CommentComposer/CommentComposer'; import { useApplicationsProvider } from '../../../../Settings/Applications/ApplicationsProvider/ApplicationsProvider'; import { formatInboxDate, isTaskOpen } from '../inbox.utils'; import { @@ -79,7 +80,6 @@ import { useCurrentUserIds } from '../useCurrentUserIds'; import { useIsScrolled } from '../useIsScrolled'; import { useTaskAboutEntity } from '../useTaskAboutEntity'; import ClampedText from './ClampedText'; -import InboxCommentComposer from './InboxCommentComposer'; import TaskActionCommentModal from './TaskActionCommentModal'; import TaskActivityTimeline from './TaskActivityTimeline'; import TaskAssetCard from './TaskAssetCard'; @@ -734,7 +734,7 @@ const TaskDetailPanel: React.FC = ({ aria-hidden className="tw:pointer-events-none tw:absolute tw:inset-x-0 tw:bottom-full tw:h-8 tw:bg-linear-to-t tw:from-secondary tw:to-transparent" /> - +