feat: glossary table having approve and reject task actions (#20269)
* feat: approve and reject action for glossaryTerm * fix: getTaskData logic and status icons * fix: tests * fix: approve/reject button style and test cases * update position of status filter * style: update color variables * remove extra fetchTasks call * fix: logic of update glossaryTerm after status change * fix: updateGlossaryTermStatus function * fix: update glossary term status
@ -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);
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 2.5V9.5M6 9.5L9.5 6M6 9.5L2.5 6" stroke="#363F72" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 233 B |
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5303 2.46967C10.8232 2.76256 10.8232 3.23744 10.5303 3.53033L5.03033 9.03033C4.73744 9.32322 4.26256 9.32322 3.96967 9.03033L1.46967 6.53033C1.17678 6.23744 1.17678 5.76256 1.46967 5.46967C1.76256 5.17678 2.23744 5.17678 2.53033 5.46967L4.5 7.43934L9.46967 2.46967C9.76256 2.17678 10.2374 2.17678 10.5303 2.46967Z" fill="#027A48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 487 B |
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.275 2.75H3C2.9337 2.75 2.87011 2.77634 2.82322 2.82322C2.77634 2.87011 2.75 2.9337 2.75 3V10C2.75 10.0663 2.77634 10.1299 2.82322 10.1768C2.87011 10.2237 2.9337 10.25 3 10.25H9C9.0663 10.25 9.12989 10.2237 9.17678 10.1768C9.22366 10.1299 9.25 10.0663 9.25 10V3C9.25 2.9337 9.22366 2.87011 9.17678 2.82322C9.12989 2.77634 9.0663 2.75 9 2.75H8.725C8.60918 3.32056 8.10474 3.75 7.5 3.75H4.5C3.89526 3.75 3.39082 3.32056 3.275 2.75ZM8.725 1.25H9C9.46413 1.25 9.90925 1.43437 10.2374 1.76256C10.5656 2.09075 10.75 2.53587 10.75 3V10C10.75 10.4641 10.5656 10.9092 10.2374 11.2374C9.90925 11.5656 9.46413 11.75 9 11.75H3C2.53587 11.75 2.09075 11.5656 1.76256 11.2374C1.43437 10.9092 1.25 10.4641 1.25 10V3C1.25 2.53587 1.43437 2.09075 1.76256 1.76256C2.09075 1.43437 2.53587 1.25 3 1.25H3.275C3.39082 0.67944 3.89526 0.25 4.5 0.25H7.5C8.10474 0.25 8.60918 0.67944 8.725 1.25ZM7.25 1.75H4.75V2.25H7.25V1.75Z" fill="#B54708"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.50033 0.166748C3.90866 0.166748 0.166992 3.90841 0.166992 8.50008C0.166992 13.0917 3.90866 16.8334 8.50033 16.8334C13.092 16.8334 16.8337 13.0917 16.8337 8.50008C16.8337 3.90841 13.092 0.166748 8.50033 0.166748ZM11.3003 10.4167C11.542 10.6584 11.542 11.0584 11.3003 11.3001C11.1753 11.4251 11.017 11.4834 10.8587 11.4834C10.7003 11.4834 10.542 11.4251 10.417 11.3001L8.50033 9.38342L6.58366 11.3001C6.45866 11.4251 6.30033 11.4834 6.14199 11.4834C5.98366 11.4834 5.82533 11.4251 5.70033 11.3001C5.45866 11.0584 5.45866 10.6584 5.70033 10.4167L7.61699 8.50008L5.70033 6.58342C5.45866 6.34175 5.45866 5.94175 5.70033 5.70008C5.94199 5.45842 6.34199 5.45842 6.58366 5.70008L8.50033 7.61675L10.417 5.70008C10.6587 5.45842 11.0587 5.45842 11.3003 5.70008C11.542 5.94175 11.542 6.34175 11.3003 6.58342L9.38366 8.50008L11.3003 10.4167Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 937 B |
@ -0,0 +1,10 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6975_15800)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.36085 6C1.40034 6.06535 1.44583 6.13855 1.49722 6.21811C1.72189 6.56598 2.05425 7.02795 2.48465 7.48704C3.35914 8.41983 4.54963 9.25 6 9.25C7.45037 9.25 8.64086 8.41983 9.51535 7.48704C9.94575 7.02795 10.2781 6.56598 10.5028 6.21811C10.5542 6.13855 10.5997 6.06535 10.6392 6C10.5997 5.93464 10.5542 5.86145 10.5028 5.78189C10.2781 5.43402 9.94575 4.97205 9.51535 4.51296C8.64086 3.58017 7.45037 2.75 6 2.75C4.54963 2.75 3.35914 3.58017 2.48465 4.51296C2.05425 4.97205 1.72189 5.43402 1.49722 5.78189C1.44583 5.86145 1.40034 5.93465 1.36085 6ZM11.5 6C12.1708 5.66459 12.1707 5.66435 12.1706 5.66408L12.1695 5.66202L12.1675 5.65798L12.1612 5.64561C12.156 5.63548 12.1488 5.62162 12.1396 5.60429C12.1213 5.56963 12.095 5.52102 12.0609 5.46043C11.9928 5.33936 11.8932 5.16989 11.7628 4.96811C11.5031 4.56598 11.1167 4.02795 10.6097 3.48704C9.60914 2.41983 8.04963 1.25 6 1.25C3.95037 1.25 2.39086 2.41983 1.39035 3.48704C0.883251 4.02795 0.496861 4.56598 0.237158 4.96811C0.106841 5.16989 0.00719546 5.33936 -0.0609082 5.46043C-0.0949883 5.52102 -0.121254 5.56963 -0.139593 5.60429C-0.148764 5.62162 -0.155961 5.63548 -0.161169 5.64561L-0.167489 5.65798L-0.169529 5.66202L-0.170268 5.66349C-0.170398 5.66375 -0.17082 5.66459 0.5 6L-0.17082 5.66459C-0.276393 5.87574 -0.276393 6.12426 -0.17082 6.33541L0.5 6C-0.17082 6.33541 -0.170951 6.33515 -0.17082 6.33541L-0.170268 6.33651L-0.169529 6.33798L-0.167489 6.34202L-0.161169 6.35439C-0.155961 6.36452 -0.148764 6.37838 -0.139593 6.39571C-0.121254 6.43037 -0.0949883 6.47898 -0.0609082 6.53957C0.00719546 6.66064 0.106841 6.83011 0.237158 7.03189C0.496861 7.43402 0.883251 7.97205 1.39035 8.51296C2.39086 9.58017 3.95037 10.75 6 10.75C8.04963 10.75 9.60914 9.58017 10.6097 8.51296C11.1167 7.97205 11.5031 7.43402 11.7628 7.03189C11.8932 6.83011 11.9928 6.66064 12.0609 6.53957C12.095 6.47898 12.1213 6.43037 12.1396 6.39571C12.1488 6.37838 12.156 6.36452 12.1612 6.35439L12.1675 6.34202L12.1695 6.33798L12.1703 6.33651C12.1704 6.33625 12.1708 6.33541 11.5 6ZM11.5 6L12.1708 6.33541C12.2764 6.12426 12.2761 5.87523 12.1706 5.66408L11.5 6ZM6 5.25C5.58579 5.25 5.25 5.58579 5.25 6C5.25 6.41421 5.58579 6.75 6 6.75C6.41421 6.75 6.75 6.41421 6.75 6C6.75 5.58579 6.41421 5.25 6 5.25ZM3.75 6C3.75 4.75736 4.75736 3.75 6 3.75C7.24264 3.75 8.25 4.75736 8.25 6C8.25 7.24264 7.24264 8.25 6 8.25C4.75736 8.25 3.75 7.24264 3.75 6ZM1.17041 5.66378C1.17035 5.66364 1.17024 5.66344 1.17041 5.66378Z" fill="#C4320A"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6975_15800">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.49967 0.833252C3.82634 0.833252 0.833008 3.82659 0.833008 7.49992C0.833008 11.1733 3.82634 14.1666 7.49967 14.1666C11.173 14.1666 14.1663 11.1733 14.1663 7.49992C14.1663 3.82659 11.173 0.833252 7.49967 0.833252ZM10.6863 5.96659L6.90634 9.74659C6.81301 9.83992 6.68634 9.89325 6.55301 9.89325C6.41967 9.89325 6.29301 9.83992 6.19967 9.74659L4.31301 7.85992C4.11967 7.66659 4.11967 7.34659 4.31301 7.15325C4.50634 6.95992 4.82634 6.95992 5.01967 7.15325L6.55301 8.68659L9.97968 5.25992C10.173 5.06659 10.493 5.06659 10.6863 5.25992C10.8797 5.45325 10.8797 5.76659 10.6863 5.96659Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 687 B |
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 3L3 9M3 3L9 9" stroke="#B42318" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 215 B |
@ -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<HTMLDivElement>(null);
|
||||
const [tableWidth, setTableWidth] = useState(0);
|
||||
const { activeGlossary, glossaryChildTerms, setGlossaryChildTerms } =
|
||||
useGlossaryStore();
|
||||
const { t } = useTranslation();
|
||||
const [termTaskThreads, setTermTaskThreads] = useState<
|
||||
Record<string, Thread[]>
|
||||
>({});
|
||||
|
||||
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<string, Thread[]>, 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<ModifiedGlossaryTerm> = [
|
||||
{
|
||||
@ -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 (
|
||||
<StatusBadge
|
||||
dataTestId={record.fullyQualifiedName + '-status'}
|
||||
label={status}
|
||||
status={StatusClass[status]}
|
||||
/>
|
||||
<div>
|
||||
{status === Status.InReview && permission ? (
|
||||
<StatusAction
|
||||
dataTestId={record.name}
|
||||
onApprove={() => handleApproveGlossaryTerm(taskId, termFQN)}
|
||||
onReject={() => handleRejectGlossaryTerm(taskId, termFQN)}
|
||||
/>
|
||||
) : (
|
||||
<StatusBadge
|
||||
dataTestId={termFQN + '-status'}
|
||||
label={status}
|
||||
status={StatusClass[status]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
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 (
|
||||
<>
|
||||
<Button
|
||||
className="text-primary"
|
||||
data-testid="expand-collapse-all-button"
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={toggleExpandAll}>
|
||||
<Space align="center" size={4}>
|
||||
{isAllExpanded ? (
|
||||
<DownUpArrowIcon color={DE_ACTIVE_COLOR} height="14px" />
|
||||
) : (
|
||||
<UpDownArrowIcon color={DE_ACTIVE_COLOR} height="14px" />
|
||||
)}
|
||||
|
||||
{isAllExpanded ? t('label.collapse-all') : t('label.expand-all')}
|
||||
</Space>
|
||||
</Button>
|
||||
<Dropdown
|
||||
className="custom-glossary-dropdown-menu status-dropdown"
|
||||
getPopupContainer={(trigger) => {
|
||||
@ -523,6 +675,22 @@ const GlossaryTermTab = ({
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button
|
||||
className="text-primary"
|
||||
data-testid="expand-collapse-all-button"
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={toggleExpandAll}>
|
||||
<Space align="center" size={4}>
|
||||
{isAllExpanded ? (
|
||||
<DownUpArrowIcon color={DE_ACTIVE_COLOR} height="14px" />
|
||||
) : (
|
||||
<UpDownArrowIcon color={DE_ACTIVE_COLOR} height="14px" />
|
||||
)}
|
||||
|
||||
{isAllExpanded ? t('label.collapse-all') : t('label.expand-all')}
|
||||
</Space>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}, [isAllExpanded, isStatusDropdownVisible, statusDropdownMenu]);
|
||||
|
||||
@ -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<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className={`approve-btn ${isRejectHovered ? 'icon-only' : ''}`}
|
||||
data-testid={dataTestId + '-approve-btn'}
|
||||
icon={<Icon component={TickCircleIcon} />}
|
||||
onClick={onApprove}>
|
||||
{!isRejectHovered && (
|
||||
<span className="btn-text">{t('label.approve')}</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className={`reject-btn ${isRejectHovered ? 'show-text' : ''}`}
|
||||
data-testid={dataTestId + '-reject-btn'}
|
||||
icon={<Icon component={CloseCircleIcon} />}
|
||||
onClick={onReject}
|
||||
onMouseEnter={() => setIsRejectHovered(true)}
|
||||
onMouseLeave={() => setIsRejectHovered(false)}>
|
||||
{isRejectHovered && (
|
||||
<span className="btn-text">{t('label.reject')}</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusAction;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
<div
|
||||
className={classNames('status-badge', status)}
|
||||
data-testid={dataTestId}>
|
||||
{label}
|
||||
{StatusIcon && <Icon className="text-sm" component={StatusIcon} />}
|
||||
<span className={`status-badge-label ${status}`}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<string, Array<any>>
|
||||
) => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||