add selection widgets to modal

This commit is contained in:
Pranita 2025-06-26 12:17:35 +05:30
parent 79c3398b51
commit ccc230b5a3
6 changed files with 258 additions and 17 deletions

View File

@ -0,0 +1,48 @@
/*
* 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 { Col, Row } from 'antd';
import React, { forwardRef } from 'react';
import { Document } from '../../../../generated/entity/docStore/document';
import WidgetCard from '../WidgetCard/WidgetCard';
import './all-widgets-content.less';
interface AllWidgetsContentProps {
widgets: Document[];
selectedWidgets: string[];
onSelectWidget: (id: string) => void;
}
const AllWidgetsContent = forwardRef<HTMLDivElement, AllWidgetsContentProps>(
({ widgets, selectedWidgets, onSelectWidget }, ref) => {
return (
<Row className="all-widgets-grid" gutter={[20, 20]} ref={ref}>
{widgets.map((widget) => (
<Col
data-widget-key={widget.fullyQualifiedName}
key={widget.id}
lg={8}
md={12}
sm={24}>
<WidgetCard
isSelected={selectedWidgets.includes(widget.id ?? '')}
widget={widget}
onSelect={() => onSelectWidget(widget.id ?? '')}
/>
</Col>
))}
</Row>
);
}
);
export default AllWidgetsContent;

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
.all-widgets-grid {
max-height: calc(100vh - 220px); // Adjust as needed
overflow-y: auto;
padding-right: 8px;
}

View File

@ -12,11 +12,18 @@
*/
import Icon from '@ant-design/icons';
import { Button, Col, Divider, Modal, Row, Typography } from 'antd';
import React, { useState } from 'react';
import { AxiosError } from 'axios';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as AddIcon } from '../../../../assets/svg/add-square.svg';
import { PAGE_SIZE_MEDIUM } from '../../../../constants/constants';
import { DEFAULT_HEADER_BG_COLOR } from '../../../../constants/Mydata.constants';
import { LandingPageWidgetKeys } from '../../../../enums/CustomizablePage.enum';
import { Document } from '../../../../generated/entity/docStore/document';
import { getAllKnowledgePanels } from '../../../../rest/DocStoreAPI';
import { showErrorToast } from '../../../../utils/ToastUtils';
import HeaderTheme from '../../HeaderTheme/HeaderTheme';
import AllWidgetsContent from '../AllWidgetsContent/AllWidgetsContent';
import './customise-home-modal.less';
interface CustomiseHomeModalProps {
@ -37,6 +44,51 @@ const CustomiseHomeModal = ({
currentBackgroundColor ?? DEFAULT_HEADER_BG_COLOR
);
const [widgets, setWidgets] = useState<Document[]>([]);
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
const [selectedKey, setSelectedKey] = useState('header-theme');
const contentRef = useRef<HTMLDivElement>(null);
const fetchWidgets = async () => {
try {
const { data } = await getAllKnowledgePanels({
fqnPrefix: 'KnowledgePanel',
limit: PAGE_SIZE_MEDIUM,
});
setWidgets(
data.filter(
(widget) =>
widget.fullyQualifiedName !== LandingPageWidgetKeys.ANNOUNCEMENTS
)
);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
useEffect(() => {
fetchWidgets();
}, []);
const handleSelectWidget = (id: string) => {
setSelectedWidgets((prev) =>
prev.includes(id) ? prev.filter((w) => w !== id) : [...prev, id]
);
};
const handleSidebarClick = (key: string) => {
if (key === 'header-theme' || key === 'all-widgets') {
setSelectedKey(key);
} else {
const target = contentRef.current?.querySelector(
`[data-widget-key="${key}"]`
);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
};
const customiseOptions = [
{
key: 'header-theme',
@ -51,15 +103,57 @@ const CustomiseHomeModal = ({
{
key: 'all-widgets',
label: t('label.all-widgets'),
component: <div>{t('label.all-widgets')}</div>,
component: (
<AllWidgetsContent
ref={contentRef}
selectedWidgets={selectedWidgets}
widgets={widgets}
onSelectWidget={handleSelectWidget}
/>
),
},
];
const [selectedKey, setSelectedKey] = useState(customiseOptions[0].key);
const sidebarItems = [
...customiseOptions.map(({ key, label }) => ({ key, label })),
...widgets.map((widget) => ({
key: widget.fullyQualifiedName,
label: widget.name,
})),
];
const selectedComponent = customiseOptions.find(
(item) => item.key === selectedKey
)?.component;
const selectedComponent = useMemo(() => {
return customiseOptions.find((item) => item.key === selectedKey)?.component;
}, [customiseOptions, selectedKey]);
const sidebarOptions = useMemo(() => {
return (
<>
{sidebarItems.map((item) => {
const isWidgetItem =
item.key !== 'header-theme' && item.key !== 'all-widgets';
const isAllWidgets = item.key === 'all-widgets';
return (
<div
className={`sidebar-option text-md font-semibold border-radius-xs cursor-pointer d-flex flex-wrap items-center
${isWidgetItem ? 'sidebar-widget-item' : ''}
${selectedKey === item.key ? 'active' : ''}`}
key={item.key}
onClick={() => handleSidebarClick(item.key)}>
<span>{item.label}</span>
{isAllWidgets && (
<span className="widget-count text-xs border-radius-md m-l-sm">
{widgets.length}
</span>
)}
</div>
);
})}
</>
);
}, [sidebarItems, selectedKey, handleSidebarClick]);
const handleApply = () => {
if (onBackgroundColorUpdate) {
@ -88,17 +182,7 @@ const CustomiseHomeModal = ({
onCancel={onClose}>
<Row className="customise-home-modal-body d-flex gap-1">
<Col className="sidebar p-box sticky top-0 self-start">
{customiseOptions.map((item) => (
<div
className={`sidebar-option text-md font-semibold border-radius-xs cursor-pointer ${
selectedKey === item.key ? 'active' : ''
}`}
data-testid={`sidebar-option-${item.key}`}
key={item.key}
onClick={() => setSelectedKey(item.key)}>
{item.label}
</div>
))}
{sidebarOptions}
</Col>
<Divider
className="customise-home-modal-divider h-auto self-stretch"

View File

@ -57,6 +57,11 @@
color: @blue-9;
}
}
.sidebar-widget-item {
color: @grey-600;
font-weight: @font-regular;
padding: @padding-xs @padding-md @padding-xs @padding-lg;
}
}
.customise-home-modal-divider {
@ -94,3 +99,9 @@
}
}
}
.widget-count {
color: @blue-9;
background-color: @alert-info-icon-bg-1;
padding: 2px @padding-xs;
}

View File

@ -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 { CheckOutlined } from '@ant-design/icons';
import { Card, Typography } from 'antd';
import React from 'react';
import { Document } from '../../../../generated/entity/docStore/document';
import './widget-card.less';
interface WidgetCardProps {
widget: Document;
isSelected: boolean;
onSelect: () => void;
}
const WidgetCard = ({ widget, isSelected, onSelect }: WidgetCardProps) => {
return (
<Card
hoverable
className={`widget-card ${isSelected ? 'selected' : ''}`}
onClick={onSelect}>
<div className="widget-card-content">
<Typography.Text strong>{widget.name}</Typography.Text>
{isSelected && <CheckOutlined className="check-icon" />}
</div>
<Typography.Paragraph className="widget-desc">
{widget.description || 'No description available.'}
</Typography.Paragraph>
</Card>
);
};
export default WidgetCard;

View File

@ -0,0 +1,39 @@
/*
* 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.
*/
.widget-card {
border: 1px solid #e5e5e5;
border-radius: 12px;
cursor: pointer;
transition: border 0.3s ease;
position: relative;
&.selected {
border: 2px solid #1890ff;
}
.widget-card-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.check-icon {
color: #1890ff;
font-size: 16px;
}
.widget-desc {
margin-top: 8px;
color: #888;
}
}