payment-kit 1.14.21 → 1.14.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/blocklet.yml +1 -1
  2. package/package.json +19 -19
  3. package/src/app.tsx +13 -12
  4. package/src/components/balance-list.tsx +12 -2
  5. package/src/components/copyable.tsx +3 -2
  6. package/src/components/customer/edit.tsx +25 -21
  7. package/src/components/customer/form.tsx +18 -28
  8. package/src/components/customer/link.tsx +1 -2
  9. package/src/components/date-range-picker.tsx +21 -0
  10. package/src/components/drawer-form.tsx +27 -4
  11. package/src/components/event/list.tsx +3 -2
  12. package/src/components/filter-toolbar.tsx +11 -4
  13. package/src/components/info-card.tsx +4 -2
  14. package/src/components/info-metric.tsx +2 -2
  15. package/src/components/info-row.tsx +33 -26
  16. package/src/components/invoice/list.tsx +2 -2
  17. package/src/components/invoice/table.tsx +148 -85
  18. package/src/components/invoice-pdf/pdf.tsx +5 -1
  19. package/src/components/layout/admin.tsx +8 -2
  20. package/src/components/metadata/editor.tsx +25 -18
  21. package/src/components/metadata/form.tsx +83 -25
  22. package/src/components/metadata/list.tsx +22 -6
  23. package/src/components/payment-intent/list.tsx +3 -3
  24. package/src/components/payment-link/preview.tsx +42 -24
  25. package/src/components/payouts/list.tsx +2 -3
  26. package/src/components/price/form.tsx +28 -15
  27. package/src/components/price/upsell.tsx +1 -4
  28. package/src/components/pricing-table/preview.tsx +42 -23
  29. package/src/components/product/cross-sell-select.tsx +1 -1
  30. package/src/components/product/cross-sell.tsx +3 -4
  31. package/src/components/product/edit-price.tsx +0 -1
  32. package/src/components/refund/list.tsx +9 -4
  33. package/src/components/section/header.tsx +11 -4
  34. package/src/components/subscription/description.tsx +10 -6
  35. package/src/components/subscription/items/index.tsx +28 -6
  36. package/src/components/subscription/list.tsx +2 -2
  37. package/src/components/subscription/metrics.tsx +10 -8
  38. package/src/components/subscription/portal/actions.tsx +37 -11
  39. package/src/components/subscription/portal/list.tsx +131 -70
  40. package/src/components/subscription/status.tsx +9 -3
  41. package/src/global.css +1 -1
  42. package/src/hooks/mobile.ts +3 -3
  43. package/src/libs/util.ts +6 -2
  44. package/src/locales/en.tsx +37 -1
  45. package/src/locales/zh.tsx +37 -1
  46. package/src/pages/admin/billing/index.tsx +24 -4
  47. package/src/pages/admin/billing/invoices/detail.tsx +302 -147
  48. package/src/pages/admin/billing/subscriptions/detail.tsx +259 -134
  49. package/src/pages/admin/customers/customers/detail.tsx +358 -175
  50. package/src/pages/admin/customers/customers/index.tsx +8 -5
  51. package/src/pages/admin/customers/index.tsx +22 -5
  52. package/src/pages/admin/developers/webhooks/index.tsx +2 -2
  53. package/src/pages/admin/index.tsx +24 -10
  54. package/src/pages/admin/overview.tsx +1 -1
  55. package/src/pages/admin/payments/index.tsx +22 -7
  56. package/src/pages/admin/payments/intents/detail.tsx +263 -121
  57. package/src/pages/admin/payments/payouts/detail.tsx +235 -102
  58. package/src/pages/admin/payments/refunds/detail.tsx +286 -133
  59. package/src/pages/admin/products/index.tsx +28 -12
  60. package/src/pages/admin/products/links/create.tsx +16 -6
  61. package/src/pages/admin/products/links/detail.tsx +280 -176
  62. package/src/pages/admin/products/links/index.tsx +4 -7
  63. package/src/pages/admin/products/passports/index.tsx +6 -3
  64. package/src/pages/admin/products/prices/detail.tsx +260 -139
  65. package/src/pages/admin/products/prices/list.tsx +7 -3
  66. package/src/pages/admin/products/pricing-tables/create.tsx +17 -5
  67. package/src/pages/admin/products/pricing-tables/detail.tsx +221 -121
  68. package/src/pages/admin/products/pricing-tables/index.tsx +1 -2
  69. package/src/pages/admin/products/products/detail.tsx +262 -119
  70. package/src/pages/admin/products/products/index.tsx +1 -2
  71. package/src/pages/admin/settings/index.tsx +27 -7
  72. package/src/pages/customer/index.tsx +431 -143
  73. package/src/pages/customer/invoice/detail.tsx +138 -26
  74. package/src/pages/customer/refund/list.tsx +193 -4
  75. package/src/pages/customer/subscription/change-payment.tsx +20 -20
  76. package/src/pages/customer/subscription/detail.tsx +168 -34
  77. package/src/pages/customer/subscription/embed.tsx +22 -19
  78. package/src/pages/home.tsx +7 -1
  79. package/src/components/table.tsx +0 -93
@@ -1,6 +1,5 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { Box, Stack } from '@mui/material';
3
- import { styled } from '@mui/system';
2
+ import { Box, Stack, SxProps } from '@mui/material';
4
3
  import type { ReactNode } from 'react';
5
4
 
6
5
  type Props = {
@@ -8,12 +7,16 @@ type Props = {
8
7
  value?: string | ReactNode;
9
8
  alignItems?: string;
10
9
  sizes?: [number, number];
10
+ direction?: 'row' | 'column';
11
+ sx?: SxProps;
11
12
  };
12
13
 
13
14
  InfoRow.defaultProps = {
14
15
  value: undefined,
15
16
  sizes: [1, 3],
16
17
  alignItems: 'center',
18
+ direction: 'row',
19
+ sx: {},
17
20
  };
18
21
 
19
22
  export default function InfoRow(props: Props) {
@@ -21,29 +24,33 @@ export default function InfoRow(props: Props) {
21
24
  const isNone = props.value === '' || typeof props.value === 'undefined';
22
25
  const sizes = props.sizes || [1, 3];
23
26
  return (
24
- <Root>
25
- <Stack
26
- direction="row"
27
- alignItems={props.alignItems}
28
- justifyContent="space-between"
29
- flexWrap="wrap"
30
- sx={{ mb: 1 }}
31
- className="info-row-wrapper">
32
- <Box flex={sizes[0]} color="text.secondary">
33
- {props.label}
34
- </Box>
35
- <Box flex={sizes[1]} color={isNone ? 'text.disabled' : 'color.primary'}>
36
- {isNone ? t('common.none') : props.value}
37
- </Box>
38
- </Stack>
39
- </Root>
27
+ <Stack
28
+ direction={props.direction || 'row'}
29
+ alignItems={props.alignItems}
30
+ justifyContent="space-between"
31
+ flexWrap="wrap"
32
+ sx={{
33
+ mb: 2,
34
+ ...props.sx,
35
+ a: {
36
+ color: 'text.link',
37
+ },
38
+ }}
39
+ className="info-row-wrapper">
40
+ <Box
41
+ flex={sizes[0]}
42
+ color="text.primary"
43
+ fontWeight={500}
44
+ fontSize={14}
45
+ sx={{
46
+ flex: props.direction === 'column' ? 'none' : sizes[0],
47
+ mb: props.direction === 'column' ? 0.5 : 0,
48
+ }}>
49
+ {props.label}
50
+ </Box>
51
+ <Box flex={sizes[1]} color={isNone ? 'text.disabled' : 'text.secondary'} className="info-row-value" fontSize={14}>
52
+ {isNone ? t('common.none') : props.value}
53
+ </Box>
54
+ </Stack>
40
55
  );
41
56
  }
42
-
43
- const Root = styled(Box)`
44
- @media (max-width: 600px) {
45
- .info-row-wrapper {
46
- display: block !important;
47
- }
48
- }
49
- `;
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { Status, api, formatBNStr, formatTime, getInvoiceStatusColor } from '@blocklet/payment-react';
3
+ import { Status, api, formatBNStr, formatTime, getInvoiceStatusColor, Table } from '@blocklet/payment-react';
4
4
  import type { TInvoiceExpanded } from '@blocklet/payment-types';
5
5
  import { CircularProgress, Typography } from '@mui/material';
6
6
  import { useLocalStorageState } from 'ahooks';
@@ -9,7 +9,6 @@ import { Link } from 'react-router-dom';
9
9
 
10
10
  import CustomerLink from '../customer/link';
11
11
  import FilterToolbar from '../filter-toolbar';
12
- import Table from '../table';
13
12
  import InvoiceActions from './action';
14
13
 
15
14
  const fetchData = (params: Record<string, any> = {}): Promise<{ list: TInvoiceExpanded[]; count: number }> => {
@@ -295,6 +294,7 @@ export default function InvoiceList({
295
294
  />
296
295
  )
297
296
  }
297
+ emptyNodeText={`${t('empty.invoices')}`}
298
298
  />
299
299
  );
300
300
  }
@@ -1,12 +1,13 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
1
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { formatAmount, formatToDate, getPriceUintAmountByCurrency } from '@blocklet/payment-react';
3
+ import { formatAmount, Table, getPriceUintAmountByCurrency } from '@blocklet/payment-react';
3
4
  import type { TInvoiceExpanded, TInvoiceItem } from '@blocklet/payment-types';
4
5
  import { InfoOutlined } from '@mui/icons-material';
5
- import { Box, Stack, Table, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography } from '@mui/material';
6
- import { styled } from '@mui/system';
6
+ import { Box, Stack, Tooltip, Typography } from '@mui/material';
7
7
  import { toBN } from '@ocap/util';
8
8
  import { useSetState } from 'ahooks';
9
9
 
10
+ import { styled } from '@mui/system';
10
11
  import LineItemActions from '../subscription/items/actions';
11
12
  import { UsageRecordDialog } from '../subscription/items/usage-records';
12
13
 
@@ -112,7 +113,7 @@ export function getInvoiceRows(invoice: TInvoiceExpanded) {
112
113
  }
113
114
 
114
115
  export default function InvoiceTable({ invoice, simple }: Props) {
115
- const { t } = useLocaleContext();
116
+ const { t, locale } = useLocaleContext();
116
117
  const { detail, summary } = getInvoiceRows(invoice);
117
118
  const [state, setState] = useSetState({
118
119
  subscriptionId: '',
@@ -135,81 +136,131 @@ export default function InvoiceTable({ invoice, simple }: Props) {
135
136
  });
136
137
  };
137
138
 
138
- return (
139
- <Box>
140
- <StyledTable>
141
- <TableHead>
142
- <TableRow sx={{ borderBottom: '1px solid #eee' }}>
143
- <TableCell sx={{ textTransform: 'none', fontWeight: 'normal' }}>Description</TableCell>
144
- <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 80 }} align="right">
145
- {t('common.quantity')}
146
- </TableCell>
147
- <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 120 }} align="right">
148
- {t('payment.customer.invoice.unitPrice')}
149
- </TableCell>
150
- <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 110 }} align="right">
151
- {t('common.amount')}
152
- </TableCell>
153
- {!simple && (
154
- <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 50 }} align="right">
155
- &nbsp;
156
- </TableCell>
157
- )}
158
- </TableRow>
159
- {invoice.period_end > 0 && invoice.period_start > 0 && (
160
- <TableRow sx={{ borderBottom: '1px solid #eee' }}>
161
- <TableCell align="left" colSpan={simple ? 4 : 5}>
162
- <Typography component="span" variant="body1" color="text.secondary">
163
- {formatToDate(invoice.period_start * 1000)} - {formatToDate(invoice.period_end * 1000)}
164
- </Typography>
165
- </TableCell>
166
- </TableRow>
167
- )}
168
- </TableHead>
169
- <TableBody>
170
- {detail.map((line) => (
171
- <TableRow key={line.id} sx={{ borderBottom: '1px solid #eee' }}>
172
- <TableCell sx={{ fontWeight: 600 }}>{line.product}</TableCell>
173
- <TableCell align="right">
174
- <Stack
175
- direction="row"
176
- spacing={0.5}
177
- alignItems="center"
178
- justifyContent="flex-end"
179
- sx={{ cursor: 'pointer' }}
180
- onClick={() => onOpenUsageRecords(line)}>
181
- <Typography component="span">{line.quantity}</Typography>
182
- {!!line.rawQuantity && (
183
- <Tooltip
184
- title={t('payment.customer.invoice.rawQuantity', { quantity: line.rawQuantity })}
185
- placement="top">
186
- <InfoOutlined fontSize="small" sx={{ color: 'text.secondary', cursor: 'pointer' }} />
187
- </Tooltip>
188
- )}
189
- </Stack>
190
- </TableCell>
191
- <TableCell align="right">{line.price}</TableCell>
192
- <TableCell align="right">{line.amount}</TableCell>
193
- {!simple && (
194
- <TableCell align="right">
195
- <LineItemActions data={line as any} />
196
- </TableCell>
139
+ const columns = [
140
+ {
141
+ label: t('common.description'),
142
+ name: 'product',
143
+ },
144
+ {
145
+ label: t('common.quantity'),
146
+ name: 'quantity',
147
+ width: 200,
148
+ align: 'right',
149
+ options: {
150
+ filter: true,
151
+ customBodyRenderLite: (_: string, index: number) => {
152
+ const item = detail[index] as InvoiceDetailItem;
153
+ return (
154
+ <Stack
155
+ direction="row"
156
+ spacing={0.5}
157
+ alignItems="center"
158
+ sx={{
159
+ cursor: 'pointer',
160
+ justifyContent: {
161
+ xs: 'flex-start',
162
+ md: 'flex-end',
163
+ },
164
+ }}
165
+ onClick={() => onOpenUsageRecords(item)}>
166
+ <Typography component="span">{item.quantity}</Typography>
167
+ {!!item.rawQuantity && (
168
+ <Tooltip
169
+ title={t('payment.customer.invoice.rawQuantity', { quantity: item.rawQuantity })}
170
+ placement="top">
171
+ <InfoOutlined fontSize="small" sx={{ color: 'text.secondary', cursor: 'pointer' }} />
172
+ </Tooltip>
197
173
  )}
198
- </TableRow>
199
- ))}
200
- {summary.map((line) => (
201
- <TableRow key={line.key}>
202
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: line.color }}>
203
- {t(line.key)}
204
- </TableCell>
205
- <TableCell align="right" sx={{ fontWeight: 600 }}>
206
- {line.value}
207
- </TableCell>
208
- <TableCell>&nbsp;</TableCell>
209
- </TableRow>
210
- ))}
211
- </TableBody>
212
- </StyledTable>
174
+ </Stack>
175
+ );
176
+ },
177
+ },
178
+ },
179
+ {
180
+ label: t('payment.customer.invoice.unitPrice'),
181
+ name: 'price',
182
+ width: 200,
183
+ align: 'right',
184
+ },
185
+ {
186
+ label: t('common.amount'),
187
+ name: 'amount',
188
+ width: 200,
189
+ align: 'right',
190
+ },
191
+ ...(simple
192
+ ? []
193
+ : [
194
+ {
195
+ label: t('common.actions'),
196
+ name: '',
197
+ width: 40,
198
+ options: {
199
+ customBodyRenderLite: (_: string, index: number) => {
200
+ const item = detail[index] as InvoiceDetailItem;
201
+ return <LineItemActions data={item as any} />;
202
+ },
203
+ },
204
+ },
205
+ ]),
206
+ ];
207
+
208
+ return (
209
+ <Root>
210
+ <Table
211
+ data={detail}
212
+ columns={columns}
213
+ loading={false}
214
+ footer={false}
215
+ toolbar={false}
216
+ components={{
217
+ TableToolbar: () => null,
218
+ TableFooter: () => null,
219
+ }}
220
+ options={{
221
+ count: detail.length,
222
+ page: 0,
223
+ rowsPerPage: 100,
224
+ // responsive: 'simple',
225
+ }}
226
+ dire
227
+ locale={locale}
228
+ mobileTDFlexDirection="row"
229
+ emptyNodeText={t('payment.customer.invoice.emptyList')}
230
+ />
231
+ <Stack
232
+ className="invoice-summary"
233
+ sx={{
234
+ display: 'flex',
235
+ flexDirection: {
236
+ xs: 'column',
237
+ md: 'row',
238
+ },
239
+ justifyContent: 'flex-end',
240
+ alignItems: {
241
+ xs: 'flex-end',
242
+ md: 'center',
243
+ },
244
+ gap: {
245
+ xs: 1,
246
+ md: 3,
247
+ },
248
+ mt: {
249
+ xs: 1,
250
+ md: 2,
251
+ },
252
+ }}>
253
+ {summary.map((line) => (
254
+ <Box key={line.key}>
255
+ <Typography component="span" variant="body1" color="text.secondary">
256
+ {t(line.key)}:&nbsp;
257
+ </Typography>
258
+ <Typography component="span" variant="body1" color="text.primary">
259
+ {line.value}
260
+ </Typography>
261
+ </Box>
262
+ ))}
263
+ </Stack>
213
264
  {state.subscriptionId && state.subscriptionItemId && (
214
265
  <UsageRecordDialog
215
266
  subscriptionId={state.subscriptionId}
@@ -219,16 +270,28 @@ export default function InvoiceTable({ invoice, simple }: Props) {
219
270
  end={invoice.metadata?.usage_end || invoice.period_end}
220
271
  />
221
272
  )}
222
- </Box>
273
+ </Root>
223
274
  );
224
275
  }
225
276
 
226
- const StyledTable = styled(Table)`
227
- .MuiTableCell-root {
228
- padding: 8px 0;
229
- }
230
- `;
231
-
232
277
  InvoiceTable.defaultProps = {
233
278
  simple: false,
234
279
  };
280
+
281
+ const Root = styled(Box)`
282
+ .invoice-summary {
283
+ padding-right: 16px;
284
+ }
285
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
286
+ .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
287
+ > div {
288
+ width: fit-content;
289
+ flex: inherit;
290
+ font-size: 14px;
291
+ }
292
+ }
293
+ .invoice-summary {
294
+ padding-right: 20px;
295
+ }
296
+ }
297
+ `;
@@ -57,7 +57,11 @@ export function Download({ data }: { data: TInvoiceExpanded }) {
57
57
  aria-label="Save PDF"
58
58
  title="Save PDF"
59
59
  className="download-pdf__pdf">
60
- <Button variant="contained" color="primary" size="small">
60
+ <Button
61
+ variant="outlined"
62
+ color="primary"
63
+ size="small"
64
+ sx={{ borderColor: 'var(--stroke-border-base, #EFF1F5)' }}>
61
65
  {t('payment.customer.invoice.download')}
62
66
  </Button>
63
67
  </PDFDownloadLink>
@@ -7,12 +7,13 @@ import { useEffect } from 'react';
7
7
 
8
8
  import { useSessionContext } from '../../contexts/session';
9
9
 
10
- const Root = styled(Dashboard)`
10
+ const Root = styled(Dashboard)<{ padding: string }>`
11
11
  width: 100%;
12
12
  background-color: white;
13
13
 
14
14
  > .dashboard-body > .dashboard-main {
15
15
  > .dashboard-content {
16
+ padding: ${(props) => props.padding || '0 24px'};
16
17
  .MuiTab-root {
17
18
  padding: 0;
18
19
  margin-right: 24px;
@@ -30,7 +31,7 @@ const Root = styled(Dashboard)`
30
31
  }
31
32
 
32
33
  .page-content {
33
- margin-top: 16px;
34
+ margin-top: 2px;
34
35
  }
35
36
 
36
37
  .MuiToggleButton-root {
@@ -49,6 +50,11 @@ const Root = styled(Dashboard)`
49
50
  }
50
51
  }
51
52
  }
53
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
54
+ > .dashboard-body > .dashboard-main > .dashboard-content {
55
+ padding: ${(props) => props.padding || '0 16px'};
56
+ }
57
+ }
52
58
  `;
53
59
 
54
60
  export default function Layout(props: any) {
@@ -2,6 +2,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { Button, CircularProgress, Stack } from '@mui/material';
3
3
  import { isObject } from 'lodash';
4
4
  import type { EventHandler } from 'react';
5
+ import Dialog from '@arcblock/ux/lib/Dialog';
5
6
  import { FormProvider, useForm } from 'react-hook-form';
6
7
 
7
8
  import { isObjectContent } from '../../libs/util';
@@ -21,11 +22,15 @@ export default function MetadataEditor({
21
22
  const { t } = useLocaleContext();
22
23
  const metadata = data.metadata || {};
23
24
  const methods = useForm<any>({
25
+ mode: 'onSubmit',
24
26
  defaultValues: {
25
- metadata: Object.keys(metadata).map((key: string) => ({
26
- key,
27
- value: isObject(metadata[key]) ? JSON.stringify(metadata[key]) : metadata[key],
28
- })),
27
+ metadata:
28
+ Object.keys(metadata).length < 1
29
+ ? [{ key: '', value: '' }]
30
+ : Object.keys(metadata).map((key: string) => ({
31
+ key,
32
+ value: isObject(metadata[key]) ? JSON.stringify(metadata[key]) : metadata[key],
33
+ })),
29
34
  },
30
35
  });
31
36
 
@@ -52,19 +57,21 @@ export default function MetadataEditor({
52
57
  };
53
58
 
54
59
  return (
55
- <FormProvider {...methods}>
56
- <MetadataForm
57
- actions={
58
- <Stack direction="row">
59
- <Button size="small" sx={{ mr: 2 }} onClick={onReset}>
60
- {t('common.cancel')}
61
- </Button>
62
- <Button variant="contained" color="primary" size="small" disabled={loading} onClick={onSubmit}>
63
- {loading && <CircularProgress size="small" />} {t('common.save')}
64
- </Button>
65
- </Stack>
66
- }
67
- />
68
- </FormProvider>
60
+ <Dialog open disableEscapeKeyDown fullWidth onClose={() => onCancel(null)} title={t('common.metadata.edit')}>
61
+ <FormProvider {...methods}>
62
+ <MetadataForm
63
+ actions={
64
+ <Stack direction="row">
65
+ <Button size="small" color="primary" variant="outlined" sx={{ mr: 2 }} onClick={onReset}>
66
+ {t('common.cancel')}
67
+ </Button>
68
+ <Button variant="contained" color="primary" size="small" disabled={loading} onClick={onSubmit}>
69
+ {loading && <CircularProgress size="small" />} {t('common.save')}
70
+ </Button>
71
+ </Stack>
72
+ }
73
+ />
74
+ </FormProvider>
75
+ </Dialog>
69
76
  );
70
77
  }
@@ -1,41 +1,99 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { FormInput } from '@blocklet/payment-react';
3
3
  import { AddOutlined, DeleteOutlineOutlined } from '@mui/icons-material';
4
- import { Box, Button, IconButton, Stack, Typography } from '@mui/material';
4
+ import { Box, Button, Divider, IconButton, Stack, Typography } from '@mui/material';
5
+ import { useEffect, useRef } from 'react';
5
6
  import { useFieldArray, useFormContext } from 'react-hook-form';
6
7
 
7
8
  export default function MetadataForm({ title, actions }: { title?: string; actions?: React.ReactNode }) {
8
9
  const { t } = useLocaleContext();
9
- const { control } = useFormContext();
10
+ const {
11
+ control,
12
+ formState: { errors },
13
+ } = useFormContext();
14
+
10
15
  const metadata = useFieldArray({ control, name: 'metadata' });
16
+ const lastItemRef = useRef<HTMLDivElement | null>(null);
17
+ const errorRef = useRef<HTMLDivElement | null>(null);
18
+ const initRef = useRef(false);
19
+
20
+ useEffect(() => {
21
+ if (!initRef.current) {
22
+ initRef.current = true;
23
+ return;
24
+ }
25
+ if (lastItemRef.current) {
26
+ lastItemRef.current.scrollIntoView({ behavior: 'smooth' });
27
+ }
28
+ }, [metadata.fields.length]);
29
+
30
+ useEffect(() => {
31
+ if (errors.metadata && errorRef.current) {
32
+ errorRef.current.scrollIntoView({ behavior: 'smooth' });
33
+ }
34
+ }, [errors.metadata, errorRef]);
35
+
11
36
  return (
12
37
  <Box sx={{ width: 1 }}>
13
38
  {!!title && <Typography>{title}</Typography>}
14
- {metadata.fields.map((meta, index) => (
15
- <Stack key={meta.id} mt={2} spacing={2} direction="row" alignItems="center">
16
- <Stack direction="row" spacing={2}>
17
- <FormInput
18
- sx={{ flex: 1 }}
19
- size="small"
20
- name={`metadata.${index}.key`}
21
- rules={{ required: t('payment.checkout.required') }}
22
- placeholder="Key"
23
- />
24
- <FormInput
25
- sx={{ flex: 2 }}
26
- size="small"
27
- name={`metadata.${index}.value`}
28
- placeholder="Value"
29
- rules={{ required: t('payment.checkout.required') }}
30
- />
39
+ <Stack
40
+ sx={{
41
+ height: {
42
+ xs: 'calc(100vh - 130px)',
43
+ md: 400,
44
+ },
45
+ minHeight: 400,
46
+ overflow: 'auto',
47
+ pb: 1.5,
48
+ mr: -1.5,
49
+ pr: 1.5,
50
+ }}>
51
+ {metadata.fields.map((meta, index) => (
52
+ <Stack
53
+ key={meta.id}
54
+ mt={2}
55
+ spacing={2}
56
+ direction="row"
57
+ alignItems="flex-end"
58
+ ref={index === metadata.fields.length - 1 ? lastItemRef : null}>
59
+ <Stack direction="row" spacing={2} sx={{ flex: 1 }}>
60
+ <FormInput
61
+ sx={{ flex: 1 }}
62
+ size="small"
63
+ name={`metadata.${index}.key`}
64
+ rules={{ required: t('payment.checkout.required') }}
65
+ placeholder="Key"
66
+ label="Key"
67
+ // @ts-ignore
68
+ ref={errors?.metadata?.[index]?.key ? errorRef : null}
69
+ />
70
+ <FormInput
71
+ sx={{ flex: 2 }}
72
+ size="small"
73
+ name={`metadata.${index}.value`}
74
+ placeholder="Value"
75
+ rules={{ required: t('payment.checkout.required') }}
76
+ label="Value"
77
+ // @ts-ignore
78
+ ref={errors?.metadata?.[index]?.value ? errorRef : null}
79
+ />
80
+ </Stack>
81
+ <IconButton
82
+ onClick={() => metadata.remove(index)}
83
+ sx={{
84
+ border: '1px solid var(--stroke-border-base, #EFF1F5)',
85
+ borderRadius: 'var(--radius-m, 8px)',
86
+ padding: '8px',
87
+ }}>
88
+ <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
89
+ </IconButton>
31
90
  </Stack>
32
- <IconButton size="small" onClick={() => metadata.remove(index)}>
33
- <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
34
- </IconButton>
35
- </Stack>
36
- ))}
91
+ ))}
92
+ </Stack>
93
+
94
+ <Divider />
37
95
  <Stack mt={metadata.fields.length ? 2 : 1} direction="row" justifyContent="space-between">
38
- <Button size="small" variant="outlined" color="inherit" onClick={() => metadata.append({ key: '', value: '' })}>
96
+ <Button size="small" variant="outlined" color="primary" onClick={() => metadata.append({ key: '', value: '' })}>
39
97
  <AddOutlined fontSize="small" /> {t('common.metadata.add')}
40
98
  </Button>
41
99
  {actions}
@@ -1,24 +1,40 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { Typography } from '@mui/material';
2
+ import { Box, Button, Typography } from '@mui/material';
3
3
  import isEmpty from 'lodash/isEmpty';
4
4
  import isObject from 'lodash/isObject';
5
5
 
6
6
  import { isObjectContent } from '../../libs/util';
7
7
  import InfoRow from '../info-row';
8
8
 
9
- export default function MetadataList({ data }: { data: any }) {
9
+ export default function MetadataList({
10
+ data,
11
+ handleEditMetadata = () => {},
12
+ }: {
13
+ data: any;
14
+ handleEditMetadata: () => void;
15
+ }) {
10
16
  const { t } = useLocaleContext();
11
17
 
12
18
  if (isEmpty(data)) {
13
- return <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>;
19
+ return (
20
+ <Box sx={{ textAlign: 'center' }}>
21
+ <Typography color="primary" fontWeight={500}>
22
+ {t('common.metadata.empty')}
23
+ </Typography>
24
+ <Typography color="text.lighter">{t('common.metadata.emptyTip')}</Typography>
25
+ <Button sx={{ color: 'text.link' }} onClick={handleEditMetadata}>
26
+ {t('common.add')}
27
+ </Button>
28
+ </Box>
29
+ );
14
30
  }
15
31
 
16
32
  const formatValue = (value: any) => {
17
33
  if (isObjectContent(value)) {
18
- return <pre>{JSON.stringify(JSON.parse(value), null, 2)}</pre>;
34
+ return <pre style={{ whiteSpace: 'break-spaces' }}>{JSON.stringify(JSON.parse(value), null, 2)}</pre>;
19
35
  }
20
36
  if (isObject(value)) {
21
- return <pre>{JSON.stringify(value, null, 2)}</pre>;
37
+ return <pre style={{ whiteSpace: 'break-spaces' }}>{JSON.stringify(value, null, 2)}</pre>;
22
38
  }
23
39
  return value;
24
40
  };
@@ -27,7 +43,7 @@ export default function MetadataList({ data }: { data: any }) {
27
43
  return (
28
44
  <>
29
45
  {Object.keys(data || {}).map((key) => (
30
- <InfoRow key={key} label={key} value={formatValue(data[key])} />
46
+ <InfoRow key={key} label={key} value={formatValue(data[key])} direction="column" alignItems="flex-start" />
31
47
  ))}
32
48
  </>
33
49
  );