diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9fbf63bee..734be4ad7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import { useAlertStore } from "./store/alert-store.js"; import { useSessionStore } from "./store/session-store.js"; import PostHogPageviewTracker from "./PostHogPageviewTracker.js"; import { useEffect } from "react"; +import CustomMarkdown from "./components/helpers/custom-markdown/CustomMarkdown.jsx"; let GoogleTagManagerHelper; try { @@ -46,7 +47,7 @@ function App() { notificationAPI.open({ message: alertDetails?.title, - description: alertDetails?.content, + description: , type: alertDetails?.type, duration: alertDetails?.duration, btn, diff --git a/frontend/src/components/agency/display-logs/DisplayLogs.jsx b/frontend/src/components/agency/display-logs/DisplayLogs.jsx index 93cb7aca7..de456743a 100644 --- a/frontend/src/components/agency/display-logs/DisplayLogs.jsx +++ b/frontend/src/components/agency/display-logs/DisplayLogs.jsx @@ -3,6 +3,7 @@ import { Col, Row, Typography } from "antd"; import "./DisplayLogs.css"; import { useSocketLogsStore } from "../../../store/socket-logs-store"; +import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown"; function DisplayLogs() { const bottomRef = useRef(null); @@ -37,9 +38,10 @@ function DisplayLogs() { - - {log?.message} - + diff --git a/frontend/src/components/custom-tools/display-logs/DisplayLogs.jsx b/frontend/src/components/custom-tools/display-logs/DisplayLogs.jsx index f0f6b7bd0..1fa43cc0a 100644 --- a/frontend/src/components/custom-tools/display-logs/DisplayLogs.jsx +++ b/frontend/src/components/custom-tools/display-logs/DisplayLogs.jsx @@ -4,6 +4,7 @@ import { Col, Row, Typography } from "antd"; import "../../agency/display-logs/DisplayLogs.css"; import { useSocketCustomToolStore } from "../../../store/socket-custom-tool"; import { getDateTimeString } from "../../../helpers/GetStaticData"; +import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown"; function DisplayLogs() { const bottomRef = useRef(null); @@ -48,9 +49,10 @@ function DisplayLogs() { - - {message?.message} - +
diff --git a/frontend/src/components/helpers/custom-markdown/CustomMarkdown.jsx b/frontend/src/components/helpers/custom-markdown/CustomMarkdown.jsx new file mode 100644 index 000000000..173d60a6e --- /dev/null +++ b/frontend/src/components/helpers/custom-markdown/CustomMarkdown.jsx @@ -0,0 +1,112 @@ +import { useMemo } from "react"; +import { Typography } from "antd"; +import PropTypes from "prop-types"; + +const { Text, Link, Paragraph } = Typography; + +const CustomMarkdown = ({ + text = "", + renderNewLines = true, + isSecondary = false, + styleClassName, +}) => { + const textType = isSecondary ? "secondary" : undefined; + + /* + Patterns with bounded quantifiers to prevent performance issues: + - Triple code: ```...``` + - Inline code: `...` + - Bold: **...** + - Link: [text](url) + - New line: \n + */ + const patterns = [ + { type: "tripleCode", regex: /```([^`]{1,1000})```/g }, + { type: "inlineCode", regex: /`([^`]{1,100})`/g }, + { type: "bold", regex: /\*\*([^*]{1,100})\*\*/g }, + { type: "link", regex: /\[([^\]]{1,100})\]\(([^)]{1,200})\)/g }, + { type: "newline", regex: /\n/g }, + ]; + + const renderToken = (type, content, url) => { + switch (type) { + case "tripleCode": + return ( + +
{content}
+
+ ); + case "inlineCode": + return ( + + {content} + + ); + case "bold": + return ( + + {content} + + ); + case "link": + return ( + + {content} + + ); + case "newline": + return renderNewLines ?
: "\n"; + default: + return content; + } + }; + + const content = useMemo(() => { + let elements = [text]; + + // Process each pattern sequentially + patterns.forEach(({ type, regex }) => { + const newElements = []; + elements.forEach((element) => { + if (typeof element !== "string") { + newElements.push(element); + return; + } + + let lastIndex = 0; + let match; + while ((match = regex.exec(element)) !== null) { + const matchIndex = match.index; + if (matchIndex > lastIndex) { + newElements.push(element.substring(lastIndex, matchIndex)); + } + const url = match[2]; // Relevant only for link + newElements.push(renderToken(type, match[1], url)); + lastIndex = matchIndex + match[0].length; + } + + if (lastIndex < element.length) { + newElements.push(element.substring(lastIndex)); + } + }); + elements = newElements; + }); + + return elements; + }, [text, renderNewLines, textType]); + + return ( + + {content} + + ); +}; + +CustomMarkdown.propTypes = { + text: PropTypes.string, + renderNewLines: PropTypes.bool, + isSecondary: PropTypes.bool, + styleClassName: PropTypes.string, +}; + +export default CustomMarkdown; diff --git a/frontend/src/components/pipelines-or-deployments/log-modal/LogsModal.jsx b/frontend/src/components/pipelines-or-deployments/log-modal/LogsModal.jsx index a41f9e041..27f3e5b64 100644 --- a/frontend/src/components/pipelines-or-deployments/log-modal/LogsModal.jsx +++ b/frontend/src/components/pipelines-or-deployments/log-modal/LogsModal.jsx @@ -7,6 +7,7 @@ import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate.js"; import { useAlertStore } from "../../../store/alert-store.js"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx"; import "./LogsModel.css"; +import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown.jsx"; const LogsModal = ({ open, @@ -38,16 +39,16 @@ const LogsModal = ({ }; axiosPrivate(requestOptions) .then((res) => { - const logDetails = res.data.results.map((item) => ({ - id: item.id, - log: item.data?.log, - type: item.data?.type, - stage: item.data?.stage, - level: item.data?.level, - event_time: item.event_time, + const logDetails = res?.data?.results?.map((item) => ({ + id: item?.id, + log: , + type: item?.data?.type, + stage: item?.data?.stage, + level: item?.data?.level, + event_time: item?.event_time, })); setLogDetails(logDetails); - setTotalCount(res.data.count); + setTotalCount(res?.data?.count); }) .catch((err) => { setAlertDetails(handleException(err)); @@ -74,7 +75,7 @@ const LogsModal = ({ return ( @@ -85,7 +86,7 @@ const LogsModal = ({ title: "Status", dataIndex: "status", key: "status", - render: (level) => {level}, + render: (level) => {level}, }, ]; @@ -104,7 +105,7 @@ const LogsModal = ({ title: "Log Level", dataIndex: "level", key: "level", - render: (level) => {level}, + render: (level) => {level}, }, { title: "Log", @@ -112,6 +113,7 @@ const LogsModal = ({ key: "log", }, ]; + return ( <> { +const AltDateTimeWidget = ({ + id, + value, + onChange, + label, + schema, + required, +}) => { + const description = schema?.description || ""; const handleDateChange = (date) => { onChange(date?.toISOString()); }; @@ -17,7 +25,11 @@ const AltDateTimeWidget = ({ id, value, onChange, label, required }) => { }; return ( - + { +const AltDateWidget = ({ id, value, onChange, label, schema, required }) => { + const description = schema?.description || ""; + const handleDateChange = (date) => { onChange(date?.toISOString()); }; return ( - + { +import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown"; +const CheckboxWidget = ({ id, value, onChange, label, schema, required }) => { const description = schema?.description || ""; const handleCheckboxChange = (event) => { onChange(event.target.checked); }; return ( - + - {label} + + {required && * } + {label} + {description?.length > 0 && ( - - - + )} ); @@ -28,6 +33,7 @@ CheckboxWidget.propTypes = { onChange: PropTypes.func.isRequired, label: PropTypes.string.isRequired, schema: PropTypes.object.isRequired, + required: PropTypes.bool, }; export { CheckboxWidget }; diff --git a/frontend/src/components/rjsf-custom-widgets/checkboxes-widget/CheckboxesWidget.jsx b/frontend/src/components/rjsf-custom-widgets/checkboxes-widget/CheckboxesWidget.jsx index 18f11651a..76fbf0873 100644 --- a/frontend/src/components/rjsf-custom-widgets/checkboxes-widget/CheckboxesWidget.jsx +++ b/frontend/src/components/rjsf-custom-widgets/checkboxes-widget/CheckboxesWidget.jsx @@ -3,7 +3,16 @@ import { Checkbox } from "antd"; import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx"; -const CheckboxesWidget = ({ id, options, value, onChange, label }) => { +const CheckboxesWidget = ({ + id, + options, + value, + onChange, + label, + schema, + required, +}) => { + const description = schema?.description || ""; const handleCheckboxChange = (optionValue) => { const newValue = [...(value || [])]; const index = newValue.indexOf(optionValue); @@ -16,7 +25,11 @@ const CheckboxesWidget = ({ id, options, value, onChange, label }) => { }; return ( - + {options.map((option) => ( { +const ColorWidget = ({ id, value, onChange, schema, label, required }) => { + const description = schema?.description || ""; + const handleColorChange = (event) => { onChange(event.target.value); }; return ( - + ); @@ -19,7 +25,9 @@ ColorWidget.propTypes = { id: PropTypes.string.isRequired, value: PropTypes.string, onChange: PropTypes.func.isRequired, + schema: PropTypes.object.isRequired, label: PropTypes.string.isRequired, + required: PropTypes.bool, }; export { ColorWidget }; diff --git a/frontend/src/components/rjsf-custom-widgets/date-time-widget/DateTimeWidget.jsx b/frontend/src/components/rjsf-custom-widgets/date-time-widget/DateTimeWidget.jsx index 218988cf1..a3b2df307 100644 --- a/frontend/src/components/rjsf-custom-widgets/date-time-widget/DateTimeWidget.jsx +++ b/frontend/src/components/rjsf-custom-widgets/date-time-widget/DateTimeWidget.jsx @@ -4,13 +4,18 @@ import PropTypes from "prop-types"; import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx"; -const DateTimeWidget = ({ id, value, onChange, label, required }) => { +const DateTimeWidget = ({ id, value, onChange, label, schema, required }) => { + const description = schema?.description || ""; const handleDateTimeChange = (dateTime) => { onChange(dateTime?.toISOString()); }; return ( - + { +const DateWidget = ({ id, value, onChange, label, schema, required }) => { + const description = schema?.description || ""; const handleDateChange = (date) => { onChange(date?.toISOString()); }; return ( - + { +const EmailWidget = ({ id, value, onChange, label, schema, required }) => { + const description = schema?.description || ""; const handleEmailChange = (event) => { onChange(event.target.value); }; return ( - + ); @@ -20,6 +25,7 @@ EmailWidget.propTypes = { value: PropTypes.string, onChange: PropTypes.func.isRequired, label: PropTypes.string.isRequired, + schema: PropTypes.object.isRequired, required: PropTypes.bool, }; diff --git a/frontend/src/components/rjsf-custom-widgets/file-widget/FileWidget.jsx b/frontend/src/components/rjsf-custom-widgets/file-widget/FileWidget.jsx index 4ba89d1a6..4a43ed121 100644 --- a/frontend/src/components/rjsf-custom-widgets/file-widget/FileWidget.jsx +++ b/frontend/src/components/rjsf-custom-widgets/file-widget/FileWidget.jsx @@ -4,7 +4,8 @@ import PropTypes from "prop-types"; import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx"; -const FileWidget = ({ id, onChange, label, required }) => { +const FileWidget = ({ id, onChange, label, schema, required }) => { + const description = schema?.description || ""; const handleFileChange = (info) => { if (info.file.status === "done") { const fileUrl = info.file.response.url; // Assuming the response contains the uploaded file URL @@ -13,7 +14,11 @@ const FileWidget = ({ id, onChange, label, required }) => { }; return ( - + @@ -25,6 +30,7 @@ FileWidget.propTypes = { id: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, label: PropTypes.string.isRequired, + schema: PropTypes.object.isRequired, required: PropTypes.bool, }; diff --git a/frontend/src/components/rjsf-custom-widgets/select-widget/SelectWidget.jsx b/frontend/src/components/rjsf-custom-widgets/select-widget/SelectWidget.jsx index 8b6846591..49af88ee7 100644 --- a/frontend/src/components/rjsf-custom-widgets/select-widget/SelectWidget.jsx +++ b/frontend/src/components/rjsf-custom-widgets/select-widget/SelectWidget.jsx @@ -1,27 +1,44 @@ -import { Form, Select } from "antd"; +import { Form, Select, Space, Typography } from "antd"; import PropTypes from "prop-types"; +import CustomMarkdown from "../../helpers/custom-markdown/CustomMarkdown"; const { Option } = Select; const SelectWidget = (props) => { - const { id, value, options, onChange, rawErrors } = props; + const { id, value, options, onChange, label, schema, rawErrors } = props; + const description = schema?.description || ""; + const handleSelectChange = (selectedValue) => { onChange(selectedValue); }; const hasError = rawErrors && rawErrors.length > 0; return ( - - + + + {label} +
+ + {description?.length > 0 && ( + + )} +
+
); }; @@ -32,6 +49,8 @@ SelectWidget.propTypes = { options: PropTypes.any, onChange: PropTypes.func.isRequired, rawErrors: PropTypes.array, + label: PropTypes.string.isRequired, + schema: PropTypes.object.isRequired, }; export { SelectWidget }; diff --git a/frontend/src/components/rjsf-custom-widgets/time-widget/TimeWidget.jsx b/frontend/src/components/rjsf-custom-widgets/time-widget/TimeWidget.jsx index f26ced515..bad749ffa 100644 --- a/frontend/src/components/rjsf-custom-widgets/time-widget/TimeWidget.jsx +++ b/frontend/src/components/rjsf-custom-widgets/time-widget/TimeWidget.jsx @@ -4,13 +4,18 @@ import PropTypes from "prop-types"; import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx"; -const TimeWidget = ({ id, value, onChange, label, required }) => { +const TimeWidget = ({ id, value, onChange, label, schema, required }) => { + const description = schema?.description || ""; const handleTimeChange = (time) => { onChange(time?.toISOString()); }; return ( - + { +const UpDownWidget = ({ id, value, onChange, label, schema, required }) => { + const description = schema?.description || ""; const handleNumberChange = (numberValue) => { onChange(numberValue); }; return ( - + ); @@ -20,6 +25,7 @@ UpDownWidget.propTypes = { value: PropTypes.number, onChange: PropTypes.func.isRequired, label: PropTypes.string.isRequired, + schema: PropTypes.object.isRequired, required: PropTypes.bool, }; diff --git a/frontend/src/components/rjsf-custom-widgets/url-widget/URLWidget.jsx b/frontend/src/components/rjsf-custom-widgets/url-widget/URLWidget.jsx index c2aba010e..ab1425713 100644 --- a/frontend/src/components/rjsf-custom-widgets/url-widget/URLWidget.jsx +++ b/frontend/src/components/rjsf-custom-widgets/url-widget/URLWidget.jsx @@ -1,20 +1,22 @@ -import { Form, Input } from "antd"; +import { Input } from "antd"; import PropTypes from "prop-types"; +import { RjsfWidgetLayout } from "../../../layouts/rjsf-widget-layout/RjsfWidgetLayout"; -const URLWidget = ({ id, value, onChange, rawErrors }) => { +const URLWidget = (props) => { + const { id, value, onChange, label, schema, required } = props; + const description = schema?.description || ""; const handleURLChange = (event) => { onChange(event.target.value); }; - const hasError = rawErrors && rawErrors.length > 0; - return ( - - + ); }; @@ -22,7 +24,9 @@ URLWidget.propTypes = { id: PropTypes.string.isRequired, value: PropTypes.string, onChange: PropTypes.func.isRequired, - rawErrors: PropTypes.array, + label: PropTypes.string.isRequired, + schema: PropTypes.object.isRequired, + required: PropTypes.bool, }; export { URLWidget }; diff --git a/frontend/src/layouts/rjsf-form-layout/CustomFieldTemplate.jsx b/frontend/src/layouts/rjsf-form-layout/CustomFieldTemplate.jsx new file mode 100644 index 000000000..598e5cc44 --- /dev/null +++ b/frontend/src/layouts/rjsf-form-layout/CustomFieldTemplate.jsx @@ -0,0 +1,21 @@ +import PropTypes from "prop-types"; + +const CustomFieldTemplate = (props) => { + const { classNames, errors, children, help } = props; + return ( +
+ {children} + {errors} + {help} +
+ ); +}; + +CustomFieldTemplate.propTypes = { + classNames: PropTypes.string, + help: PropTypes.node, + errors: PropTypes.node, + children: PropTypes.node, +}; + +export { CustomFieldTemplate }; diff --git a/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.css b/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.css index 8edb72964..46a09663b 100644 --- a/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.css +++ b/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.css @@ -1,7 +1,7 @@ /* Styles for RjsfFormLayout */ .rjsf-form-layout-spin { - padding: 10px 0px; + padding: 10px 0px; } .my-rjsf-form .ant-form-item { @@ -19,3 +19,7 @@ padding-right: 0 !important; padding-bottom: 0 !important; } + +.rjsf-helper-font { + font-size: 12px; +} diff --git a/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.jsx b/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.jsx index 6c971c680..85bf8efb3 100644 --- a/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.jsx +++ b/frontend/src/layouts/rjsf-form-layout/RjsfFormLayout.jsx @@ -1,3 +1,4 @@ +import { useCallback, useMemo } from "react"; import Form from "@rjsf/antd"; import validator from "@rjsf/validator-ajv8"; import PropTypes from "prop-types"; @@ -17,6 +18,12 @@ import { SelectWidget } from "../../components/rjsf-custom-widgets/select-widget import { TimeWidget } from "../../components/rjsf-custom-widgets/time-widget/TimeWidget.jsx"; import { URLWidget } from "../../components/rjsf-custom-widgets/url-widget/URLWidget.jsx"; import { SpinnerLoader } from "../../components/widgets/spinner-loader/SpinnerLoader.jsx"; +import { TextWidget } from "../../components/rjsf-custom-widgets/text-widget/TextWidget.jsx"; +import { PasswordWidget } from "../../components/rjsf-custom-widgets/password-widget/PasswordWidget.jsx"; +import { UpDownWidget } from "../../components/rjsf-custom-widgets/up-down-widget/UpDownWidget.jsx"; +import { CustomFieldTemplate } from "./CustomFieldTemplate.jsx"; +import { Alert, Space } from "antd"; +import CustomMarkdown from "../../components/helpers/custom-markdown/CustomMarkdown.jsx"; import "./RjsfFormLayout.css"; function RjsfFormLayout({ @@ -29,59 +36,73 @@ function RjsfFormLayout({ validateAndSubmit, isStateUpdateRequired, }) { - schema.title = ""; - schema.description = ""; - const widgets = { - CheckboxWidget: CheckboxWidget, - DateWidget: DateWidget, - AltDateTimeWidget: AltDateTimeWidget, - AltDateWidget: AltDateWidget, - CheckboxesWidget: CheckboxesWidget, - ColorWidget: ColorWidget, - DateTimeWidget: DateTimeWidget, - EmailWidget: EmailWidget, - FileWidget: FileWidget, - HiddenWidget: HiddenWidget, - TimeWidget: TimeWidget, - URLWidget: URLWidget, - SelectWidget: SelectWidget, - }; + const formSchema = useMemo(() => { + if (!schema) return {}; + const rest = { ...schema }; + delete rest.title; + delete rest.description; + return rest; + }, [schema]); - const fields = { - ArrayField: ArrayField, - }; + const description = useMemo(() => schema?.description || "", [schema]); - const uiSchema = { - "ui:classNames": "my-rjsf-form", - mark_horizontal_lines: { - "ui:widget": !formData?.mark_vertical_lines ? "hidden" : undefined, - }, - }; + const widgets = useMemo( + () => ({ + AltDateTimeWidget, + AltDateWidget, + CheckboxWidget, + CheckboxesWidget, + ColorWidget, + DateTimeWidget, + DateWidget, + EmailWidget, + FileWidget, + HiddenWidget, + PasswordWidget, + SelectWidget, + TextWidget, + TimeWidget, + UpDownWidget, + URLWidget, + }), + [] + ); + + const fields = useMemo( + () => ({ + ArrayField, + }), + [] + ); + + const uiSchema = useMemo( + () => ({ + "ui:classNames": "my-rjsf-form", + mark_horizontal_lines: { + "ui:widget": !formData?.mark_vertical_lines ? "hidden" : undefined, + }, + }), + [formData] + ); - const removeBlankDefault = (schema) => { - /** - * We are removing the "required fields" default property if the value is null or "". - * We need this for applying the required field form valiation. - */ + const removeBlankDefault = useCallback((schema) => { if (schema?.properties && schema?.required) { - Object.keys(schema.properties).forEach((key) => { + const properties = schema.properties; + schema.required.forEach((key) => { if ( - schema.required.includes(key) && - (schema.properties[key].default === null || - schema.properties[key].default === "") + properties[key] && + (properties[key].default === null || properties[key].default === "") ) { - delete schema.properties[key].default; + delete properties[key].default; } }); } return schema; - }; + }, []); - // Change the error message for required fields. - const transformErrors = (errors) => { + const transformErrors = useCallback((errors) => { return errors.map((error) => { if (error.name === "required") { - // Change the error message for the "required" validation return { ...error, message: "This field is mandatory. Please provide a value.", @@ -89,41 +110,51 @@ function RjsfFormLayout({ } return error; }); - }; + }, []); - // If required, the `formData` state can be dynamically updated to store the latest user input as they interact with the form. - const handleChange = (event) => { - if (!isStateUpdateRequired) { - return; - } - const data = event.formData; - setFormData(data); - }; + const handleChange = useCallback( + (event) => { + if (!isStateUpdateRequired) { + return; + } + const data = event.formData; + setFormData(data); + }, + [isStateUpdateRequired, setFormData] + ); return ( <> {isLoading ? ( ) : ( -
{}} - onSubmit={(e) => validateAndSubmit(e.formData)} - formContext={{ - descriptionLocation: "tooltip", - }} - showErrorList={false} - onChange={handleChange} - > - {children} -
+ + {description && ( + } + type="info" + /> + )} +
{}} + onSubmit={(e) => validateAndSubmit(e.formData)} + showErrorList={false} + onChange={handleChange} + templates={{ + FieldTemplate: CustomFieldTemplate, + }} + > + {children} +
+
)} ); diff --git a/frontend/src/layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx b/frontend/src/layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx index f33b1eb6d..6a6991fbd 100644 --- a/frontend/src/layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx +++ b/frontend/src/layouts/rjsf-widget-layout/RjsfWidgetLayout.jsx @@ -1,7 +1,7 @@ -import { QuestionCircleOutlined } from "@ant-design/icons"; -import { Form, Tooltip, Typography } from "antd"; +import { Form, Typography } from "antd"; import PropTypes from "prop-types"; import "./RjsfWidgetLayout.css"; +import CustomMarkdown from "../../components/helpers/custom-markdown/CustomMarkdown"; function RjsfWidgetLayout({ children, label, description, required }) { return ( @@ -9,13 +9,15 @@ function RjsfWidgetLayout({ children, label, description, required }) { {required && * } {label} - {description?.length > 0 && ( - - - - )} {children} + {description?.length > 0 && ( + + )}
); }