316 lines
8.8 KiB
TypeScript
316 lines
8.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Table,
|
|
Typography,
|
|
Tooltip,
|
|
Input,
|
|
Button,
|
|
Dropdown,
|
|
Modal as AntdModal,
|
|
Avatar, TableColumnType, Spin
|
|
} from 'antd';
|
|
import {
|
|
EditOutlined,
|
|
DeleteOutlined,
|
|
} from '@ant-design/icons';
|
|
import { EllipsisVertical } from 'lucide-react';
|
|
import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface';
|
|
import { useStore } from 'App/mstore';
|
|
import { toast } from 'react-toastify';
|
|
import { useHistory } from 'react-router';
|
|
import { withSiteId } from 'App/routes';
|
|
import { Icon } from 'UI';
|
|
import cn from 'classnames';
|
|
import { TYPE_ICONS, TYPE_NAMES } from 'App/constants/card';
|
|
import Widget from 'App/mstore/types/widget';
|
|
import { useTranslation } from 'react-i18next';
|
|
import ORLoader from 'Shared/ORLoader';
|
|
|
|
const { Text } = Typography;
|
|
|
|
interface Props {
|
|
list: Widget[];
|
|
siteId: string;
|
|
selectedList: number[];
|
|
toggleSelection?: (metricId: number | number[]) => void;
|
|
disableSelection?: boolean;
|
|
inLibrary?: boolean;
|
|
loading?: boolean;
|
|
}
|
|
|
|
const ListView: React.FC<Props> = ({
|
|
list,
|
|
siteId,
|
|
selectedList,
|
|
toggleSelection,
|
|
disableSelection = false,
|
|
inLibrary = false,
|
|
loading = false
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const [editingMetricId, setEditingMetricId] = useState<number | null>(null);
|
|
const [newName, setNewName] = useState('');
|
|
const { metricStore } = useStore();
|
|
const history = useHistory();
|
|
|
|
const totalMessage = (
|
|
<>
|
|
{t('Showing')}{' '}
|
|
<Text strong>
|
|
{(metricStore.pageSize || 10) * ((metricStore.page || 1) - 1) + 1}
|
|
</Text>{' '}
|
|
{t('to')}{' '}
|
|
<Text strong>
|
|
{Math.min(
|
|
(metricStore.pageSize || 10) * (metricStore.page || 1),
|
|
list.length
|
|
)}
|
|
</Text>{' '}
|
|
{t('of')} <Text strong>{list.length}</Text> {t('cards')}
|
|
</>
|
|
);
|
|
|
|
const parseDate = (dateString: string) => {
|
|
let date = new Date(dateString);
|
|
if (isNaN(date.getTime())) {
|
|
date = new Date(parseInt(dateString, 10));
|
|
}
|
|
return date;
|
|
};
|
|
|
|
const formatDate = (date: Date) => {
|
|
const now = new Date();
|
|
const diffTime = Math.abs(now.getTime() - date.getTime());
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
const formatTime = (d: Date) => {
|
|
let hours = d.getHours();
|
|
const minutes = d.getMinutes().toString().padStart(2, '0');
|
|
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
hours = hours % 12 || 12;
|
|
return `${hours}:${minutes} ${ampm}`;
|
|
};
|
|
if (diffDays <= 1) return `${t('Today at')} ${formatTime(date)}`;
|
|
if (diffDays === 2) return `${t('Yesterday at')} ${formatTime(date)}`;
|
|
if (diffDays <= 3)
|
|
return `${diffDays} ${t('days ago at')} ${formatTime(date)}`;
|
|
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${t('at')} ${formatTime(date)}`;
|
|
};
|
|
|
|
const MetricTypeIcon: React.FC<{ type: string }> = ({ type }) => (
|
|
<Tooltip title={<div className="capitalize">{TYPE_NAMES(t)[type]}</div>}>
|
|
<Avatar
|
|
src={
|
|
<Icon
|
|
name={TYPE_ICONS[type]}
|
|
size="16"
|
|
color="tealx"
|
|
strokeColor="tealx"
|
|
/>
|
|
}
|
|
size="default"
|
|
className="bg-tealx-lightest text-tealx mr-2 cursor-default avatar-card-list-item"
|
|
/>
|
|
</Tooltip>
|
|
);
|
|
|
|
const onItemClick = (metric: Widget) => {
|
|
if (disableSelection) {
|
|
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
|
history.push(path);
|
|
} else {
|
|
toggleSelection?.(metric.metricId);
|
|
}
|
|
};
|
|
|
|
const onMenuClick = async (metric: Widget, { key }: { key: string }) => {
|
|
if (key === 'delete') {
|
|
AntdModal.confirm({
|
|
title: t('Confirm'),
|
|
content: t('Are you sure you want to permanently delete this card?'),
|
|
okText: t('Yes, delete'),
|
|
cancelText: t('No'),
|
|
onOk: async () => {
|
|
await metricStore.delete(metric);
|
|
}
|
|
});
|
|
}
|
|
if (key === 'rename') {
|
|
setEditingMetricId(metric.metricId);
|
|
setNewName(metric.name);
|
|
}
|
|
};
|
|
|
|
const onRename = async () => {
|
|
const metric = list.find((m) => m.metricId === editingMetricId);
|
|
if (!metric) return;
|
|
try {
|
|
metric.update({ name: newName });
|
|
await metricStore.save(metric);
|
|
// await metricStore.fetchList();
|
|
setEditingMetricId(null);
|
|
} catch (e) {
|
|
toast.error(t('Failed to rename card'));
|
|
}
|
|
};
|
|
|
|
const menuItems = [
|
|
{ key: 'rename', icon: <EditOutlined />, label: t('Rename') },
|
|
{ key: 'delete', icon: <DeleteOutlined />, label: t('Delete') }
|
|
];
|
|
|
|
const renderTitle = (_text: string, metric: Widget) => (
|
|
<div
|
|
className="flex items-center cursor-pointer"
|
|
onClick={() => onItemClick(metric)}
|
|
>
|
|
<MetricTypeIcon type={metric.metricType} />
|
|
<div className={cn('capitalize-first block', !inLibrary ? 'link' : '')}>
|
|
{metric.name}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderOwner = (_text: string, metric: Widget) => (
|
|
<div>{metric.owner}</div>
|
|
);
|
|
|
|
const renderLastModified = (_text: string, metric: Widget) => {
|
|
const date = parseDate(metric.lastModified);
|
|
return formatDate(date);
|
|
};
|
|
|
|
const renderOptions = (_text: string, metric: Widget) => (
|
|
<div className="flex justify-end pr-4">
|
|
<Dropdown
|
|
menu={{ items: menuItems, onClick: (e) => onMenuClick(metric, e) }}
|
|
trigger={['click']}
|
|
>
|
|
<Button
|
|
icon={<EllipsisVertical size={16} />}
|
|
className="btn-cards-list-item-more-options"
|
|
type="text"
|
|
/>
|
|
</Dropdown>
|
|
</div>
|
|
);
|
|
|
|
const columns: TableColumnType<any>[] = [
|
|
{
|
|
title: t('Title'),
|
|
dataIndex: 'name',
|
|
key: 'title',
|
|
className: 'cap-first pl-4',
|
|
sorter: true,
|
|
sortOrder: metricStore.sort.field === 'name' ? metricStore.sort.order : undefined,
|
|
width: inLibrary ? '31%' : '25%',
|
|
render: renderTitle
|
|
},
|
|
{
|
|
title: t('Owner'),
|
|
dataIndex: 'owner_email',
|
|
key: 'owner',
|
|
className: 'capitalize',
|
|
sorter: true,
|
|
sortOrder: metricStore.sort.field === 'owner_email' ? metricStore.sort.order : undefined,
|
|
width: inLibrary ? '31%' : '25%',
|
|
render: renderOwner
|
|
},
|
|
{
|
|
title: t('Last Modified'),
|
|
dataIndex: 'edited_at',
|
|
key: 'lastModified',
|
|
sorter: true,
|
|
sortOrder: metricStore.sort.field === 'edited_at' ? metricStore.sort.order : undefined,
|
|
width: inLibrary ? '31%' : '25%',
|
|
render: renderLastModified
|
|
}
|
|
];
|
|
|
|
if (!inLibrary) {
|
|
columns.push({
|
|
title: '',
|
|
key: 'options',
|
|
className: 'text-right',
|
|
width: '5%',
|
|
render: renderOptions
|
|
});
|
|
}
|
|
|
|
const handleTableChange = (
|
|
pag: TablePaginationConfig,
|
|
_filters: Record<string, (string | number | boolean)[] | null>,
|
|
sorterParam: SorterResult<Widget> | SorterResult<Widget>[]
|
|
) => {
|
|
const sorter = Array.isArray(sorterParam) ? sorterParam[0] : sorterParam;
|
|
let order = sorter.order;
|
|
if (metricStore.sort.field === sorter.field) {
|
|
order = metricStore.sort.order === 'ascend' ? 'descend' : 'ascend';
|
|
}
|
|
console.log('sorter', { field: sorter.field, order });
|
|
metricStore.updateKey('sort', { field: sorter.field, order });
|
|
metricStore.updateKey('page', pag.current || 1);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={list}
|
|
rowKey="metricId"
|
|
showSorterTooltip={false}
|
|
onChange={handleTableChange}
|
|
sortDirections={['ascend', 'descend']}
|
|
loading={{
|
|
spinning: loading,
|
|
delay: 0,
|
|
indicator: <Spin indicator={<ORLoader />} />,
|
|
}}
|
|
onRow={
|
|
inLibrary
|
|
? (record) => ({
|
|
onClick: () => {
|
|
if (!disableSelection) toggleSelection?.(record?.metricId);
|
|
}
|
|
})
|
|
: undefined
|
|
}
|
|
rowSelection={
|
|
!disableSelection
|
|
? {
|
|
selectedRowKeys: selectedList,
|
|
onChange: (keys) => toggleSelection && toggleSelection(keys),
|
|
columnWidth: 16
|
|
}
|
|
: undefined
|
|
}
|
|
pagination={{
|
|
current: metricStore.page,
|
|
pageSize: metricStore.pageSize,
|
|
total: metricStore.total,
|
|
showSizeChanger: false,
|
|
className: 'px-4',
|
|
showLessItems: true,
|
|
showTotal: () => totalMessage,
|
|
size: 'small',
|
|
simple: true
|
|
}}
|
|
/>
|
|
<AntdModal
|
|
title={t('Rename Card')}
|
|
open={editingMetricId !== null}
|
|
okText={t('Save')}
|
|
cancelText={t('Cancel')}
|
|
onOk={onRename}
|
|
onCancel={() => setEditingMetricId(null)}
|
|
>
|
|
<Input
|
|
placeholder={t('Enter new card title')}
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
/>
|
|
</AntdModal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ListView;
|