detailGrid 탭 추가
This commit is contained in:
@@ -1,526 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getOptionsArray } from '../../../utils';
|
||||
import {
|
||||
SelectInput,
|
||||
SearchBarAlert
|
||||
} from '../../../styles/Components';
|
||||
import {
|
||||
FormInput, FormInputSuffix, FormInputSuffixWrapper,
|
||||
FormLabel,
|
||||
FormRowGroup,
|
||||
FormStatusBar,
|
||||
FormStatusLabel,
|
||||
FormStatusWarning,
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { CheckBox, SingleDatePicker, SingleTimePicker } from '../../common';
|
||||
import Button from '../../common/button/Button';
|
||||
import styled from 'styled-components';
|
||||
import ImageUploadBtn from '../../ServiceManage/ImageUploadBtn';
|
||||
|
||||
const CaliForm = ({
|
||||
config, // 폼 설정 JSON
|
||||
mode, // 'create', 'update', 'view' 중 하나
|
||||
initialData, // 초기 데이터
|
||||
externalData, // 외부 데이터(옵션 등)
|
||||
onSubmit, // 제출 핸들러
|
||||
onCancel, // 취소 핸들러
|
||||
className, // 추가 CSS 클래스
|
||||
onFieldValidation, // 필드 유효성 검사 콜백
|
||||
formRef // 폼 ref
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState({ ...(config?.initData || {}), ...(initialData || {}) });
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
|
||||
// 필드 변경 핸들러
|
||||
const handleFieldChange = (fieldId, value) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[fieldId]: value
|
||||
}));
|
||||
};
|
||||
|
||||
// 날짜 변경 핸들러
|
||||
const handleDateChange = (fieldId, date) => {
|
||||
if (!date) return;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[fieldId]: date
|
||||
}));
|
||||
};
|
||||
|
||||
// 시간 변경 핸들러
|
||||
const handleTimeChange = (fieldId, time) => {
|
||||
if (!time) return;
|
||||
|
||||
const newDateTime = formData[fieldId] ? new Date(formData[fieldId]) : new Date();
|
||||
newDateTime.setHours(time.getHours(), time.getMinutes(), 0, 0);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[fieldId]: newDateTime
|
||||
}));
|
||||
};
|
||||
|
||||
// 폼 유효성 검사
|
||||
useEffect(() => {
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
let isValid = true;
|
||||
|
||||
if (!config) return false;
|
||||
|
||||
// 필수 필드 검사
|
||||
const requiredFields = config.fields
|
||||
.filter(f =>
|
||||
f.visibleOn.includes(mode) &&
|
||||
f.validations?.includes("required")
|
||||
)
|
||||
.map(f => f.id);
|
||||
|
||||
requiredFields.forEach(fieldId => {
|
||||
if (!formData[fieldId] && formData[fieldId] !== 0) {
|
||||
newErrors[fieldId] = t('REQUIRED_FIELD');
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 조건부 유효성 검사
|
||||
if (config.validations && config.validations[mode]) {
|
||||
for (const validation of config.validations[mode]) {
|
||||
const conditionResult = evaluateCondition(validation.condition, {
|
||||
...formData,
|
||||
current_time: new Date().getTime()
|
||||
});
|
||||
|
||||
if (conditionResult) {
|
||||
// 전체 폼 검증 오류
|
||||
newErrors._form = t(validation.message);
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
setIsFormValid(isValid);
|
||||
|
||||
if (onFieldValidation) {
|
||||
onFieldValidation(isValid, newErrors);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
validateForm();
|
||||
}, [config, formData, mode, t, onFieldValidation]);
|
||||
|
||||
// 간단한 조건식 평가 함수
|
||||
const evaluateCondition = (conditionStr, context) => {
|
||||
try {
|
||||
const fn = new Function(...Object.keys(context), `return ${conditionStr}`);
|
||||
return fn(...Object.values(context));
|
||||
} catch (e) {
|
||||
console.error('Error evaluating condition:', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 필드 렌더링
|
||||
const renderField = (field) => {
|
||||
const isEditable = field.editableOn.includes(mode);
|
||||
const value = formData[field.id] !== undefined ? formData[field.id] : '';
|
||||
const hasError = errors[field.id];
|
||||
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<div className="form-field">
|
||||
<FormInput
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => handleFieldChange(field.id, e.target.value)}
|
||||
disabled={!isEditable}
|
||||
width={field.width}
|
||||
className={hasError ? 'error' : ''}
|
||||
/>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<div className="form-field">
|
||||
<FormInput
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={e => handleFieldChange(field.id, Number(e.target.value))}
|
||||
disabled={!isEditable}
|
||||
width={field.width}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step || 1}
|
||||
className={hasError ? 'error' : ''}
|
||||
/>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
let options = [];
|
||||
|
||||
if (field.optionsKey) {
|
||||
// 옵션 설정에서 가져오기
|
||||
options = getOptionsArray(field.optionsKey);
|
||||
} else if (field.dataSource && externalData) {
|
||||
// 외부 데이터 소스 사용
|
||||
const dataSource = externalData[field.dataSource] || [];
|
||||
|
||||
options = dataSource.map(item => ({
|
||||
value: item[field.valueField],
|
||||
label: field.displayFormat
|
||||
? field.displayFormat.replace('{value}', item[field.valueField])
|
||||
.replace('{display}', item[field.displayField])
|
||||
: `${item[field.displayField]}(${item[field.valueField]})`
|
||||
}));
|
||||
} else if (field.options) {
|
||||
options = field.options;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-field">
|
||||
<SelectInput
|
||||
value={value}
|
||||
onChange={e => handleFieldChange(field.id, e.target.value)}
|
||||
disabled={!isEditable}
|
||||
width={field.width}
|
||||
className={hasError ? 'error' : ''}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<option key={index} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'datePicker':
|
||||
return (
|
||||
<div className="form-field">
|
||||
<SingleDatePicker
|
||||
label={field.label}
|
||||
disabled={!isEditable}
|
||||
dateLabel={field.dateLabel}
|
||||
onDateChange={date => handleDateChange(field.id, date)}
|
||||
selectedDate={value}
|
||||
minDate={field.minDate}
|
||||
maxDate={field.maxDate}
|
||||
className={hasError ? 'error' : ''}
|
||||
/>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'timePicker':
|
||||
return (
|
||||
<div className="form-field">
|
||||
<SingleTimePicker
|
||||
label={field.label}
|
||||
disabled={!isEditable}
|
||||
selectedTime={value}
|
||||
onTimeChange={time => handleTimeChange(field.id, time)}
|
||||
className={hasError ? 'error' : ''}
|
||||
/>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'status':
|
||||
let statusText = "";
|
||||
if (field.optionsKey && formData[field.statusField]) {
|
||||
const statusOptions = getOptionsArray(field.optionsKey);
|
||||
const statusItem = statusOptions.find(item => item.value === formData[field.statusField]);
|
||||
statusText = statusItem ? statusItem.name : "등록";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-field">
|
||||
<FormStatusBar>
|
||||
<FormStatusLabel>
|
||||
{field.label}: {statusText}
|
||||
</FormStatusLabel>
|
||||
{mode === 'update' && field.warningMessage && (
|
||||
<FormStatusWarning>
|
||||
{t(field.warningMessage)}
|
||||
</FormStatusWarning>
|
||||
)}
|
||||
</FormStatusBar>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'dateTimeRange':
|
||||
return (
|
||||
<div className="form-field">
|
||||
<div className="date-time-range">
|
||||
<SingleDatePicker
|
||||
label={field.startDateLabel}
|
||||
disabled={!isEditable}
|
||||
dateLabel={field.startDateLabel}
|
||||
onDateChange={date => handleDateChange(field.startDateField, date)}
|
||||
selectedDate={formData[field.startDateField]}
|
||||
/>
|
||||
<SingleTimePicker
|
||||
disabled={!isEditable}
|
||||
selectedTime={formData[field.startDateField]}
|
||||
onTimeChange={time => handleTimeChange(field.startDateField, time)}
|
||||
/>
|
||||
<SingleDatePicker
|
||||
label={field.endDateLabel}
|
||||
disabled={!isEditable}
|
||||
dateLabel={field.endDateLabel}
|
||||
onDateChange={date => handleDateChange(field.endDateField, date)}
|
||||
selectedDate={formData[field.endDateField]}
|
||||
/>
|
||||
<SingleTimePicker
|
||||
disabled={!isEditable}
|
||||
selectedTime={formData[field.endDateField]}
|
||||
onTimeChange={time => handleTimeChange(field.endDateField, time)}
|
||||
/>
|
||||
</div>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'imageUpload':
|
||||
const imageLanguage = field.language;
|
||||
const imageList = formData.image_list || [];
|
||||
const imageData = imageList.find(img => img.language === imageLanguage) || { content: '' };
|
||||
|
||||
return (
|
||||
<div className="form-field">
|
||||
<LanguageWrapper>
|
||||
<LanguageLabel>{imageLanguage}</LanguageLabel>
|
||||
<ImageUploadBtn
|
||||
onImageUpload={(file, fileName) => {
|
||||
const updatedImageList = [...imageList];
|
||||
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
|
||||
|
||||
if (index !== -1) {
|
||||
updatedImageList[index] = {
|
||||
...updatedImageList[index],
|
||||
content: fileName
|
||||
};
|
||||
} else {
|
||||
updatedImageList.push({
|
||||
language: imageLanguage,
|
||||
content: fileName
|
||||
});
|
||||
}
|
||||
|
||||
handleFieldChange('image_list', updatedImageList);
|
||||
}}
|
||||
onFileDelete={() => {
|
||||
const updatedImageList = [...imageList];
|
||||
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
|
||||
|
||||
if (index !== -1) {
|
||||
updatedImageList[index] = {
|
||||
...updatedImageList[index],
|
||||
content: ''
|
||||
};
|
||||
|
||||
handleFieldChange('image_list', updatedImageList);
|
||||
}
|
||||
}}
|
||||
fileName={imageData.content}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</LanguageWrapper>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<div className="form-field">
|
||||
<CheckBox
|
||||
label={field.label}
|
||||
id={field.id}
|
||||
checked={formData[field.id] || false}
|
||||
setData={e => handleFieldChange(field.id, e.target.checked)}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'textWithSuffix':
|
||||
const linkLanguage = field.suffix;
|
||||
const linkList = formData.link_list || [];
|
||||
const linkData = linkList.find(link => link.language === linkLanguage) || { content: '' };
|
||||
|
||||
return (
|
||||
<div className="form-field">
|
||||
{field.label && <FormLabel>{field.label}</FormLabel>}
|
||||
<FormInputSuffixWrapper>
|
||||
<FormInput
|
||||
type="text"
|
||||
value={linkData.content}
|
||||
onChange={e => {
|
||||
const updatedLinkList = [...linkList];
|
||||
const index = updatedLinkList.findIndex(link => link.language === linkLanguage);
|
||||
|
||||
if (index !== -1) {
|
||||
updatedLinkList[index] = {
|
||||
...updatedLinkList[index],
|
||||
content: e.target.value
|
||||
};
|
||||
} else {
|
||||
updatedLinkList.push({
|
||||
language: linkLanguage,
|
||||
content: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleFieldChange('link_list', updatedLinkList);
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
width={field.width}
|
||||
suffix="true"
|
||||
/>
|
||||
<FormInputSuffix>{linkLanguage}</FormInputSuffix>
|
||||
</FormInputSuffixWrapper>
|
||||
{hasError && <div className="field-error">{hasError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 조건부 렌더링을 위한 필드 필터링
|
||||
const getVisibleFields = () => {
|
||||
if (!config) return [];
|
||||
|
||||
return config.fields.filter(field => {
|
||||
if (!field.visibleOn.includes(mode)) return false;
|
||||
|
||||
// 조건부 표시 필드 처리
|
||||
if (field.conditional) {
|
||||
const { field: condField, operator, value } = field.conditional;
|
||||
|
||||
if (operator === "==" && formData[condField] !== value) return false;
|
||||
if (operator === "!=" && formData[condField] === value) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// 그리드 기반 필드 렌더링
|
||||
const renderGridFields = () => {
|
||||
if (!config) return null;
|
||||
|
||||
const visibleFields = getVisibleFields();
|
||||
const { rows, columns } = config.grid;
|
||||
|
||||
// 그리드 레이아웃 생성
|
||||
return (
|
||||
<div className="form-grid" style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: '10px' }}>
|
||||
{visibleFields.map((field) => {
|
||||
const { row, col, width } = field.position;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className="form-cell"
|
||||
style={{
|
||||
gridRow: row + 1,
|
||||
gridColumn: `${col + 1} / span ${width}`,
|
||||
padding: '5px'
|
||||
}}
|
||||
>
|
||||
<FormRowGroup>
|
||||
<FormLabel>{field.label}{field.validations?.includes("required") && <span className="required">*</span>}</FormLabel>
|
||||
{renderField(field)}
|
||||
</FormRowGroup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 버튼 렌더링
|
||||
const renderButtons = () => {
|
||||
if (!config || !config.actions || !config.actions[mode]) return null;
|
||||
|
||||
return (
|
||||
<div className="form-actions">
|
||||
{config.actions[mode].map(action => (
|
||||
<Button
|
||||
key={action.id}
|
||||
text={action.label}
|
||||
theme={action.theme}
|
||||
handleClick={() => {
|
||||
if (action.action === 'submit') {
|
||||
if (isFormValid) {
|
||||
onSubmit(formData);
|
||||
}
|
||||
} else if (action.action === 'close' || action.action === 'cancel') {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
disabled={action.action === 'submit' && !isFormValid}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!config) return <div>로딩 중...</div>;
|
||||
|
||||
return (
|
||||
<div className={`json-config-form ${className || ''}`} ref={formRef}>
|
||||
<div className="form-content">
|
||||
{renderGridFields()}
|
||||
|
||||
{errors._form && (
|
||||
<SearchBarAlert $marginTop="15px" $align="right">
|
||||
{errors._form}
|
||||
</SearchBarAlert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-footer">
|
||||
{renderButtons()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaliForm;
|
||||
|
||||
const LanguageWrapper = styled.div`
|
||||
width: ${props => props.width || '100%'};
|
||||
//margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 90px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const LanguageLabel = styled.h4`
|
||||
color: #444;
|
||||
margin: 0 0 10px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import dayjs from 'dayjs';
|
||||
import { AnimatedTabs } from '../index';
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
/**
|
||||
@@ -52,7 +53,10 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
|
||||
max,
|
||||
format,
|
||||
required,
|
||||
showTime
|
||||
showTime,
|
||||
tabItems,
|
||||
activeKey,
|
||||
onTabChange
|
||||
} = item;
|
||||
|
||||
// 현재 값 가져오기 (formData에서 또는 항목에서)
|
||||
@@ -105,6 +109,7 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
|
||||
return (
|
||||
<DatePicker
|
||||
{...commonProps}
|
||||
showTime={showTime || false}
|
||||
value={currentValue ? dayjs(currentValue) : null}
|
||||
format={format || 'YYYY-MM-DD'}
|
||||
onChange={(date) => onChange(key, date, handler)}
|
||||
@@ -191,8 +196,8 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
|
||||
case 'tab':
|
||||
return <AnimatedTabs
|
||||
items={tabItems}
|
||||
activeKey={activeLanguage}
|
||||
onChange={handleTabChange}
|
||||
activeKey={activeKey}
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
|
||||
case 'custom':
|
||||
@@ -249,13 +254,15 @@ const StatusDisplay = ({ status }) => {
|
||||
let color = '';
|
||||
let text = '';
|
||||
|
||||
switch (status) {
|
||||
const lowerStatus = typeof status === 'string' ? status.toLowerCase() : status;
|
||||
|
||||
switch (lowerStatus) {
|
||||
case 'wait':
|
||||
color = '#faad14';
|
||||
color = '#FAAD14';
|
||||
text = '대기';
|
||||
break;
|
||||
case 'running':
|
||||
color = '#52c41a';
|
||||
color = '#4287f5';
|
||||
text = '진행중';
|
||||
break;
|
||||
case 'finish':
|
||||
@@ -271,7 +278,7 @@ const StatusDisplay = ({ status }) => {
|
||||
text = '삭제';
|
||||
break;
|
||||
default:
|
||||
color = '#1890ff';
|
||||
color = '#DEBB46';
|
||||
text = status;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,38 +5,72 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// 통합된 애니메이션 탭 컴포넌트
|
||||
const AnimatedTabs = ({ items, activeKey, onChange }) => {
|
||||
// 각 항목의 children을 애니메이션 래퍼로 감싸기
|
||||
const tabItems = items.map(item => ({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
children: (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeKey}
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
{item.children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}));
|
||||
|
||||
return (
|
||||
<StyledTabs
|
||||
activeKey={activeKey}
|
||||
onChange={onChange}
|
||||
centered={true}
|
||||
>
|
||||
{items.map(item => (
|
||||
<Tabs.TabPane
|
||||
tab={item.label}
|
||||
key={item.key}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeKey}
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
{item.children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</StyledTabs>
|
||||
items={tabItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// const AnimatedTabs = ({ items, activeKey, onChange }) => {
|
||||
// return (
|
||||
// <StyledTabs
|
||||
// activeKey={activeKey}
|
||||
// onChange={onChange}
|
||||
// centered={true}
|
||||
// >
|
||||
// {items.map(item => (
|
||||
// <Tabs.TabPane
|
||||
// tab={item.label}
|
||||
// key={item.key}
|
||||
// >
|
||||
// <AnimatePresence mode="wait">
|
||||
// <motion.div
|
||||
// key={activeKey}
|
||||
// initial={{ opacity: 0, x: 50 }}
|
||||
// animate={{ opacity: 1, x: 0 }}
|
||||
// exit={{ opacity: 0, x: -50 }}
|
||||
// transition={{
|
||||
// type: "spring",
|
||||
// stiffness: 300,
|
||||
// damping: 30
|
||||
// }}
|
||||
// >
|
||||
// {item.children}
|
||||
// </motion.div>
|
||||
// </AnimatePresence>
|
||||
// </Tabs.TabPane>
|
||||
// ))}
|
||||
// </StyledTabs>
|
||||
// );
|
||||
// };
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -21,6 +21,7 @@ import CDivider from './CDivider';
|
||||
import TopButton from './button/TopButton';
|
||||
import AntButton from './button/AntButton';
|
||||
import DetailLayout from './Layout/DetailLayout';
|
||||
import AnimatedTabs from './control/AnimatedTabs';
|
||||
|
||||
import CaliTable from './Custom/CaliTable'
|
||||
|
||||
@@ -56,5 +57,6 @@ export { DateTimeInput,
|
||||
FrontPagination,
|
||||
DownloadProgress,
|
||||
CaliTable,
|
||||
DetailLayout
|
||||
DetailLayout,
|
||||
AnimatedTabs
|
||||
};
|
||||
Reference in New Issue
Block a user