mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-31 04:14:34 +00:00
Feat: Support for column customization for Glossary term table (#18584)
This commit is contained in:
parent
fda0e542e4
commit
626464f443
@ -43,18 +43,27 @@ import {
|
||||
approveTagsTask,
|
||||
assignTagToGlossaryTerm,
|
||||
changeTermHierarchyFromModal,
|
||||
clickSaveButton,
|
||||
confirmationDragAndDropGlossary,
|
||||
createDescriptionTaskForGlossary,
|
||||
createGlossary,
|
||||
createGlossaryTerms,
|
||||
createTagTaskForGlossary,
|
||||
deleteGlossaryOrGlossaryTerm,
|
||||
deselectColumns,
|
||||
dragAndDropColumn,
|
||||
dragAndDropTerm,
|
||||
filterStatus,
|
||||
goToAssetsTab,
|
||||
openColumnDropdown,
|
||||
renameGlossaryTerm,
|
||||
selectActiveGlossary,
|
||||
selectActiveGlossaryTerm,
|
||||
selectColumns,
|
||||
toggleAllColumnsSelection,
|
||||
validateGlossaryTerm,
|
||||
verifyAllColumns,
|
||||
verifyColumnsVisibility,
|
||||
verifyGlossaryDetails,
|
||||
verifyGlossaryTermAssets,
|
||||
} from '../../utils/glossary';
|
||||
@ -924,6 +933,141 @@ test.describe('Glossary tests', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('Column selection and visibility for Glossary Terms table', async ({
|
||||
browser,
|
||||
}) => {
|
||||
const { page, afterAction, apiContext } = await performAdminLogin(browser);
|
||||
const glossary1 = new Glossary();
|
||||
const glossaryTerm1 = new GlossaryTerm(glossary1);
|
||||
const glossaryTerm2 = new GlossaryTerm(glossary1);
|
||||
glossary1.data.terms = [glossaryTerm1, glossaryTerm2];
|
||||
|
||||
try {
|
||||
await glossary1.create(apiContext);
|
||||
await glossaryTerm1.create(apiContext);
|
||||
await glossaryTerm2.create(apiContext);
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary1.data.displayName);
|
||||
|
||||
await test.step(
|
||||
'Open column dropdown and select columns and check if they are visible',
|
||||
async () => {
|
||||
await openColumnDropdown(page);
|
||||
const checkboxLabels = ['Reviewer', 'Synonyms'];
|
||||
await selectColumns(page, checkboxLabels);
|
||||
await clickSaveButton(page);
|
||||
await verifyColumnsVisibility(page, checkboxLabels, true);
|
||||
}
|
||||
);
|
||||
|
||||
await test.step(
|
||||
'Open column dropdown and deselect columns and check if they are hidden',
|
||||
async () => {
|
||||
await openColumnDropdown(page);
|
||||
const checkboxLabels = ['Reviewer', 'Owners'];
|
||||
await deselectColumns(page, checkboxLabels);
|
||||
await clickSaveButton(page);
|
||||
await verifyColumnsVisibility(page, checkboxLabels, false);
|
||||
}
|
||||
);
|
||||
|
||||
await test.step('All columns selection', async () => {
|
||||
await toggleAllColumnsSelection(page, true);
|
||||
const tableColumns = [
|
||||
'TERMS',
|
||||
'DESCRIPTION',
|
||||
'REVIEWER',
|
||||
'SYNONYMS',
|
||||
'OWNERS',
|
||||
'STATUS',
|
||||
'ACTIONS',
|
||||
];
|
||||
await verifyAllColumns(page, tableColumns, true);
|
||||
});
|
||||
} finally {
|
||||
await glossaryTerm1.delete(apiContext);
|
||||
await glossaryTerm2.delete(apiContext);
|
||||
await glossary1.delete(apiContext);
|
||||
await afterAction();
|
||||
}
|
||||
});
|
||||
|
||||
test('Glossary Terms Table Status filtering', async ({ browser }) => {
|
||||
const { page, afterAction, apiContext } = await performAdminLogin(browser);
|
||||
const glossary1 = new Glossary();
|
||||
const glossaryTerm1 = new GlossaryTerm(glossary1);
|
||||
const glossaryTerm2 = new GlossaryTerm(glossary1);
|
||||
glossary1.data.terms = [glossaryTerm1, glossaryTerm2];
|
||||
|
||||
try {
|
||||
await glossary1.create(apiContext);
|
||||
await glossaryTerm1.create(apiContext);
|
||||
await glossaryTerm2.create(apiContext);
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary1.data.displayName);
|
||||
|
||||
await test.step(
|
||||
'Deselect status and check if the table has filtered rows',
|
||||
async () => {
|
||||
await filterStatus(page, ['Draft'], ['Approved']);
|
||||
}
|
||||
);
|
||||
|
||||
await test.step(
|
||||
'Re-select the status and check if it appears again',
|
||||
async () => {
|
||||
await filterStatus(page, ['Draft'], ['Draft', 'Approved']);
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
await glossaryTerm1.delete(apiContext);
|
||||
await glossaryTerm2.delete(apiContext);
|
||||
await glossary1.delete(apiContext);
|
||||
await afterAction();
|
||||
}
|
||||
});
|
||||
|
||||
test('Column dropdown drag-and-drop functionality for Glossary Terms table', async ({
|
||||
browser,
|
||||
}) => {
|
||||
const { page, afterAction, apiContext } = await performAdminLogin(browser);
|
||||
const glossary1 = new Glossary();
|
||||
const glossaryTerm1 = new GlossaryTerm(glossary1);
|
||||
const glossaryTerm2 = new GlossaryTerm(glossary1);
|
||||
glossary1.data.terms = [glossaryTerm1, glossaryTerm2];
|
||||
|
||||
try {
|
||||
await glossary1.create(apiContext);
|
||||
await glossaryTerm1.create(apiContext);
|
||||
await glossaryTerm2.create(apiContext);
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary1.data.displayName);
|
||||
await openColumnDropdown(page);
|
||||
const dragColumn = 'Owners';
|
||||
const dropColumn = 'Status';
|
||||
await dragAndDropColumn(page, dragColumn, dropColumn);
|
||||
const saveButton = page.locator('.ant-btn-primary', {
|
||||
hasText: 'Save',
|
||||
});
|
||||
await saveButton.click();
|
||||
const columnHeaders = await page.locator('thead th');
|
||||
const columnText = await columnHeaders.allTextContents();
|
||||
|
||||
expect(columnText).toEqual([
|
||||
'Terms',
|
||||
'Description',
|
||||
'Status',
|
||||
'Owners',
|
||||
'Actions',
|
||||
]);
|
||||
} finally {
|
||||
await glossaryTerm1.delete(apiContext);
|
||||
await glossaryTerm2.delete(apiContext);
|
||||
await glossary1.delete(apiContext);
|
||||
await afterAction();
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const { afterAction, apiContext } = await performAdminLogin(browser);
|
||||
await user1.delete(apiContext);
|
||||
|
@ -1004,3 +1004,152 @@ export const approveTagsTask = async (
|
||||
|
||||
await expect(tagVisibility).toBe(true);
|
||||
};
|
||||
|
||||
export async function openColumnDropdown(page: Page): Promise<void> {
|
||||
const dropdownButton = page.getByTestId('glossary-column-dropdown');
|
||||
|
||||
await expect(dropdownButton).toBeVisible();
|
||||
|
||||
await dropdownButton.click();
|
||||
}
|
||||
|
||||
export async function selectColumns(
|
||||
page: Page,
|
||||
checkboxLabels: string[]
|
||||
): Promise<void> {
|
||||
for (const label of checkboxLabels) {
|
||||
const checkbox = page.locator('.glossary-dropdown-label', {
|
||||
hasText: label,
|
||||
});
|
||||
await checkbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
export async function deselectColumns(
|
||||
page: Page,
|
||||
checkboxLabels: string[]
|
||||
): Promise<void> {
|
||||
for (const label of checkboxLabels) {
|
||||
const checkbox = page.locator('.glossary-dropdown-label', {
|
||||
hasText: label,
|
||||
});
|
||||
await checkbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
export async function clickSaveButton(page: Page): Promise<void> {
|
||||
const saveButton = page.locator('.ant-btn-primary', {
|
||||
hasText: 'Save',
|
||||
});
|
||||
await saveButton.click();
|
||||
}
|
||||
|
||||
export async function verifyColumnsVisibility(
|
||||
page: Page,
|
||||
checkboxLabels: string[],
|
||||
shouldBeVisible: boolean
|
||||
): Promise<void> {
|
||||
const glossaryTermsTable = page.getByTestId('glossary-terms-table');
|
||||
|
||||
await expect(glossaryTermsTable).toBeVisible();
|
||||
|
||||
for (const label of checkboxLabels) {
|
||||
const termsColumnHeader = glossaryTermsTable.locator('th', {
|
||||
hasText: label,
|
||||
});
|
||||
if (shouldBeVisible) {
|
||||
await expect(termsColumnHeader).toBeVisible();
|
||||
} else {
|
||||
await expect(termsColumnHeader).toBeHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleAllColumnsSelection(
|
||||
page: Page,
|
||||
isSelected: boolean
|
||||
): Promise<void> {
|
||||
const dropdownButton = page.getByTestId('glossary-column-dropdown');
|
||||
|
||||
await expect(dropdownButton).toBeVisible();
|
||||
|
||||
await dropdownButton.click();
|
||||
|
||||
const checkboxLabel = 'All';
|
||||
const checkbox = page.locator('.custom-glossary-col-sel-checkbox', {
|
||||
hasText: checkboxLabel,
|
||||
});
|
||||
if (isSelected) {
|
||||
await checkbox.click();
|
||||
}
|
||||
await clickSaveButton(page);
|
||||
}
|
||||
|
||||
export async function verifyAllColumns(
|
||||
page: Page,
|
||||
tableColumns: string[],
|
||||
shouldBeVisible: boolean
|
||||
): Promise<void> {
|
||||
const glossaryTermsTable = page.getByTestId('glossary-terms-table');
|
||||
|
||||
await expect(glossaryTermsTable).toBeVisible();
|
||||
|
||||
for (const columnHeader of tableColumns) {
|
||||
const termsColumnHeader = glossaryTermsTable.locator('th', {
|
||||
hasText: columnHeader,
|
||||
});
|
||||
|
||||
if (shouldBeVisible) {
|
||||
await expect(termsColumnHeader).toBeVisible();
|
||||
} else {
|
||||
if (columnHeader !== 'TERMS') {
|
||||
await expect(termsColumnHeader).not.toBeVisible();
|
||||
} else {
|
||||
await expect(termsColumnHeader).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export const filterStatus = async (
|
||||
page: Page,
|
||||
statusLabels: string[],
|
||||
expectedStatus: string[]
|
||||
): Promise<void> => {
|
||||
const dropdownButton = page.getByTestId('glossary-status-dropdown');
|
||||
await dropdownButton.click();
|
||||
|
||||
for (const label of statusLabels) {
|
||||
const checkbox = page.locator('.glossary-dropdown-label', {
|
||||
hasText: label,
|
||||
});
|
||||
await checkbox.click();
|
||||
}
|
||||
|
||||
const saveButton = page.locator('.ant-btn-primary', {
|
||||
hasText: 'Save',
|
||||
});
|
||||
await saveButton.click();
|
||||
|
||||
const glossaryTermsTable = page.getByTestId('glossary-terms-table');
|
||||
const rows = glossaryTermsTable.locator('tbody tr');
|
||||
const statusColumnIndex = 3;
|
||||
|
||||
for (let i = 0; i < (await rows.count()); i++) {
|
||||
const statusCell = rows
|
||||
.nth(i)
|
||||
.locator(`td:nth-child(${statusColumnIndex + 1})`);
|
||||
const statusText = await statusCell.textContent();
|
||||
|
||||
expect(expectedStatus).toContain(statusText);
|
||||
}
|
||||
};
|
||||
|
||||
export const dragAndDropColumn = async (
|
||||
page: Page,
|
||||
dragColumn: string,
|
||||
dropColumn: string
|
||||
) => {
|
||||
await page
|
||||
.locator('.draggable-menu-item', { hasText: dragColumn })
|
||||
.dragTo(page.locator('.draggable-menu-item', { hasText: dropColumn }));
|
||||
};
|
||||
|
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="7" viewBox="0 0 14 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 2.16671H0.99998C0.823169 2.16671 0.653599 2.09647 0.528575 1.97144C0.403551 1.84642 0.333313 1.67685 0.333313 1.50004C0.333313 1.32323 0.403551 1.15366 0.528575 1.02864C0.653599 0.903612 0.823169 0.833374 0.99998 0.833374H13C13.1768 0.833374 13.3464 0.903612 13.4714 1.02864C13.5964 1.15366 13.6667 1.32323 13.6667 1.50004C13.6667 1.67685 13.5964 1.84642 13.4714 1.97144C13.3464 2.09647 13.1768 2.16671 13 2.16671ZM13.6667 5.50004C13.6667 5.32323 13.5964 5.15366 13.4714 5.02863C13.3464 4.90361 13.1768 4.83337 13 4.83337H0.99998C0.823169 4.83337 0.653599 4.90361 0.528575 5.02863C0.403551 5.15366 0.333313 5.32323 0.333313 5.50004C0.333313 5.67685 0.403551 5.84642 0.528575 5.97144C0.653599 6.09647 0.823169 6.16671 0.99998 6.16671H13C13.1768 6.16671 13.3464 6.09647 13.4714 5.97144C13.5964 5.84642 13.6667 5.67685 13.6667 5.50004Z" fill="#757575"/>
|
||||
</svg>
|
After Width: | Height: | Size: 965 B |
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Checkbox } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { ReactComponent as ColumnDragIcon } from '../../../assets/svg/menu-duo.svg';
|
||||
|
||||
interface DraggableMenuItemProps {
|
||||
option: { value: string; label: string };
|
||||
index: number;
|
||||
options: { value: string; label: string }[];
|
||||
onMoveItem: (updatedList: { value: string; label: string }[]) => void;
|
||||
selectedOptions: string[];
|
||||
onSelect: (key: string, checked: boolean, type: 'columns' | 'status') => void;
|
||||
}
|
||||
const DraggableMenuItem: React.FC<DraggableMenuItemProps> = ({
|
||||
option,
|
||||
index,
|
||||
options,
|
||||
onMoveItem,
|
||||
selectedOptions,
|
||||
onSelect,
|
||||
}) => {
|
||||
const moveDropdownMenuItem = useCallback(
|
||||
(fromIndex: number, toIndex: number) => {
|
||||
const updatedList = [...options];
|
||||
const [movedItem] = updatedList.splice(fromIndex, 1);
|
||||
updatedList.splice(toIndex, 0, movedItem);
|
||||
onMoveItem(updatedList);
|
||||
},
|
||||
[options, onMoveItem]
|
||||
);
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'CHECKBOX',
|
||||
item: { index },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [, drop] = useDrop({
|
||||
accept: 'CHECKBOX',
|
||||
hover: (draggedItem: any) => {
|
||||
if (draggedItem.index !== index) {
|
||||
moveDropdownMenuItem(draggedItem.index, index);
|
||||
draggedItem.index = index;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`draggable-menu-item ${isDragging ? 'dragging' : ''}`}
|
||||
ref={(node) => {
|
||||
drag(drop(node));
|
||||
}}>
|
||||
<ColumnDragIcon
|
||||
className="glossary-col-dropdown-drag-icon m-l-xs m-t-xs"
|
||||
height={16}
|
||||
width={16}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={selectedOptions.includes(option.value)}
|
||||
className="custom-glossary-col-sel-checkbox m-l-sm"
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onChange={(e) => onSelect(option.value, e.target.checked, 'columns')}>
|
||||
<p className="glossary-dropdown-label">{option.label}</p>
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableMenuItem;
|
@ -11,10 +11,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import Icon from '@ant-design/icons/lib/components/Icon';
|
||||
import { Button, Col, Modal, Row, Space, TableProps, Tooltip } from 'antd';
|
||||
import { ColumnsType, ExpandableConfig } from 'antd/lib/table/interface';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Col,
|
||||
Dropdown,
|
||||
Modal,
|
||||
Row,
|
||||
Space,
|
||||
TableProps,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
ColumnsType,
|
||||
ColumnType,
|
||||
ExpandableConfig,
|
||||
} from 'antd/lib/table/interface';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { compare } from 'fast-json-patch';
|
||||
@ -38,6 +52,7 @@ import StatusBadge from '../../../components/common/StatusBadge/StatusBadge.comp
|
||||
import {
|
||||
API_RES_MAX_SIZE,
|
||||
DE_ACTIVE_COLOR,
|
||||
NO_DATA_PLACEHOLDER,
|
||||
TEXT_BODY_COLOR,
|
||||
} from '../../../constants/constants';
|
||||
import { GLOSSARIES_DOCS } from '../../../constants/docs.constants';
|
||||
@ -49,7 +64,6 @@ import {
|
||||
GlossaryTerm,
|
||||
Status,
|
||||
} from '../../../generated/entity/data/glossaryTerm';
|
||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||
import {
|
||||
getFirstLevelGlossaryTerms,
|
||||
getGlossaryTerms,
|
||||
@ -64,13 +78,14 @@ import {
|
||||
findExpandableKeysForArray,
|
||||
findGlossaryTermByFqn,
|
||||
StatusClass,
|
||||
StatusFilters,
|
||||
} from '../../../utils/GlossaryUtils';
|
||||
import { getGlossaryPath } from '../../../utils/RouterUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import { DraggableBodyRowProps } from '../../common/Draggable/DraggableBodyRowProps.interface';
|
||||
import Loader from '../../common/Loader/Loader';
|
||||
import Table from '../../common/Table/Table';
|
||||
import TagButton from '../../common/TagButton/TagButton.component';
|
||||
import DraggableMenuItem from '../GlossaryColumnsSelectionDropdown/DraggableMenuItem.component';
|
||||
import { ModifiedGlossary, useGlossaryStore } from '../useGlossary.store';
|
||||
import {
|
||||
GlossaryTermTabProps,
|
||||
@ -89,7 +104,6 @@ const GlossaryTermTab = ({
|
||||
}: GlossaryTermTabProps) => {
|
||||
const { activeGlossary, glossaryChildTerms, setGlossaryChildTerms } =
|
||||
useGlossaryStore();
|
||||
const { theme } = useApplicationStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const glossaryTerms = (glossaryChildTerms as ModifiedGlossaryTerm[]) ?? [];
|
||||
@ -100,6 +114,19 @@ const GlossaryTermTab = ({
|
||||
const [isTableLoading, setIsTableLoading] = useState(false);
|
||||
const [isTableHovered, setIsTableHovered] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
const [isStatusDropdownVisible, setIsStatusDropdownVisible] =
|
||||
useState<boolean>(false);
|
||||
const statusOptions = useMemo(
|
||||
() =>
|
||||
Object.values(Status).map((status) => ({ value: status, label: status })),
|
||||
[]
|
||||
);
|
||||
const [statusDropdownSelection, setStatusDropdownSelections] = useState<
|
||||
string[]
|
||||
>(['Approved', 'Draft']);
|
||||
const [selectedStatus, setSelectedStatus] = useState<string[]>([
|
||||
...statusDropdownSelection,
|
||||
]);
|
||||
|
||||
const glossaryTermStatus: Status | null = useMemo(() => {
|
||||
if (!isGlossary) {
|
||||
@ -162,6 +189,36 @@ const GlossaryTermTab = ({
|
||||
<span className="text-grey-muted">{t('label.no-description')}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('label.reviewer'),
|
||||
dataIndex: 'reviewers',
|
||||
key: 'reviewers',
|
||||
width: '33%',
|
||||
render: (reviewers: EntityReference[]) => (
|
||||
<OwnerLabel owners={reviewers} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('label.synonym-plural'),
|
||||
dataIndex: 'synonyms',
|
||||
key: 'synonyms',
|
||||
width: '33%',
|
||||
render: (synonyms: string[]) => {
|
||||
return isEmpty(synonyms) ? (
|
||||
<div>{NO_DATA_PLACEHOLDER}</div>
|
||||
) : (
|
||||
<div className="d-flex flex-wrap">
|
||||
{synonyms.map((synonym: string) => (
|
||||
<TagButton
|
||||
className="glossary-synonym-tag"
|
||||
key={synonym}
|
||||
label={synonym}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.owner-plural'),
|
||||
dataIndex: 'owners',
|
||||
@ -174,14 +231,6 @@ const GlossaryTermTab = ({
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '12%',
|
||||
filterIcon: (filtered) => (
|
||||
<FilterOutlined
|
||||
style={{
|
||||
color: filtered ? theme.primaryColor : undefined,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
filters: StatusFilters,
|
||||
render: (_, record) => {
|
||||
const status = record.status ?? Status.Approved;
|
||||
|
||||
@ -249,6 +298,303 @@ const GlossaryTermTab = ({
|
||||
return data;
|
||||
}, [glossaryTerms, permissions]);
|
||||
|
||||
const listOfVisibleColumns = useMemo(() => {
|
||||
return ['name', 'description', 'owners', 'status', 'new-term'];
|
||||
}, []);
|
||||
|
||||
const defaultCheckedList = useMemo(
|
||||
() =>
|
||||
columns.reduce<string[]>(
|
||||
(acc, column) =>
|
||||
listOfVisibleColumns.includes(column.key as string)
|
||||
? [...acc, column.key as string]
|
||||
: acc,
|
||||
[]
|
||||
),
|
||||
[columns, listOfVisibleColumns]
|
||||
);
|
||||
|
||||
const [selectedColumns, setSelectedColumns] =
|
||||
useState<string[]>(defaultCheckedList);
|
||||
const [columnDropdownSelections, setColumnDropdownSelections] = useState<
|
||||
string[]
|
||||
>([...selectedColumns]);
|
||||
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
|
||||
|
||||
const [options, setOptions] = useState<{ value: string; label: string }[]>(
|
||||
columns.reduce<{ value: string; label: string }[]>(
|
||||
(acc, { key, title }) =>
|
||||
key !== 'name'
|
||||
? [...acc, { label: title as string, value: key as string }]
|
||||
: acc,
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
||||
const newColumns = useMemo(() => {
|
||||
return columns.map((item) => ({
|
||||
...item,
|
||||
hidden: !selectedColumns.includes(item.key as string),
|
||||
}));
|
||||
}, [columns, selectedColumns]);
|
||||
|
||||
const rearrangedColumns = useMemo(
|
||||
() =>
|
||||
newColumns
|
||||
.filter((column) => !column.hidden)
|
||||
.sort((a, b) => {
|
||||
const aIndex = options.findIndex(
|
||||
(option: { value: string; label: string }) => option.value === a.key
|
||||
);
|
||||
const bIndex = options.findIndex(
|
||||
(option: { value: string; label: string }) => option.value === b.key
|
||||
);
|
||||
|
||||
return aIndex - bIndex;
|
||||
}),
|
||||
[newColumns]
|
||||
);
|
||||
|
||||
const handleColumnSelectionDropdownSave = useCallback(() => {
|
||||
setSelectedColumns(columnDropdownSelections);
|
||||
setIsDropdownVisible(false);
|
||||
}, [columnDropdownSelections]);
|
||||
|
||||
const handleColumnSelectionDropdownCancel = useCallback(() => {
|
||||
setColumnDropdownSelections(selectedColumns);
|
||||
setIsDropdownVisible(false);
|
||||
}, [selectedColumns]);
|
||||
|
||||
const handleMoveItem = (updatedList: { value: string; label: string }[]) => {
|
||||
setOptions(updatedList);
|
||||
setColumnDropdownSelections((prevCheckedList) => {
|
||||
const updatedCheckedList = prevCheckedList.map((item) => {
|
||||
const index = updatedList.findIndex((option) => option.value === item);
|
||||
|
||||
return updatedList[index]?.value || item;
|
||||
});
|
||||
|
||||
return updatedCheckedList;
|
||||
});
|
||||
};
|
||||
const handleCheckboxChange = useCallback(
|
||||
(key: string, checked: boolean, type: 'columns' | 'status') => {
|
||||
const setCheckedList =
|
||||
type === 'columns'
|
||||
? setColumnDropdownSelections
|
||||
: setStatusDropdownSelections;
|
||||
|
||||
const optionsToUse =
|
||||
type === 'columns'
|
||||
? (columns as ColumnType<ModifiedGlossaryTerm>[])
|
||||
: (statusOptions as { value: string }[]);
|
||||
|
||||
if (key === 'all') {
|
||||
if (checked) {
|
||||
const newCheckedList = [
|
||||
'all',
|
||||
...optionsToUse.map((option) => {
|
||||
return type === 'columns'
|
||||
? String((option as ColumnType<ModifiedGlossaryTerm>).key)
|
||||
: (option as { value: string }).value ?? '';
|
||||
}),
|
||||
];
|
||||
setCheckedList(newCheckedList);
|
||||
} else {
|
||||
setCheckedList([type === 'columns' ? 'name' : 'Draft']);
|
||||
}
|
||||
} else {
|
||||
setCheckedList((prev: string[]) => {
|
||||
const newCheckedList = checked
|
||||
? [...prev, key]
|
||||
: prev.filter((item) => item !== key);
|
||||
|
||||
const allChecked =
|
||||
type === 'columns'
|
||||
? (optionsToUse as ColumnType<ModifiedGlossaryTerm>[]).every(
|
||||
(opt) => newCheckedList.includes(String(opt.key))
|
||||
)
|
||||
: (optionsToUse as { value: string }[]).every((opt) =>
|
||||
newCheckedList.includes(opt.value ?? '')
|
||||
);
|
||||
|
||||
if (allChecked) {
|
||||
return ['all', ...newCheckedList];
|
||||
}
|
||||
|
||||
return newCheckedList.filter((item) => item !== 'all');
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
columns,
|
||||
statusOptions,
|
||||
setColumnDropdownSelections,
|
||||
setStatusDropdownSelections,
|
||||
]
|
||||
);
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: [
|
||||
{
|
||||
key: 'addColumn',
|
||||
label: (
|
||||
<div className="glossary-col-sel-dropdown-title">
|
||||
<p className="m-l-md">{t('label.add-column')}</p>
|
||||
<Checkbox.Group
|
||||
className="glossary-col-sel-checkbox-group"
|
||||
value={columnDropdownSelections}>
|
||||
<Checkbox
|
||||
checked={columns
|
||||
.filter((col) => col.key === 'name')
|
||||
.every(({ key }) =>
|
||||
columnDropdownSelections.includes(key as string)
|
||||
)}
|
||||
className="custom-glossary-col-sel-checkbox m-l-lg p-l-md"
|
||||
key="all"
|
||||
value="all"
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange('all', e.target.checked, 'columns')
|
||||
}>
|
||||
{t('label.all')}
|
||||
</Checkbox>
|
||||
{options.map(
|
||||
(option: { value: string; label: string }, index: number) => (
|
||||
<div
|
||||
className="d-flex justify-start items-center"
|
||||
key={option.value}>
|
||||
<DraggableMenuItem
|
||||
index={index}
|
||||
option={option}
|
||||
options={options}
|
||||
selectedOptions={columnDropdownSelections}
|
||||
onMoveItem={handleMoveItem}
|
||||
onSelect={handleCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'divider',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: (
|
||||
<div className="flex-center">
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleColumnSelectionDropdownSave}>
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleColumnSelectionDropdownCancel}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
[
|
||||
columnDropdownSelections,
|
||||
columns,
|
||||
options,
|
||||
handleCheckboxChange,
|
||||
handleColumnSelectionDropdownSave,
|
||||
handleColumnSelectionDropdownCancel,
|
||||
]
|
||||
);
|
||||
const handleStatusSelectionDropdownSave = () => {
|
||||
setSelectedStatus(statusDropdownSelection);
|
||||
setIsStatusDropdownVisible(false);
|
||||
};
|
||||
|
||||
const handleStatusSelectionDropdownCancel = () => {
|
||||
setStatusDropdownSelections(selectedStatus);
|
||||
setIsStatusDropdownVisible(false);
|
||||
};
|
||||
const statusDropdownMenu = useMemo(
|
||||
() => ({
|
||||
items: [
|
||||
{
|
||||
key: 'statusSelection',
|
||||
label: (
|
||||
<div className="status-selection-dropdown">
|
||||
<Checkbox.Group
|
||||
className="glossary-col-sel-checkbox-group"
|
||||
value={statusDropdownSelection}>
|
||||
<Checkbox
|
||||
className="custom-glossary-col-sel-checkbox"
|
||||
key="all"
|
||||
value="all"
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange('all', e.target.checked, 'status')
|
||||
}>
|
||||
<p className="glossary-dropdown-label">{t('label.all')}</p>
|
||||
</Checkbox>
|
||||
{statusOptions.map((option) => (
|
||||
<div key={option.value}>
|
||||
<Checkbox
|
||||
className="custom-glossary-col-sel-checkbox"
|
||||
value={option.value}
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange(
|
||||
option.value,
|
||||
e.target.checked,
|
||||
'status'
|
||||
)
|
||||
}>
|
||||
<p className="glossary-dropdown-label">{option.label}</p>
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'divider',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: (
|
||||
<div className="flex-center">
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleStatusSelectionDropdownSave}>
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleStatusSelectionDropdownCancel}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
[
|
||||
statusDropdownSelection,
|
||||
statusOptions,
|
||||
handleStatusSelectionDropdownSave,
|
||||
handleStatusSelectionDropdownCancel,
|
||||
]
|
||||
);
|
||||
|
||||
const handleAddGlossaryTermClick = () => {
|
||||
onAddGlossaryTerm(
|
||||
!isGlossary ? (activeGlossary as GlossaryTerm) : undefined
|
||||
@ -468,6 +814,52 @@ const GlossaryTermTab = ({
|
||||
{isAllExpanded ? t('label.collapse-all') : t('label.expand-all')}
|
||||
</Space>
|
||||
</Button>
|
||||
<Dropdown
|
||||
className="custom-glossary-dropdown-menu status-dropdown"
|
||||
getPopupContainer={(trigger) => {
|
||||
const customContainer = trigger.closest(
|
||||
'.custom-glossary-dropdown-menu.status-dropdown'
|
||||
);
|
||||
|
||||
return customContainer as HTMLElement;
|
||||
}}
|
||||
menu={statusDropdownMenu}
|
||||
open={isStatusDropdownVisible}
|
||||
trigger={['click']}
|
||||
onOpenChange={setIsStatusDropdownVisible}>
|
||||
<Button
|
||||
className="custom-status-dropdown-btn m-r-sm"
|
||||
data-testid="glossary-status-dropdown">
|
||||
<Space>
|
||||
{t('label.status')}
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Dropdown
|
||||
className="mb-4 custom-glossary-dropdown-menu"
|
||||
getPopupContainer={(trigger) => {
|
||||
const customContainer = trigger.closest(
|
||||
'.custom-glossary-dropdown-menu'
|
||||
);
|
||||
|
||||
return customContainer as HTMLElement;
|
||||
}}
|
||||
menu={menu}
|
||||
open={isDropdownVisible}
|
||||
trigger={['click']}
|
||||
onOpenChange={setIsDropdownVisible}>
|
||||
<Button
|
||||
className="custom-status-dropdown-btn m-r-xs"
|
||||
data-testid="glossary-column-dropdown">
|
||||
<Space>
|
||||
{t('label.column-plural')}
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DndProvider>
|
||||
</div>
|
||||
|
||||
{glossaryTerms.length > 0 ? (
|
||||
@ -477,9 +869,12 @@ const GlossaryTermTab = ({
|
||||
className={classNames('drop-over-background', {
|
||||
'drop-over-table': isTableHovered,
|
||||
})}
|
||||
columns={columns}
|
||||
columns={rearrangedColumns.filter((col) => !col.hidden)}
|
||||
components={TABLE_CONSTANTS}
|
||||
dataSource={glossaryTerms}
|
||||
data-testid="glossary-terms-table"
|
||||
dataSource={glossaryTerms.filter((term) =>
|
||||
selectedStatus.includes(term.status as string)
|
||||
)}
|
||||
expandable={expandableConfig}
|
||||
loading={isTableLoading}
|
||||
pagination={false}
|
||||
|
@ -90,3 +90,102 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-glossary-col-sel-checkbox {
|
||||
font-size: 16px;
|
||||
color: @grey-3;
|
||||
width: 100%;
|
||||
|
||||
.ant-checkbox-inner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: @white;
|
||||
border-color: @grey-4;
|
||||
|
||||
&::after {
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border-color: @purple-2;
|
||||
border-width: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: @white;
|
||||
border-color: @grey-4;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper:hover .ant-checkbox-inner,
|
||||
.ant-checkbox:hover .ant-checkbox-inner,
|
||||
.ant-checkbox-input:focus + .ant-checkbox-inner {
|
||||
border-color: @grey-4;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-glossary-dropdown-menu {
|
||||
.ant-dropdown-menu {
|
||||
background-color: @white;
|
||||
border-color: @grey-4;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.ant-dropdown-menu-item:hover {
|
||||
background-color: @white;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
.glossary-col-sel-dropdown-title {
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
color: @grey-4;
|
||||
}
|
||||
|
||||
.glossary-col-sel-checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background-color: @white;
|
||||
border-color: @grey-3;
|
||||
}
|
||||
.status-dropdown {
|
||||
.ant-dropdown-menu-item {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
.draggable-menu-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: background-color 0.3s ease, opacity 0.3s ease;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.draggable-menu-item.dragging {
|
||||
background-color: #f0f0f0;
|
||||
opacity: 0.8;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.glossary-dropdown-label {
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
color: @grey-4;
|
||||
}
|
||||
|
||||
.custom-status-dropdown-btn {
|
||||
background-color: @white;
|
||||
outline: 1px solid @grey-2;
|
||||
color: @black;
|
||||
}
|
||||
|
||||
.glossary-col-dropdown-drag-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Hinzufügen",
|
||||
"add-a-new-service": "Neuen Service hinzufügen",
|
||||
"add-an-image": "Add an image",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Benutzerdefinierte Eigenschaft zu {{entity}} hinzufügen",
|
||||
"add-deploy": "Hinzufügen und Bereitstellen",
|
||||
"add-entity": "{{entity}} hinzufügen",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Add",
|
||||
"add-a-new-service": "Add a New Service",
|
||||
"add-an-image": "Add an image",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Add Custom {{entity}} Property",
|
||||
"add-deploy": "Add & Deploy",
|
||||
"add-entity": "Add {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Añadir",
|
||||
"add-a-new-service": "Añadir un nuevo servicio",
|
||||
"add-an-image": "Añadir una imagen",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Añadir propiedad personalizada de {{entity}}",
|
||||
"add-deploy": "Añadir y desplegar",
|
||||
"add-entity": "Añadir {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Ajouter",
|
||||
"add-a-new-service": "Ajouter un Nouveau Service",
|
||||
"add-an-image": "Ajouter une Image",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Ajouter une Propriété Personnalisée à {{entity}}",
|
||||
"add-deploy": "Ajouter et Déployer",
|
||||
"add-entity": "Ajouter {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Engadir",
|
||||
"add-a-new-service": "Engadir un novo servizo",
|
||||
"add-an-image": "Engadir unha imaxe",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Engadir unha propiedade personalizada de {{entity}}",
|
||||
"add-deploy": "Engadir e despregar",
|
||||
"add-entity": "Engadir {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "הוסף",
|
||||
"add-a-new-service": "הוסף שירות חדש",
|
||||
"add-an-image": "הוסף תמונה",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "הוסף נכס מותאם אישית ל {{entity}}",
|
||||
"add-deploy": "הוסף והפעל",
|
||||
"add-entity": "הוסף {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "追加",
|
||||
"add-a-new-service": "新しいサービスを追加",
|
||||
"add-an-image": "Add an image",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Add Custom {{entity}} Property",
|
||||
"add-deploy": "追加&デプロイ",
|
||||
"add-entity": "{{entity}}を追加",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Toevoegen",
|
||||
"add-a-new-service": "Nieuwe service toevoegen",
|
||||
"add-an-image": "Afbeelding toevoegen",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Aangepaste {{entity}} eigenschap toevoegen",
|
||||
"add-deploy": "Toevoegen en deployen",
|
||||
"add-entity": "{{entity}} toevoegen",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "اضافه کردن",
|
||||
"add-a-new-service": "اضافه کردن یک سرویس جدید",
|
||||
"add-an-image": "اضافه کردن تصویر",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "اضافه کردن خصوصیت سفارشی {{entity}}",
|
||||
"add-deploy": "اضافه کردن و استقرار",
|
||||
"add-entity": "اضافه کردن {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Adicionar",
|
||||
"add-a-new-service": "Adicionar um Novo Serviço",
|
||||
"add-an-image": "Adicionar uma imagem",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Adicionar Propriedade Personalizada {{entity}}",
|
||||
"add-deploy": "Adicionar & Implementar",
|
||||
"add-entity": "Adicionar {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "Добавить",
|
||||
"add-a-new-service": "Добавить новый сервис",
|
||||
"add-an-image": "Add an image",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "Добавить пользовательское свойство {{entity}}",
|
||||
"add-deploy": "Добавить & Развернуть",
|
||||
"add-entity": "Добавить {{entity}}",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"add": "添加",
|
||||
"add-a-new-service": "添加新服务",
|
||||
"add-an-image": "添加图片",
|
||||
"add-column": "Add Column",
|
||||
"add-custom-entity-property": "添加自定义{{entity}}属性",
|
||||
"add-deploy": "添加部署",
|
||||
"add-entity": "添加{{entity}}",
|
||||
|
@ -335,7 +335,11 @@ export const getFirstLevelGlossaryTerms = async (parentFQN: string) => {
|
||||
>(apiUrl, {
|
||||
params: {
|
||||
directChildrenOf: parentFQN,
|
||||
fields: [TabSpecificField.CHILDREN_COUNT, TabSpecificField.OWNERS],
|
||||
fields: [
|
||||
TabSpecificField.CHILDREN_COUNT,
|
||||
TabSpecificField.OWNERS,
|
||||
TabSpecificField.REVIEWERS,
|
||||
],
|
||||
limit: 100000,
|
||||
},
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user