@ant-design/icons#PlusCircleFilled TypeScript Examples
The following examples show how to use
@ant-design/icons#PlusCircleFilled.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: DesktopCustom.spec.tsx From next-basics with GNU General Public License v3.0 | 6 votes |
describe("DesktopCustom", () => {
it("should work", () => {
const stopPropagation = jest.fn();
const preventDefault = jest.fn();
const handleClick = jest.fn();
const handleAddClick = jest.fn();
const name = "世界";
const url = "/hello";
const wrapper = shallow(
<DesktopCustom
name={name}
url={url}
onAddClick={handleAddClick}
onClick={handleClick}
showAddIcon={true}
isFavorite={true}
/>
);
expect(wrapper.find("img").prop("src")).toBe("png-url");
expect(wrapper.find(PlusCircleFilled).exists()).toBe(true);
wrapper.find(PlusCircleFilled).invoke("onClick")({
stopPropagation,
preventDefault,
} as any);
expect(handleAddClick).toBeCalled();
expect(stopPropagation).toBeCalled();
expect(preventDefault).toBeCalled();
wrapper.find(Link).invoke("onClick")({
stopPropagation,
} as any);
expect(handleClick).toBeCalled();
expect(stopPropagation).toHaveBeenCalledTimes(2);
expect(preventDefault).toHaveBeenCalledTimes(1);
});
});
Example #2
Source File: DesktopApp.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function DesktopApp({
app,
onAddClick,
onClick,
isFavorite,
showAddIcon,
size,
}: DesktopAppProps): React.ReactElement {
const installing = app.installStatus === "running";
const handleAppClick = (e: React.MouseEvent): void => {
e.stopPropagation();
if (installing) {
e.preventDefault();
}
onClick?.();
getRuntime().resetWorkspaceStack();
};
const handleAddIconClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
onAddClick?.();
};
return (
<>
<Link
className={classNames(
styles.appLink,
styles[size],
app.iconBackground === "circle" ? styles.circle : styles.square,
{
[styles.installing]: installing,
}
)}
to={app.homepage}
onClick={handleAppClick}
>
<img
className={styles.appIcon}
src={
app.icons && app.icons.large
? /^(\/|https?:\/\/)/.test(app.icons.large)
? app.icons.large
: `micro-apps/${app.id}/${app.icons.large}`
: defaultAppIcon
}
/>
{showAddIcon && isFavorite && (
<PlusCircleFilled
className={classNames(
styles.addIcon,
app.iconBackground === "circle"
? styles.circleIcon
: styles.squareIcon
)}
onClick={handleAddIconClick}
/>
)}
</Link>
<span className={styles.appName}>
{installing && (
<Loading3QuartersOutlined spin style={{ paddingRight: 5 }} />
)}
{app.localeName}
</span>
</>
);
}
Example #3
Source File: DesktopCustom.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function DesktopCustom({
name,
url,
showAddIcon,
isFavorite,
onClick,
onAddClick,
size,
responsive = true,
}: DesktopCustomProps): React.ReactElement {
const handleItemClick = (e: React.MouseEvent): void => {
e.stopPropagation();
onClick?.();
};
const handleAddIconClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
onAddClick?.();
};
return (
<>
<Link
className={classNames(styles.appLink, styles.circle, styles[size], {
[styles.responsive]: responsive,
})}
href={url}
target="_blank"
onClick={handleItemClick}
>
<img className={styles.appIcon} src={defaultAppIcon} />
{showAddIcon && isFavorite && (
<PlusCircleFilled
className={classNames(styles.addIcon, styles.circleIcon)}
onClick={handleAddIconClick}
/>
)}
</Link>
<span className={styles.appName}>{name}</span>
</>
);
}
Example #4
Source File: CreateMeterReadingsActionBar.tsx From condo with MIT License | 5 votes |
CreateMeterReadingsActionBar = ({
handleSave,
handleAddMeterButtonClick,
isLoading,
newMeterReadings,
}) => {
const intl = useIntl()
const SendMetersReadingMessage = intl.formatMessage({ id: 'pages.condo.meter.SendMetersReading' })
const AddMeterMessage = intl.formatMessage({ id: 'pages.condo.meter.AddMeter' })
return (
<Form.Item
noStyle
dependencies={PROPERTY_DEPENDENCY}
shouldUpdate={handleShouldUpdate}
>
{
({ getFieldsValue }) => {
const { property, unitName } = getFieldsValue(['property', 'unitName'])
const isSubmitButtonDisabled = !property || !unitName || isEmpty(newMeterReadings)
const isCreateMeterButtonDisabled = !property || !unitName
return (
<ActionBar>
<Space size={12}>
<Button
key='submit'
onClick={handleSave}
type='sberDefaultGradient'
loading={isLoading}
disabled={isSubmitButtonDisabled}
>
{SendMetersReadingMessage}
</Button>
<Button
onClick={handleAddMeterButtonClick}
type='sberDefaultGradient'
disabled={isCreateMeterButtonDisabled}
icon={<PlusCircleFilled/>}
secondary
>
{AddMeterMessage}
</Button>
<ErrorsContainer
property={property}
unitName={unitName}
/>
</Space>
</ActionBar>
)
}
}
</Form.Item>
)
}
Example #5
Source File: index.tsx From dashboard with Apache License 2.0 | 5 votes |
ImageUploader: React.FC<ImageUploaderProps> = (props) => {
const {value, onChange} = props;
const [loading, setLoading] = useState<boolean>(false);
return (
<Upload
accept={'.jpg,.png,.jpeg'}
name='avatar'
listType='picture-card'
className={styles.imageUploader}
showUploadList={false}
beforeUpload={(file) => {
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
message.error('只能上传jpg和png格式');
return false;
}
if (file.size / 1024 / 1024 > 20) {
message.error('图片最大20M');
return false;
}
return true;
}}
onChange={(info) => {
if (info.file.status === 'uploading') {
setLoading(true);
return;
}
if (info.file.status === 'done') {
setLoading(false);
}
}}
{...(_.omit(props, ['value']))}
>
<div>
{value && (
<Badge
count={
<CloseCircleFilled
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (onChange) {
onChange('');
}
setLoading(false);
}}
style={{color: 'rgb(199,199,199)'}}
/>
}
>
<Image
preview={false}
className={styles.image}
src={value}
fallback={defaultImage}
/>
</Badge>
)}
{!value && (
<div className={styles.button}>
{loading ? <LoadingOutlined/> : <PlusCircleFilled/>}
<div className={styles.text}>上传图片</div>
</div>
)}
</div>
</Upload>
);
}
Example #6
Source File: index.tsx From dashboard with Apache License 2.0 | 5 votes |
fileMap = {
'formImage': {
accept: '.jpg,.png',
contentType: ['image/png', 'image/jpg'],
limitSize: 2,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
existValueRender: (value: any, fileInfo: any, fileType: string) => (
<Image
preview={false}
className={styles.image}
src={value}
fallback={defaultImage}
/>
),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
noValueRender: (loading: boolean, fileType: string) => (
<div className={styles.formImageButton}>
{loading ? <LoadingOutlined/> : <PlusCircleFilled/>}
<div className={styles.text}>上传图片</div>
</div>
),
},
'海报': {
accept: '.jpg,.png',
contentType: ['image/png', 'image/jpg'],
limitSize: 2,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
existValueRender: (value: any, fileInfo: any, fileType: string) => (
<Image
preview={false}
className={styles.image}
src={value}
fallback={defaultImage}
/>
),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType)
},
'视频': {
accept: '.mp4',
contentType: ['video/mp4'],
limitSize: 10,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
existValueRender: (value: any, fileInfo: any, fileType: string) => (
<video src={value} style={{width: 260}} controls={true}></video>
),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType),
},
'PDF': {
accept: '.pdf',
contentType: ['application/pdf'],
limitSize: 20,
image: pdfImage,
existValueRender: (value: any, fileInfo: any, fileType: string) => commonExistValueRender(value, fileInfo, fileType),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType),
},
'PPT': {
accept: '.pptx,.ppt',
contentType: ['application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
limitSize: 20,
image: pptImage,
existValueRender: (value: any, fileInfo: any, fileType: string) => commonExistValueRender(value, fileInfo, fileType),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType),
},
'表格': {
accept: '.xls,.xlsx',
contentType: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
limitSize: 20,
image: excelImage,
existValueRender: (value: any, fileInfo: any, fileType: string) => commonExistValueRender(value, fileInfo, fileType),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType),
},
'文档': {
accept: '.doc,.docx',
contentType: ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
limitSize: 20,
image: wordImage,
existValueRender: (value: any, fileInfo: any, fileType: string) => commonExistValueRender(value, fileInfo, fileType),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType),
}
}
Example #7
Source File: index.tsx From dashboard with Apache License 2.0 | 5 votes |
fileMap = {
'formImage': {
accept: '.jpg,.png',
contentType: [ 'image/png', 'image/jpg'],
limitSize: 2,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
existValueRender: (value: any, fileInfo: any, fileType: string) => (
<Image
preview={false}
className={styles.image}
src={value}
fallback={defaultImage}
/>
),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
noValueRender: (loading: boolean, fileType: string) => (
<div className={styles.formImageButton}>
{loading ? <LoadingOutlined/> : <PlusCircleFilled/>}
<div className={styles.text}>上传图片</div>
</div>
),
},
'视频': {
accept: '.mp4',
contentType: ['video/mp4'],
limitSize: 10,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
existValueRender: (value: any, fileInfo: any, fileType: string) => (
<video src={value} style={{width: 260}} controls={true}></video>
),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType),
},
'PDF': {
accept: '.pdf',
contentType: ['application/pdf'],
limitSize: 20,
image: pdfImage,
existValueRender: (value: any, fileInfo: any, fileType: string) => commonExistValueRender(value, fileInfo, fileType),
noValueRender: (loading: boolean, fileType: string) => commonNoValueRender(loading, fileType),
},
}
Example #8
Source File: DesktopApp.spec.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
describe("DesktopApp", () => {
it("should work", () => {
const stopPropagation = jest.fn();
const preventDefault = jest.fn();
const handleClick = jest.fn();
const handleAddClick = jest.fn();
const app: MicroApp = {
id: "hello",
name: "世界",
localeName: "world",
homepage: "/hello",
iconBackground: "circle",
};
const wrapper = shallow(
<DesktopApp
app={app}
onAddClick={handleAddClick}
onClick={handleClick}
showAddIcon={true}
isFavorite={true}
/>
);
expect(wrapper.find("img").prop("src")).toBe("png-url");
wrapper.find(Link).invoke("onClick")({
stopPropagation,
preventDefault,
} as any);
expect(stopPropagation).toBeCalled();
expect(preventDefault).not.toBeCalled();
expect(resetWorkspaceStack).toBeCalled();
expect(handleClick).toBeCalled();
expect(wrapper.find(PlusCircleFilled).exists()).toBe(true);
wrapper.find(PlusCircleFilled).invoke("onClick")({
stopPropagation,
preventDefault,
} as any);
expect(handleAddClick).toBeCalled();
wrapper.setProps({
app: {
...app,
installStatus: "running",
},
});
wrapper.find(Link).invoke("onClick")({
stopPropagation,
preventDefault,
} as any);
expect(stopPropagation).toHaveBeenCalledTimes(3);
expect(preventDefault).toHaveBeenCalledTimes(2);
expect(wrapper.find(Loading3QuartersOutlined)).toHaveLength(1);
});
it("should render icon", () => {
const app: MicroApp = {
id: "hello",
name: "世界",
localeName: "world",
homepage: "/hello",
icons: {
large: "icons/large.png",
},
};
const wrapper = shallow(<DesktopApp app={app} />);
expect(wrapper.find("img").prop("src")).toBe(
`micro-apps/hello/icons/large.png`
);
wrapper.setProps({
showAddIcon: true,
isFavorite: true,
app: {
...app,
icons: {
large: "http://a.com/b.jpg",
},
iconBackground: "square",
},
});
expect(wrapper.find("img").prop("src")).toBe("http://a.com/b.jpg");
expect(wrapper.find(".addIcon").hasClass("squareIcon")).toEqual(true);
});
});
Example #9
Source File: index.tsx From condo with MIT License | 4 votes |
TicketAnalyticsPage: ITicketAnalyticsPage = () => {
const intl = useIntl()
const PageTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.PageTitle' })
const HeaderButtonTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.HeaderButtonTitle' })
const ViewModeTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.ViewModeTitle' })
const StatusFilterLabel = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.groupByFilter.Status' })
const PropertyFilterLabel = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.groupByFilter.Property' })
const CategoryFilterLabel = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.groupByFilter.Category' })
const UserFilterLabel = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.groupByFilter.User' })
const ResponsibleFilterLabel = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.groupByFilter.Responsible' })
const AllAddresses = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.AllAddresses' })
const ManyAddresses = intl.formatMessage({ id:'pages.condo.analytics.TicketAnalyticsPage.ManyAddresses' })
const AllAddressTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.tableColumns.AllAddresses' })
const SingleAddress = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.SingleAddress' })
const AllCategories = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.AllCategories' })
const AllCategoryClassifiersTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.tableColumns.AllClassifiers' })
const AllExecutorsTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.tableColumns.AllExecutors' })
const AllAssigneesTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.tableColumns.AllAssignees' })
const EmptyCategoryClassifierTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.NullReplaces.CategoryClassifier' })
const EmptyExecutorTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.NullReplaces.Executor' })
const EmptyAssigneeTitle = intl.formatMessage({ id: 'pages.condo.analytics.TicketAnalyticsPage.NullReplaces.Assignee' })
const TableTitle = intl.formatMessage({ id: 'Table' })
const NotImplementedYetMessage = intl.formatMessage({ id: 'NotImplementedYet' })
const PrintTitle = intl.formatMessage({ id: 'Print' })
const ExcelTitle = intl.formatMessage({ id: 'Excel' })
const router = useRouter()
const userOrganization = useOrganization()
const userOrganizationId = get(userOrganization, ['organization', 'id'])
const { isSmall } = useLayoutContext()
const filtersRef = useRef<null | ticketAnalyticsPageFilters>(null)
const mapperInstanceRef = useRef(null)
const ticketLabelsRef = useRef<TicketLabel[]>([])
const [groupTicketsBy, setGroupTicketsBy] = useState<GroupTicketsByTypes>('status')
const [viewMode, setViewMode] = useState<ViewModeTypes>('line')
const [analyticsData, setAnalyticsData] = useState<null | TicketGroupedCounter[]>(null)
const [excelDownloadLink, setExcelDownloadLink] = useState<null | string>(null)
const [ticketType, setTicketType] = useState<TicketSelectTypes>('all')
const [dateFrom, dateTo] = filtersRef.current !== null ? filtersRef.current.range : []
const selectedPeriod = filtersRef.current !== null ? filtersRef.current.range.map(e => e.format(DATE_DISPLAY_FORMAT)).join(' - ') : ''
const selectedAddresses = filtersRef.current !== null ? filtersRef.current.addressList : []
const ticketTypeRef = useRef<TicketSelectTypes>('all')
const { TicketWarningModal, setIsVisible } = useTicketWarningModal(groupTicketsBy)
const nullReplaces = {
categoryClassifier: EmptyCategoryClassifierTitle,
executor: EmptyExecutorTitle,
assignee: EmptyAssigneeTitle,
}
const [loadTicketAnalytics, { loading }] = useLazyQuery(TICKET_ANALYTICS_REPORT_QUERY, {
onError: error => {
console.log(error)
notification.error(error)
},
fetchPolicy: 'network-only',
onCompleted: response => {
const { result: { groups, ticketLabels } } = response
ticketLabelsRef.current = ticketLabels
setAnalyticsData(groups)
},
})
const [exportTicketAnalyticsToExcel, { loading: isXSLXLoading }] = useLazyQuery(EXPORT_TICKET_ANALYTICS_TO_EXCEL, {
onError: error => {
console.log(error)
notification.error(error)
},
fetchPolicy: 'network-only',
onCompleted: response => {
const { result: { link } } = response
setExcelDownloadLink(link)
},
})
const getAnalyticsData = useCallback(() => {
if (filtersRef.current !== null) {
mapperInstanceRef.current = new TicketChart({
line: {
chart: (viewMode, ticketGroupedCounter) => {
const { groupBy } = filterToQuery(
{ filter: filtersRef.current, viewMode, ticketType, mainGroup: groupTicketsBy }
)
const data = getAggregatedData(ticketGroupedCounter, groupBy)
const axisLabels = Array.from(new Set(Object.values(data).flatMap(e => Object.keys(e))))
const legend = Object.keys(data)
const series = []
Object.entries(data).map(([groupBy, dataObj]) => {
series.push({
name: groupBy,
type: viewMode,
symbol: 'none',
stack: groupBy,
data: Object.values(dataObj),
emphasis: {
focus: 'none',
blurScope: 'none',
},
})
})
const axisData = { yAxis: { type: 'value', data: null }, xAxis: { type: 'category', data: axisLabels } }
const tooltip = { trigger: 'axis', axisPointer: { type: 'line' } }
const result = { series, legend, axisData, tooltip }
if (groupBy[0] === 'status') {
result['color'] = ticketLabelsRef.current.map(({ color }) => color)
}
return result
},
table: (viewMode, ticketGroupedCounter, restOptions) => {
const { groupBy } = filterToQuery(
{ filter: filtersRef.current, viewMode, ticketType, mainGroup: groupTicketsBy }
)
const data = getAggregatedData(ticketGroupedCounter, groupBy)
const dataSource = []
const { translations, filters } = restOptions
const tableColumns: TableColumnsType = [
{ title: translations['address'], dataIndex: 'address', key: 'address', sorter: (a, b) => a['address'] - b['address'] },
{
title: translations['date'],
dataIndex: 'date',
key: 'date',
defaultSortOrder: 'descend',
sorter: (a, b) => dayjs(a['date'], DATE_DISPLAY_FORMAT).unix() - dayjs(b['date'], DATE_DISPLAY_FORMAT).unix(),
},
...Object.entries(data).map(([key]) => ({ title: key, dataIndex: key, key, sorter: (a, b) =>a[key] - b[key] })),
]
const uniqueDates = Array.from(new Set(Object.values(data).flatMap(e => Object.keys(e))))
uniqueDates.forEach((date, key) => {
const restTableColumns = {}
Object.keys(data).forEach(ticketType => (restTableColumns[ticketType] = data[ticketType][date]))
let address = translations['allAddresses']
const addressList = get(filters, 'address')
if (addressList && addressList.length) {
address = addressList.join(', ')
}
dataSource.push({ key, address, date, ...restTableColumns })
})
return { dataSource, tableColumns }
},
},
bar: {
chart: (viewMode, ticketGroupedCounter) => {
const { groupBy } = filterToQuery(
{ filter: filtersRef.current, viewMode, ticketType, mainGroup: groupTicketsBy }
)
const data = getAggregatedData(ticketGroupedCounter, groupBy, true)
const series = []
const axisLabels = Object.keys(data.summary)
.sort((firstLabel, secondLabel) => data.summary[firstLabel] - data.summary[secondLabel])
const legend = Object.keys(data)
Object.entries(data).map(([name, dataObj]) => {
const seriesData = []
axisLabels.forEach(axisLabel => {
seriesData.push(dataObj[axisLabel])
})
series.push({
name,
type: viewMode,
symbol: 'none',
stack: 'total',
sampling: 'sum',
large: true,
largeThreshold: 200,
data: seriesData,
emphasis: {
focus: 'self',
blurScope: 'self',
},
})
})
const axisData = { yAxis: { type: 'category', data: axisLabels }, xAxis: { type: 'value', data: null } }
const tooltip = { trigger: 'item', axisPointer: { type: 'line' }, borderColor: '#fff' }
const result = { series, legend, axisData, tooltip }
if (groupBy[0] === 'status') {
result['color'] = ticketLabelsRef.current.map(({ color }) => color)
}
return result
},
table: (viewMode, ticketGroupedCounter, restOptions) => {
const { groupBy } = filterToQuery(
{ filter: filtersRef.current, viewMode, ticketType, mainGroup: groupTicketsBy }
)
const data = getAggregatedData(ticketGroupedCounter, groupTicketsBy === 'status' ? groupBy.reverse() : groupBy)
const { translations, filters } = restOptions
const dataSource = []
const tableColumns: TableColumnsType = [
{ title: translations['address'], dataIndex: 'address', key: 'address', sorter: (a, b) => a['address'] - b['address'] },
...Object.entries(data).map(([key]) => ({ title: key, dataIndex: key, key, sorter: (a, b) => a[key] - b[key] })),
]
if (TICKET_REPORT_TABLE_MAIN_GROUP.includes(groupBy[1])) {
tableColumns.unshift({
title: translations[groupBy[1]],
dataIndex: groupBy[1],
key: groupBy[1],
sorter: (a, b) => a[groupBy[1]] - b[groupBy[1]],
})
}
const restTableColumns = {}
const addressList = get(filters, 'address', [])
const aggregateSummary = [addressList, get(filters, groupBy[1], [])]
.every(filterList => filterList.length === 0)
if (aggregateSummary) {
Object.entries(data).forEach((rowEntry) => {
const [ticketType, dataObj] = rowEntry
const counts = Object.values(dataObj) as number[]
restTableColumns[ticketType] = sum(counts)
})
dataSource.push({
key: 0,
address: translations['allAddresses'],
categoryClassifier: translations['allCategoryClassifiers'],
executor: translations['allExecutors'],
assignee: translations['allAssignees'],
...restTableColumns,
})
} else {
const mainAggregation = TICKET_REPORT_TABLE_MAIN_GROUP.includes(groupBy[1]) ? get(filters, groupBy[1], []) : null
// TODO(sitozzz): find clean solution for aggregation by 2 id_in fields
if (mainAggregation === null) {
addressList.forEach((address, key) => {
const tableRow = { key, address }
Object.entries(data).forEach(rowEntry => {
const [ticketType, dataObj] = rowEntry
const counts = Object.entries(dataObj)
.filter(obj => obj[0] === address).map(e => e[1]) as number[]
tableRow[ticketType] = sum(counts)
})
dataSource.push(tableRow)
})
} else {
mainAggregation.forEach((aggregateField, key) => {
const tableRow = { key, [groupBy[1]]: aggregateField }
tableRow['address'] = addressList.length
? addressList.join(', ')
: translations['allAddresses']
Object.entries(data).forEach(rowEntry => {
const [ticketType, dataObj] = rowEntry
const counts = Object.entries(dataObj)
.filter(obj => obj[0] === aggregateField).map(e => e[1]) as number[]
tableRow[ticketType] = sum(counts)
})
dataSource.push(tableRow)
})
}
}
return { dataSource, tableColumns }
},
},
pie: {
chart: (viewMode, ticketGroupedCounter) => {
const { groupBy } = filterToQuery(
{ filter: filtersRef.current, viewMode, ticketType, mainGroup: groupTicketsBy }
)
const data = getAggregatedData(ticketGroupedCounter, groupBy)
const series = []
const legend = [...new Set(Object.values(data).flatMap(e => Object.keys(e)))]
Object.entries(data).forEach(([label, groupObject]) => {
const chartData = Object.entries(groupObject)
.map(([name, value]) => ({ name, value }))
if (chartData.map(({ value }) => value).some(value => value > 0)) {
series.push({
name: label,
data: chartData,
selectedMode: false,
type: viewMode,
radius: [60, 120],
center: ['25%', '50%'],
symbol: 'none',
emphasis: {
focus: 'self',
blurScope: 'self',
},
labelLine: { show: false },
label: {
show: true,
fontSize: fontSizes.content,
overflow: 'none',
formatter: [
'{value|{b}} {percent|{d} %}',
].join('\n'),
rich: {
value: {
fontSize: fontSizes.content,
align: 'left',
width: 100,
},
percent: {
align: 'left',
fontWeight: 700,
fontSize: fontSizes.content,
width: 40,
},
},
},
labelLayout: (chart) => {
const { dataIndex, seriesIndex } = chart
const elementYOffset = 25 * dataIndex
const yOffset = 75 + 250 * Math.floor(seriesIndex / 2) + 10 + elementYOffset
return {
x: 340,
y: yOffset,
align: 'left',
verticalAlign: 'top',
}
},
})
}
})
return { series, legend, color: ticketLabelsRef.current.map(({ color }) => color) }
},
table: (viewMode, ticketGroupedCounter, restOptions) => {
const { groupBy } = filterToQuery(
{ filter: filtersRef.current, viewMode, ticketType, mainGroup: groupTicketsBy }
)
const data = getAggregatedData(ticketGroupedCounter, groupBy.reverse())
const { translations, filters } = restOptions
const dataSource = []
const tableColumns: TableColumnsType = [
{ title: translations['address'], dataIndex: 'address', key: 'address', sorter: (a, b) => a['address'] - b['address'] },
...Object.entries(data).map(([key]) => ({ title: key, dataIndex: key, key, sorter: (a, b) => a[key] - b[key] })),
]
if (TICKET_REPORT_TABLE_MAIN_GROUP.includes(groupBy[1])) {
tableColumns.unshift({
title: translations[groupBy[1]],
dataIndex: groupBy[1],
key: groupBy[1],
sorter: (a, b) => a[groupBy[1]] - b[groupBy[1]],
})
}
const restTableColumns = {}
const addressList = get(filters, 'address', [])
const aggregateSummary = [addressList, get(filters, groupBy[1], [])]
.every(filterList => filterList.length === 0)
if (aggregateSummary) {
const totalCount = Object.values(data)
.reduce((prev, curr) => prev + sum(Object.values(curr)), 0)
Object.entries(data).forEach((rowEntry) => {
const [ticketType, dataObj] = rowEntry
const counts = Object.values(dataObj) as number[]
restTableColumns[ticketType] = totalCount > 0
? ((sum(counts) / totalCount) * 100).toFixed(2) + ' %'
: totalCount
})
dataSource.push({
key: 0,
address: translations['allAddresses'],
categoryClassifier: translations['allCategoryClassifiers'],
executor: translations['allExecutors'],
assignee: translations['allAssignees'],
...restTableColumns,
})
} else {
const totalCounts = {}
Object.values(data).forEach((dataObj) => {
Object.entries(dataObj).forEach(([aggregationField, count]) => {
if (get(totalCounts, aggregationField, false)) {
totalCounts[aggregationField] += count
} else {
totalCounts[aggregationField] = count
}
})
})
const mainAggregation = TICKET_REPORT_TABLE_MAIN_GROUP.includes(groupBy[1]) ? get(filters, groupBy[1], []) : null
// TODO(sitozzz): find clean solution for aggregation by 2 id_in fields
if (mainAggregation === null) {
addressList.forEach((address, key) => {
const tableRow = { key, address }
Object.entries(data).forEach(rowEntry => {
const [ticketType, dataObj] = rowEntry
const counts = Object.entries(dataObj)
.filter(obj => obj[0] === address).map(e => e[1]) as number[]
const totalPropertyCount = sum(counts)
tableRow[ticketType] = totalCounts[address] > 0
? (totalPropertyCount / totalCounts[address] * 100).toFixed(2) + ' %'
: totalCounts[address]
})
dataSource.push(tableRow)
})
} else {
mainAggregation.forEach((aggregateField, key) => {
const tableRow = { key, [groupBy[1]]: aggregateField }
tableRow['address'] = addressList.length
? addressList.join(', ')
: translations['allAddresses']
Object.entries(data).forEach(rowEntry => {
const [ticketType, dataObj] = rowEntry
const counts = Object.entries(dataObj)
.filter(obj => obj[0] === aggregateField).map(e => e[1]) as number[]
const totalPropertyCount = sum(counts)
tableRow[ticketType] = totalCounts[aggregateField] > 0
? (totalPropertyCount / totalCounts[aggregateField] * 100).toFixed(2) + ' %'
: totalCounts[aggregateField]
})
dataSource.push(tableRow)
})
}
}
return { dataSource, tableColumns }
},
},
})
const { AND, groupBy } = filterToQuery(
{ filter: filtersRef.current, viewMode, ticketType: ticketTypeRef.current, mainGroup: groupTicketsBy }
)
const where = { organization: { id: userOrganizationId }, AND }
loadTicketAnalytics({ variables: { data: { groupBy, where, nullReplaces } } })
}
}, [userOrganizationId, viewMode, groupTicketsBy])
useEffect(() => {
const queryParams = getQueryParams()
setGroupTicketsBy(get(queryParams, 'groupTicketsBy', 'status'))
setViewMode(get(queryParams, 'viewMode', 'line'))
}, [])
useEffect(() => {
ticketTypeRef.current = ticketType
setAnalyticsData(null)
getAnalyticsData()
}, [groupTicketsBy, userOrganizationId, ticketType, viewMode])
// Download excel file when file link was created
useEffect(() => {
if (excelDownloadLink !== null && !isXSLXLoading) {
const link = document.createElement('a')
link.href = excelDownloadLink
link.target = '_blank'
link.hidden = true
document.body.appendChild(link)
link.click()
link.parentNode.removeChild(link)
setExcelDownloadLink(null)
}
}, [excelDownloadLink, isXSLXLoading])
const printPdf = useCallback(
() => {
let currentFilter
switch (groupTicketsBy) {
case 'property':
currentFilter = filtersRef.current.addressList
break
case 'categoryClassifier':
currentFilter = filtersRef.current.classifierList
break
case 'executor':
currentFilter = filtersRef.current.executorList
break
case 'assignee':
currentFilter = filtersRef.current.responsibleList
break
default:
currentFilter = null
}
const uniqueDataSets = Array.from(new Set(analyticsData.map(ticketCounter => ticketCounter[groupTicketsBy])))
const isPdfAvailable = currentFilter === null
|| uniqueDataSets.length < MAX_FILTERED_ELEMENTS
|| (currentFilter.length !== 0 && currentFilter.length < MAX_FILTERED_ELEMENTS)
if (isPdfAvailable) {
router.push(router.route + '/pdf?' + qs.stringify({
dateFrom: dateFrom.toISOString(),
dateTo: dateTo.toISOString(),
groupBy: groupTicketsBy,
ticketType,
viewMode,
addressList: JSON.stringify(filtersRef.current.addressList),
executorList: JSON.stringify(filtersRef.current.executorList),
assigneeList: JSON.stringify(filtersRef.current.responsibleList),
categoryClassifierList: JSON.stringify(filtersRef.current.classifierList),
specification: filtersRef.current.specification,
}))
} else {
setIsVisible(true)
}
},
[ticketType, viewMode, dateFrom, dateTo, groupTicketsBy, userOrganizationId, analyticsData],
)
const downloadExcel = useCallback(
() => {
const { AND, groupBy } = filterToQuery({ filter: filtersRef.current, viewMode, ticketType, mainGroup: groupTicketsBy })
const where = { organization: { id: userOrganizationId }, AND }
const filters = filtersRef.current
const translates: ExportTicketAnalyticsToExcelTranslates = {
property: filters.addressList.length
? filters.addressList.map(({ value }) => value).join('@')
: AllAddressTitle,
categoryClassifier: filters.classifierList.length
? filters.classifierList.map(({ value }) => value).join('@')
: AllCategoryClassifiersTitle,
executor: filters.executorList.length
? filters.executorList.map(({ value }) => value).join('@')
: AllExecutorsTitle,
assignee: filters.responsibleList.length
? filters.responsibleList.map(({ value }) => value).join('@')
: AllAssigneesTitle,
}
exportTicketAnalyticsToExcel({ variables: { data: { groupBy, where, translates, nullReplaces } } })
},
[ticketType, viewMode, dateFrom, dateTo, groupTicketsBy, userOrganizationId],
)
const onFilterChange: ITicketAnalyticsPageFilterProps['onChange'] = useCallback((filters) => {
setAnalyticsData(null)
filtersRef.current = filters
getAnalyticsData()
}, [viewMode, userOrganizationId, groupTicketsBy, dateFrom, dateTo])
let addressFilterTitle = selectedAddresses.length === 0 ? AllAddresses : `${SingleAddress} «${selectedAddresses[0].value}»`
if (selectedAddresses.length > 1) {
addressFilterTitle = ManyAddresses
}
const onTabChange = useCallback((key: GroupTicketsByTypes) => {
setGroupTicketsBy((prevState) => {
if (prevState !== key) {
setAnalyticsData(null)
return key
} else {
return prevState
}
})
if (key === 'status') {
setViewMode('line')
} else {
setViewMode('bar')
}
}, [viewMode, groupTicketsBy])
const isControlsDisabled = loading || isXSLXLoading || filtersRef.current === null
return <>
<Head>
<title>{PageTitle}</title>
</Head>
<PageWrapper>
<PageContent>
<Row gutter={[0, 40]}>
<Col xs={24} sm={18}>
<PageHeader title={<Typography.Title>{PageTitle}</Typography.Title>} />
</Col>
<Col span={6} hidden={isSmall}>
<Tooltip title={NotImplementedYetMessage}>
<Button icon={<PlusCircleFilled />} type='sberPrimary' secondary>{HeaderButtonTitle}</Button>
</Tooltip>
</Col>
</Row>
<Row gutter={[0, 24]} align={'top'} justify={'space-between'}>
<Col span={24}>
<Tabs
css={tabsCss}
defaultActiveKey='status'
activeKey={groupTicketsBy}
onChange={onTabChange}
>
<Tabs.TabPane key='status' tab={StatusFilterLabel} />
<Tabs.TabPane key='property' tab={PropertyFilterLabel} />
<Tabs.TabPane key='categoryClassifier' tab={CategoryFilterLabel} />
<Tabs.TabPane key='executor' tab={UserFilterLabel} />
<Tabs.TabPane key='assignee' tab={ResponsibleFilterLabel} />
</Tabs>
</Col>
<Col span={24}>
<Row justify={'space-between'} gutter={[0, 20]}>
<Col span={24}>
<TicketAnalyticsPageFilter
onChange={onFilterChange}
viewMode={viewMode}
groupTicketsBy={groupTicketsBy}
/>
</Col>
<Col span={24}>
<Divider/>
</Col>
<Col xs={24} lg={16}>
<Typography.Title level={3}>
{ViewModeTitle} {selectedPeriod} {addressFilterTitle} {AllCategories}
</Typography.Title>
</Col>
<Col xs={12} lg={3}>
<RadioGroupWithIcon
value={viewMode}
size='small'
buttonStyle='outline'
onChange={(e) => setViewMode(e.target.value)}
>
{groupTicketsBy === 'status' && (
<Radio.Button value='line'>
<LinearChartIcon height={32} width={24} />
</Radio.Button>
)}
<Radio.Button value='bar'>
<BarChartIcon height={32} width={24} />
</Radio.Button>
{groupTicketsBy !== 'status' && (
<Radio.Button value='pie'>
<PieChartIcon height={32} width={24} />
</Radio.Button>
)}
</RadioGroupWithIcon>
</Col>
<Col
xs={8}
hidden={!isSmall}
>
<TicketTypeSelect
ticketType={ticketType}
setTicketType={setTicketType}
loading={loading}
/>
</Col>
</Row>
</Col>
<Col span={24}>
{useMemo(() => (
<TicketChartView
data={analyticsData}
loading={loading}
viewMode={viewMode}
mainGroup={groupTicketsBy}
chartConfig={{
animationEnabled: true,
chartOptions: { renderer: 'svg', height: viewMode === 'line' ? 440 : 'auto' },
}}
mapperInstance={mapperInstanceRef.current}
>
<Col
style={{ position: 'absolute', top: 0, right: 0, minWidth: '132px' }}
hidden={isSmall}
>
<TicketTypeSelect
ticketType={ticketType}
setTicketType={setTicketType}
loading={loading}
/>
</Col>
</TicketChartView>
), [analyticsData, loading, viewMode, ticketType, userOrganizationId, groupTicketsBy, isSmall])}
</Col>
<Col span={24}>
<Row gutter={[0, 20]}>
<Col span={24}>
<Typography.Title level={4}>{TableTitle}</Typography.Title>
</Col>
<Col span={24}>
{useMemo(() => (
<TicketListView
data={analyticsData}
loading={loading}
viewMode={viewMode}
filters={filtersRef.current}
mapperInstance={mapperInstanceRef.current}
/>
), [analyticsData, loading, viewMode, ticketType, userOrganizationId, groupTicketsBy])}
</Col>
</Row>
</Col>
<ActionBar hidden={isSmall}>
<Button disabled={isControlsDisabled || isEmpty(analyticsData)} onClick={printPdf} icon={<FilePdfFilled />} type='sberPrimary' secondary>
{PrintTitle}
</Button>
<Button disabled={isControlsDisabled || isEmpty(analyticsData)} onClick={downloadExcel} loading={isXSLXLoading} icon={<EditFilled />} type='sberPrimary' secondary>
{ExcelTitle}
</Button>
</ActionBar>
</Row>
<TicketWarningModal />
</PageContent>
</PageWrapper>
</>
}
Example #10
Source File: index.tsx From sidebar with Apache License 2.0 | 4 votes |
ScriptLibraryList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [createModalVisible, setCreateModalVisible] = useState(false);
const [groups, setGroups] = useState<ScriptLibraryGroupItem[]>([]);
const [keyword, setKeyword] = useState<string>("");
useEffect(() => {
setLoading(true);
SearchScriptLibraryGroup({
page_size: 5000,
keyword
}).then((res: CommonItemsResp<ScriptLibraryGroupItem>) => {
setLoading(false);
if (res.code !== 0) {
message.error("获取话术库分组失败");
return
}
setGroups(res.data.items)
})
}, [keyword])
return (
<div className={styles.scriptLibraryContainer}>
<ProCard
tabs={{
type: 'line',
}}
>
<ProCard.TabPane key="corp" tab="企业话术">
<div>
<Input.Search placeholder="输入关键词" loading={loading} allowClear onSearch={(value) => {
setKeyword(value);
}}/>
</div>
<div hidden={true}>
<Button
type={'link'}
style={{padding: 0, marginTop: 12}}
icon={<PlusCircleFilled style={{color: '#1966ff'}}/>}
onClick={() => setCreateModalVisible(true)}
>新建分组</Button>
</div>
<div>
{groups.length === 0 && (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
)}
{groups.length > 0 && (
<Collapse expandIcon={(panelProps) => {
const rotate = panelProps.isActive ? 90 : 0;
return <CaretRightFilled rotate={rotate} style={{fontSize: 10, color: 'rgba(66,66,66,0.65)'}}/>
}} ghost>
{groups.map((group) => (
<Collapse.Panel header={<>{group.name}({group?.quick_replies?.length})</>} key={group.id}>
{group?.quick_replies && group?.quick_replies?.length > 0 && (
<div className={styles.scriptList}>
{group.quick_replies.map((quick_reply) => (
<Space direction={'vertical'} className={styles.scriptItem} key={quick_reply.id}>
<Space direction={'horizontal'} className={styles.scriptTitleBar}>
<span style={{width: 200}} className={styles.sendButtonWrap}>
<Button
// 发送一组话术
onClick={async () => {
await BatchSendQuickReply(quick_reply.reply_details)
}}
type={'link'}
className={styles.sendButton}
icon={getIcon('icon-fasong')}
/>
</span>
<span className={styles.title}>
{quick_reply.name}
</span>
</Space>
<Space direction={'vertical'} className={styles.replyDetailList}>
{quick_reply?.reply_details && quick_reply?.reply_details?.length > 0 && quick_reply.reply_details.map((reply_detail) => (
<Space direction={'horizontal'} className={styles.replyDetailItem}
key={reply_detail.id}>
<div className={styles.sendButtonWrap}>
<Button
// 发送单一话术
type={'link'}
onClick={async () => {
await SendQuickReply(reply_detail)
}}
className={styles.sendButton}
icon={getIcon('icon-fasong')}/>
</div>
<div>
{
reply_detail.content_type === 2 && (
<ExpandableParagraph
rows={3}
content={reply_detail.quick_reply_content.text.content}
/>
)
}
{
reply_detail.content_type === 3 &&
<div key={reply_detail.id} className={styles.replyPreviewItem}>
<div className={styles.leftPart}>
<Image src={reply_detail.quick_reply_content?.image?.picurl} fallback={fileIconImage}
/>
</div>
<div className={styles.rightPart}>
<p>{reply_detail.quick_reply_content?.image?.title}</p>
<p>{humanFileSize(reply_detail?.quick_reply_content?.image?.size)}</p>
</div>
</div>
}
{
reply_detail.content_type === 4 && <div className={styles.replyPreviewItem}>
<div className={styles.leftPart}>
<Image src={reply_detail.quick_reply_content?.link?.picurl} preview={false} fallback={fileIconImage}
/>
</div>
<div className={styles.rightPart}>
<p>{reply_detail.quick_reply_content?.link?.title}</p>
<p>{reply_detail.quick_reply_content?.link?.desc}</p>
</div>
</div>
}
{
reply_detail.content_type === 5 && <div className={styles.replyPreviewItem}>
<div className={styles.leftPart}>
<Image src={pdfImage} preview={false}/>
</div>
<div className={styles.rightPart}>
<p>{reply_detail.quick_reply_content?.pdf?.title}</p>
<p>{humanFileSize(reply_detail.quick_reply_content?.pdf?.size)}</p>
</div>
</div>
}
{
reply_detail.content_type === 6 && <div className={styles.replyPreviewItem}>
<div className={styles.leftPart}>
<video src={reply_detail.quick_reply_content?.video?.picurl}/>
</div>
<div className={styles.rightPart}>
<p>{reply_detail.quick_reply_content?.video?.title}</p>
<p>{humanFileSize(reply_detail.quick_reply_content?.video?.size)}</p>
</div>
</div>
}
</div>
</Space>
))}
</Space>
</Space>
))}
</div>
)}
</Collapse.Panel>
))}
</Collapse>
)}
</div>
</ProCard.TabPane>
<ProCard.TabPane key="staff" tab="个人话术" disabled={true}>
</ProCard.TabPane>
</ProCard>
<BottomNavBar links={[
{
title: '话术',
url: '/staff-frontend/script-library',
icon: 'icon-message-success',
},
{
title: '素材',
url: '/staff-frontend/material-library',
icon: 'icon-sucai-outline',
},
{
title: '雷达',
url: '/staff-frontend/radar',
icon: 'icon-leida',
disabled: true,
},
]}/>
<ModalForm
width={400}
className={'dialog'}
layout={'horizontal'}
visible={createModalVisible}
onVisibleChange={setCreateModalVisible}
// onFinish={async (params) =>
// HandleRequest({...currentGroup, ...params}, CreateGroup, () => {
// setGroupItemsTimestamp(Date.now);
// })
// }
>
<h2 className='dialog-title'> 新建分组 </h2>
<ProFormText
name='name'
label='分组名称'
tooltip='最长为 24 个汉字'
placeholder='请输入分组名称'
rules={[
{
required: true,
message: '请填写分组名称',
},
]}
/>
</ModalForm>
</div>
);
}