diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 1ad4a6433c1..10cad2f839d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -374,6 +374,80 @@ test.describe('Glossary tests', () => { } }); + test('Approve and reject glossary term from Glossary Listing', async ({ + browser, + }) => { + test.slow(true); + + const { page, afterAction, apiContext } = await performAdminLogin(browser); + const { page: page1, afterAction: afterActionUser1 } = + await performUserLogin(browser, user3); + const glossary1 = new Glossary(); + + glossary1.data.owners = [{ name: 'admin', type: 'user' }]; + glossary1.data.mutuallyExclusive = true; + glossary1.data.reviewers = [ + { name: `${user3.data.firstName}${user3.data.lastName}`, type: 'user' }, + ]; + glossary1.data.terms = [ + new GlossaryTerm(glossary1), + new GlossaryTerm(glossary1), + ]; + + await test.step('Create Glossary and Terms', async () => { + await sidebarClick(page, SidebarItem.GLOSSARY); + await createGlossary(page, glossary1.data, false); + await verifyGlossaryDetails(page, glossary1.data); + await createGlossaryTerms(page, glossary1.data); + }); + + await test.step('Approve and Reject Glossary Term', async () => { + await redirectToHomePage(page1); + await sidebarClick(page1, SidebarItem.GLOSSARY); + await selectActiveGlossary(page1, glossary1.data.name); + await verifyTaskCreated( + page1, + glossary1.data.fullyQualifiedName, + glossary1.data.terms[0].data.name + ); + await verifyTaskCreated( + page1, + glossary1.data.fullyQualifiedName, + glossary1.data.terms[1].data.name + ); + await redirectToHomePage(page1); + await sidebarClick(page1, SidebarItem.GLOSSARY); + await selectActiveGlossary(page1, glossary1.data.name); + + const taskResolve = page1.waitForResponse('/api/v1/feed/tasks/*/resolve'); + await page1 + .getByTestId(`${glossary1.data.terms[0].data.name}-approve-btn`) + .click(); + await taskResolve; + await toastNotification(page1, /Task resolved successfully/); + + await validateGlossaryTerm( + page1, + glossary1.data.terms[0].data, + 'Approved' + ); + + await page1 + .getByTestId(`${glossary1.data.terms[1].data.name}-reject-btn`) + .click(); + await taskResolve; + + await expect( + page1.getByTestId(`${glossary1.data.terms[1].data.name}`) + ).not.toBeVisible(); + + await afterActionUser1(); + }); + + await glossary1.delete(apiContext); + await afterAction(); + }); + test('Add and Remove Assets', async ({ browser }) => { test.slow(true); diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/arrow-down-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/arrow-down-colored.svg new file mode 100644 index 00000000000..ff8ba8f542c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/arrow-down-colored.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/check-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/check-colored.svg new file mode 100644 index 00000000000..1bcd8419dc6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/check-colored.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/clipboard-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/clipboard-colored.svg new file mode 100644 index 00000000000..7d05f781f21 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/clipboard-colored.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/close-circle-white.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/close-circle-white.svg new file mode 100644 index 00000000000..a1510a9f0f7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/close-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/eye-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/eye-colored.svg new file mode 100644 index 00000000000..e62015d3975 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/eye-colored.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/tick-circle-white.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/tick-circle-white.svg new file mode 100644 index 00000000000..363012c268e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/tick-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/x-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/x-colored.svg new file mode 100644 index 00000000000..8e198b15712 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/x-colored.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx index 2f562ddb1aa..f1ade5b9330 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx @@ -58,6 +58,7 @@ import { TEXT_BODY_COLOR, } from '../../../constants/constants'; import { GLOSSARIES_DOCS } from '../../../constants/docs.constants'; +import { TaskOperation } from '../../../constants/Feeds.constants'; import { DEFAULT_VISIBLE_COLUMNS, GLOSSARY_TERM_TABLE_COLUMNS_KEYS, @@ -65,12 +66,21 @@ import { } from '../../../constants/Glossary.contant'; import { TABLE_CONSTANTS } from '../../../constants/Teams.constants'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; -import { TabSpecificField } from '../../../enums/entity.enum'; +import { EntityType, TabSpecificField } from '../../../enums/entity.enum'; +import { ResolveTask } from '../../../generated/api/feed/resolveTask'; import { EntityReference, GlossaryTerm, Status, } from '../../../generated/entity/data/glossaryTerm'; +import { + Thread, + ThreadTaskStatus, + ThreadType, +} from '../../../generated/entity/feed/thread'; +import { User } from '../../../generated/entity/teams/user'; +import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { getAllFeeds, updateTask } from '../../../rest/feedsAPI'; import { getFirstLevelGlossaryTerms, getGlossaryTerms, @@ -85,12 +95,14 @@ import { findExpandableKeysForArray, findItemByFqn, glossaryTermTableColumnsWidth, + permissionForApproveOrReject, StatusClass, } from '../../../utils/GlossaryUtils'; import { getGlossaryPath } from '../../../utils/RouterUtils'; -import { showErrorToast } from '../../../utils/ToastUtils'; +import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { DraggableBodyRowProps } from '../../common/Draggable/DraggableBodyRowProps.interface'; import Loader from '../../common/Loader/Loader'; +import StatusAction from '../../common/StatusAction/StatusAction'; import Table from '../../common/Table/Table'; import TagButton from '../../common/TagButton/TagButton.component'; import { ModifiedGlossary, useGlossaryStore } from '../useGlossary.store'; @@ -109,11 +121,15 @@ const GlossaryTermTab = ({ onEditGlossaryTerm, className, }: GlossaryTermTabProps) => { + const { currentUser } = useApplicationStore(); const tableRef = useRef(null); const [tableWidth, setTableWidth] = useState(0); const { activeGlossary, glossaryChildTerms, setGlossaryChildTerms } = useGlossaryStore(); const { t } = useTranslation(); + const [termTaskThreads, setTermTaskThreads] = useState< + Record + >({}); const { glossaryTerms, expandableKeys } = useMemo(() => { const terms = (glossaryChildTerms as ModifiedGlossaryTerm[]) ?? []; @@ -170,6 +186,48 @@ const GlossaryTermTab = ({ setIsTableLoading(false); }; + const fetchAllTasks = useCallback(async () => { + if (!activeGlossary?.fullyQualifiedName) { + return; + } + + try { + const { data } = await getAllFeeds( + `<#E::${EntityType.GLOSSARY}::${activeGlossary.fullyQualifiedName}>`, + undefined, + ThreadType.Task, + undefined, + ThreadTaskStatus.Open, + undefined, + API_RES_MAX_SIZE + ); + + // Organize tasks by glossary term FQN + const tasksByTerm = data.reduce( + (acc: Record, thread: Thread) => { + const termFQN = thread.about; + if (termFQN) { + if (!acc[termFQN]) { + acc[termFQN] = []; + } + acc[termFQN].push(thread); + } + + return acc; + }, + {} + ); + + setTermTaskThreads(tasksByTerm); + } catch (error) { + showErrorToast(error as AxiosError); + } + }, [activeGlossary?.fullyQualifiedName]); + + useEffect(() => { + fetchAllTasks(); + }, [fetchAllTasks]); + const glossaryTermStatus: Status | null = useMemo(() => { if (!isGlossary) { return (activeGlossary as GlossaryTerm).status ?? Status.Approved; @@ -183,6 +241,94 @@ const GlossaryTermTab = ({ [permissions.Create, tableWidth] ); + const updateGlossaryTermStatus = ( + terms: ModifiedGlossary[], + targetFqn: string, + newStatus: Status + ): ModifiedGlossary[] => { + return terms.map((term) => { + if (term.fullyQualifiedName === targetFqn) { + return { + ...term, + status: newStatus, + }; + } + + if (term.children && term.children.length > 0) { + return { + ...term, + children: updateGlossaryTermStatus( + term.children as ModifiedGlossary[], + targetFqn, + newStatus + ) as ModifiedGlossaryTerm[], + }; + } + + return term; + }); + }; + + const updateTaskData = useCallback( + async (data: ResolveTask, taskId: string, glossaryTermFqn: string) => { + try { + if (!taskId) { + return; + } + + await updateTask(TaskOperation.RESOLVE, taskId + '', data); + showSuccessToast(t('server.task-resolved-successfully')); + + const currentExpandedKeys = [...expandedRowKeys]; + setExpandedRowKeys(currentExpandedKeys); + + if (glossaryChildTerms && glossaryTermFqn) { + const newStatus = + data.newValue === 'approved' ? Status.Approved : Status.Rejected; + + const updatedTerms = updateGlossaryTermStatus( + glossaryChildTerms, + glossaryTermFqn, + newStatus + ); + + setGlossaryChildTerms(updatedTerms); + + // remove resolved task from term task threads + if (termTaskThreads[glossaryTermFqn]) { + const updatedThreads = { ...termTaskThreads }; + updatedThreads[glossaryTermFqn] = updatedThreads[ + glossaryTermFqn + ].filter( + (thread) => !(thread.id && thread.id.toString() === taskId) + ); + + setTermTaskThreads(updatedThreads); + } + } + } catch (error) { + showErrorToast(error as AxiosError); + } + }, + [expandedRowKeys, glossaryChildTerms, termTaskThreads] + ); + + const handleApproveGlossaryTerm = useCallback( + (taskId: string, glossaryTermFqn: string) => { + const data = { newValue: 'approved' } as ResolveTask; + updateTaskData(data, taskId, glossaryTermFqn); + }, + [updateTaskData] + ); + + const handleRejectGlossaryTerm = useCallback( + (taskId: string, glossaryTermFqn: string) => { + const data = { newValue: 'rejected' } as ResolveTask; + updateTaskData(data, taskId, glossaryTermFqn); + }, + [updateTaskData] + ); + const columns = useMemo(() => { const data: ColumnsType = [ { @@ -285,13 +431,29 @@ const GlossaryTermTab = ({ }), render: (_, record) => { const status = record.status ?? Status.Approved; + const termFQN = record.fullyQualifiedName ?? ''; + const { permission, taskId } = permissionForApproveOrReject( + record, + currentUser as User, + termTaskThreads + ); return ( - +
+ {status === Status.InReview && permission ? ( + handleApproveGlossaryTerm(taskId, termFQN)} + onReject={() => handleRejectGlossaryTerm(taskId, termFQN)} + /> + ) : ( + + )} +
); }, onFilter: (value, record) => record.status === value, @@ -348,7 +510,13 @@ const GlossaryTermTab = ({ } return data; - }, [permissions, tableColumnsWidth]); + }, [ + permissions, + tableColumnsWidth, + termTaskThreads, + handleApproveGlossaryTerm, + handleRejectGlossaryTerm, + ]); const handleCheckboxChange = useCallback( (key: string, checked: boolean) => { @@ -485,22 +653,6 @@ const GlossaryTermTab = ({ const extraTableFilters = useMemo(() => { return ( <> - { @@ -523,6 +675,22 @@ const GlossaryTermTab = ({ + ); }, [isAllExpanded, isStatusDropdownVisible, statusDropdownMenu]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/StatusAction/StatusAction.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusAction/StatusAction.tsx new file mode 100644 index 00000000000..0292ecf5b84 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusAction/StatusAction.tsx @@ -0,0 +1,61 @@ +/* + * 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 Icon from '@ant-design/icons'; +import { Button } from 'antd'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as CloseCircleIcon } from '../../../assets/svg/close-circle-white.svg'; +import { ReactComponent as TickCircleIcon } from '../../../assets/svg/tick-circle-white.svg'; +import './status-action.less'; + +interface StatusActionProps { + onApprove: () => void; + onReject: () => void; + dataTestId?: string; +} + +const StatusAction = ({ + onApprove, + onReject, + dataTestId, +}: StatusActionProps) => { + const { t } = useTranslation(); + const [isRejectHovered, setIsRejectHovered] = useState(false); + + return ( +
+ + +
+ ); +}; + +export default StatusAction; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/StatusAction/status-action.less b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusAction/status-action.less new file mode 100644 index 00000000000..ec303da75ab --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusAction/status-action.less @@ -0,0 +1,105 @@ +/* + * 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 url('../../../styles/variables.less'); + +.approve-btn, +.reject-btn { + &.ant-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + transition: width 0.3s ease; + border-radius: 8px; + padding: 4px 12px; + font-size: 12px; + font-weight: 500; + height: 32px; + + .anticon { + display: flex; + align-items: center; + justify-content: center; + } + + .btn-text { + margin-left: 4px; + white-space: nowrap; + transition: opacity 0.3s ease; + } + } +} + +.approve-btn { + &.ant-btn { + background-color: @blue-18; + color: @white; + border: 1px solid @blue-19; + width: 100px; + padding: 4px 12px; + + &.icon-only { + width: 32px; + padding: 4px; + + .btn-text { + opacity: 0; + width: 0; + margin-left: 0; + } + } + + &:hover, + &:focus, + &:active { + background-color: @blue-18; + border-color: @blue-19; + color: @white; + } + } +} + +.reject-btn { + &.ant-btn { + background-color: @red-15; + color: @white; + border: 1px solid @red-4; + width: 32px; + padding: 4px; + + .btn-text { + opacity: 0; + width: 0; + margin-left: 0; + } + + &.show-text { + width: 80px; + padding: 4px 12px; + + .btn-text { + opacity: 1; + width: auto; + margin-left: 4px; + } + } + + &:hover, + &:focus, + &:active { + background-color: @red-15; + border-color: @red-4; + color: @white; + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/StatusBadge.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/StatusBadge.component.tsx index 5cafeed6ff7..9f035efad09 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/StatusBadge.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/StatusBadge.component.tsx @@ -10,17 +10,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import Icon from '@ant-design/icons'; import classNames from 'classnames'; import React from 'react'; +import { ReactComponent as DeprecatedIcon } from '../../../assets/svg/arrow-down-colored.svg'; +import { ReactComponent as ApprovedIcon } from '../../../assets/svg/check-colored.svg'; +import { ReactComponent as DraftIcon } from '../../../assets/svg/clipboard-colored.svg'; +import { ReactComponent as InReviewIcon } from '../../../assets/svg/eye-colored.svg'; +import { ReactComponent as RejectedIcon } from '../../../assets/svg/x-colored.svg'; +import { Status } from '../../../generated/entity/data/glossaryTerm'; import './status-badge.less'; import { StatusBadgeProps } from './StatusBadge.interface'; +const icons = { + [Status.Approved]: ApprovedIcon, + [Status.Rejected]: RejectedIcon, + [Status.InReview]: InReviewIcon, + [Status.Draft]: DraftIcon, + [Status.Deprecated]: DeprecatedIcon, +} as const; + const StatusBadge = ({ label, status, dataTestId }: StatusBadgeProps) => { + const StatusIcon = icons[label as Status]; + return (
- {label} + {StatusIcon && } + {label}
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/status-badge.less b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/status-badge.less index 56561cc19ee..038919e645a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/status-badge.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/StatusBadge/status-badge.less @@ -14,37 +14,46 @@ @import (reference) url('../../../styles/variables.less'); .status-badge { - font-weight: 500; - font-size: 10px; - border: 1px solid; - border-radius: 4px; - padding: 4px 8px; + border-radius: 16px; + padding: 6px 12px; text-align: center; max-width: fit-content; + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; &.success { - color: @green-3; - border-color: @green-3; - background-color: @green-4; + color: @green-10; + background-color: @green-9; } &.failure { - color: @red-3; - border-color: @red-3; - background-color: @red-4; + color: @red-10; + background-color: @red-9; } &.warning { - color: @yellow-2; - border-color: @yellow-2; - background-color: @yellow-3; + color: @yellow-11; + background-color: @yellow-10; } &.started, &.running { - color: @purple-3; - background-color: @purple-1; - border: 1px solid @purple-3; + color: @orange-2; + background-color: @orange-1; } &.stopped { - color: @grey-3; - background-color: @grey-2; - border: 1px solid @grey-3; + color: @grey-19; + background-color: @grey-9; } } + +.status-badge { + svg { + margin-top: 2px; + margin-bottom: -3px; + } +} + +.status-badge-label { + font-weight: 500; + font-size: 12px; + line-height: 20px; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less index f374d4df536..0ab8be5a1de 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less @@ -61,6 +61,9 @@ @red-12: #f31260; @red-13: #fef3f2; @red-14: #fda29b; +@red-15: #e52315; +@orange-1: #fff6ed; +@orange-2: #c4320a; @purple-1: #f2edfd; @purple-2: #7147e8; @@ -82,6 +85,8 @@ @blue-15: #eaecf5; @blue-16: #84caff; @blue-17: #eff8ff; +@blue-18: #0968da; +@blue-19: #e3e3e3; @partial-success-1: #06a4a4; @partial-success-2: #bdeeee; @@ -105,6 +110,10 @@ @grey-14: #a4a7ae; @grey-15: #eaecf5; @grey-16: #f4f5f7; +@grey-17: #fafafa; +@grey-18: #afb5d9; +@grey-19: #363f72; + @text-grey-muted: @grey-4; @font-size-base: 14px; @box-shadow-base: 0px 2px 10px rgba(0, 0, 0, 0.12); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.test.ts index c3a3b5cebd5..7ab99e6d65e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.test.ts @@ -364,7 +364,7 @@ describe('Glossary Utils - glossaryTermTableColumnsWidth', () => { name: 400, owners: 170, reviewers: 330, - status: 120, + status: 330, synonyms: 330, }); }); @@ -377,7 +377,7 @@ describe('Glossary Utils - glossaryTermTableColumnsWidth', () => { name: 400, owners: 170, reviewers: 330, - status: 120, + status: 330, synonyms: 330, }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx index a64101a854b..febe56a7d03 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx @@ -36,6 +36,7 @@ import { TermReference, } from '../generated/entity/data/glossaryTerm'; import { Domain } from '../generated/entity/domains/domain'; +import { User } from '../generated/entity/teams/user'; import { calculatePercentageFromValue } from './CommonUtils'; import { getEntityName } from './EntityUtils'; import { VersionStatus } from './EntityVersionUtils.interface'; @@ -450,5 +451,28 @@ export const glossaryTermTableColumnsWidth = ( reviewers: calculatePercentageFromValue(tableWidth, 33), synonyms: calculatePercentageFromValue(tableWidth, 33), owners: calculatePercentageFromValue(tableWidth, 17), - status: calculatePercentageFromValue(tableWidth, 12), + status: calculatePercentageFromValue(tableWidth, 33), }); + +export const getGlossaryEntityLink = (glossaryTermFQN: string) => + `<#E::${EntityType.GLOSSARY_TERM}::${glossaryTermFQN}>`; + +export const permissionForApproveOrReject = ( + record: ModifiedGlossaryTerm, + currentUser: User, + termTaskThreads: Record> +) => { + const entityLink = getGlossaryEntityLink(record.fullyQualifiedName ?? ''); + const taskThread = termTaskThreads[entityLink]?.find( + (thread) => thread.about === entityLink + ); + + const isReviewer = record.reviewers?.some( + (reviewer) => reviewer.id === currentUser?.id + ); + + return { + permission: taskThread && isReviewer, + taskId: taskThread?.task?.id, + }; +};