@blocklet/payment-react 1.20.13 → 1.20.15
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.
- package/es/components/source-data-viewer.d.ts +23 -0
- package/es/components/source-data-viewer.js +228 -0
- package/es/history/credit/transactions-list.d.ts +1 -0
- package/es/history/credit/transactions-list.js +137 -20
- package/es/index.d.ts +2 -1
- package/es/index.js +3 -1
- package/es/locales/en.js +2 -1
- package/es/locales/zh.js +2 -1
- package/es/payment/index.js +31 -4
- package/es/payment/success.js +1 -1
- package/es/payment/summary.js +6 -6
- package/lib/components/source-data-viewer.d.ts +23 -0
- package/lib/components/source-data-viewer.js +236 -0
- package/lib/history/credit/transactions-list.d.ts +1 -0
- package/lib/history/credit/transactions-list.js +132 -16
- package/lib/index.d.ts +2 -1
- package/lib/index.js +8 -0
- package/lib/locales/en.js +2 -1
- package/lib/locales/zh.js +2 -1
- package/lib/payment/index.js +31 -4
- package/lib/payment/success.js +1 -1
- package/lib/payment/summary.js +6 -6
- package/package.json +3 -3
- package/src/components/source-data-viewer.tsx +295 -0
- package/src/history/credit/transactions-list.tsx +142 -18
- package/src/index.ts +2 -0
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/payment/index.tsx +32 -4
- package/src/payment/success.tsx +1 -1
- package/src/payment/summary.tsx +7 -7
package/lib/payment/index.js
CHANGED
|
@@ -145,10 +145,7 @@ function PaymentInner({
|
|
|
145
145
|
});
|
|
146
146
|
const currency = (0, _util2.findCurrency)(paymentMethods, currencyId) || settings.baseCurrency;
|
|
147
147
|
const method = paymentMethods.find(x => x.id === currency.payment_method_id);
|
|
148
|
-
|
|
149
|
-
if (onChange) {
|
|
150
|
-
onChange(methods.getValues());
|
|
151
|
-
}
|
|
148
|
+
const recalculatePromotion = () => {
|
|
152
149
|
if (state.checkoutSession?.discounts?.length) {
|
|
153
150
|
_api.default.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
|
|
154
151
|
currency_id: currencyId
|
|
@@ -156,6 +153,12 @@ function PaymentInner({
|
|
|
156
153
|
onPromotionUpdate();
|
|
157
154
|
});
|
|
158
155
|
}
|
|
156
|
+
};
|
|
157
|
+
(0, _react.useEffect)(() => {
|
|
158
|
+
if (onChange) {
|
|
159
|
+
onChange(methods.getValues());
|
|
160
|
+
}
|
|
161
|
+
recalculatePromotion();
|
|
159
162
|
}, [currencyId]);
|
|
160
163
|
const onUpsell = async (from, to) => {
|
|
161
164
|
try {
|
|
@@ -165,6 +168,10 @@ function PaymentInner({
|
|
|
165
168
|
from,
|
|
166
169
|
to
|
|
167
170
|
});
|
|
171
|
+
if (data.discounts?.length) {
|
|
172
|
+
recalculatePromotion();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
168
175
|
setState({
|
|
169
176
|
checkoutSession: data
|
|
170
177
|
});
|
|
@@ -180,6 +187,10 @@ function PaymentInner({
|
|
|
180
187
|
} = await _api.default.put(`/api/checkout-sessions/${state.checkoutSession.id}/downsell`, {
|
|
181
188
|
from
|
|
182
189
|
});
|
|
190
|
+
if (data.discounts?.length) {
|
|
191
|
+
recalculatePromotion();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
183
194
|
setState({
|
|
184
195
|
checkoutSession: data
|
|
185
196
|
});
|
|
@@ -195,6 +206,10 @@ function PaymentInner({
|
|
|
195
206
|
} = await _api.default.put(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`, {
|
|
196
207
|
to
|
|
197
208
|
});
|
|
209
|
+
if (data.discounts?.length) {
|
|
210
|
+
recalculatePromotion();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
198
213
|
setState({
|
|
199
214
|
checkoutSession: data
|
|
200
215
|
});
|
|
@@ -211,6 +226,10 @@ function PaymentInner({
|
|
|
211
226
|
itemId,
|
|
212
227
|
quantity
|
|
213
228
|
});
|
|
229
|
+
if (data.discounts?.length) {
|
|
230
|
+
recalculatePromotion();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
214
233
|
setState({
|
|
215
234
|
checkoutSession: data
|
|
216
235
|
});
|
|
@@ -224,6 +243,10 @@ function PaymentInner({
|
|
|
224
243
|
const {
|
|
225
244
|
data
|
|
226
245
|
} = await _api.default.delete(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`);
|
|
246
|
+
if (data.discounts?.length) {
|
|
247
|
+
recalculatePromotion();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
227
250
|
setState({
|
|
228
251
|
checkoutSession: data
|
|
229
252
|
});
|
|
@@ -243,6 +266,10 @@ function PaymentInner({
|
|
|
243
266
|
priceId,
|
|
244
267
|
amount: (0, _util.fromTokenToUnit)(amount, currency.decimal).toString()
|
|
245
268
|
});
|
|
269
|
+
if (data.discounts?.length) {
|
|
270
|
+
recalculatePromotion();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
246
273
|
setState({
|
|
247
274
|
checkoutSession: data
|
|
248
275
|
});
|
package/lib/payment/success.js
CHANGED
|
@@ -44,7 +44,7 @@ function PaymentSuccess({
|
|
|
44
44
|
const response = await api.get((0, _ufo.joinURL)(prefix, `/api/vendors/order/${sessionId}/status`), {});
|
|
45
45
|
const needCheckError = Date.now() - timerRef.current > 6 * 1e3;
|
|
46
46
|
const allCompleted = response.data?.vendors?.every(vendor => vendor.progress >= 100);
|
|
47
|
-
const hasAnyFailed = response.data?.vendors?.some(vendor => vendor.status === "failed" || needCheckError && !!vendor.error);
|
|
47
|
+
const hasAnyFailed = response.data?.vendors?.some(vendor => vendor.status === "failed" || needCheckError && !!vendor.error && !!vendor.error_message);
|
|
48
48
|
if (hasAnyFailed || allCompleted) {
|
|
49
49
|
clearInterval(interval2);
|
|
50
50
|
}
|
package/lib/payment/summary.js
CHANGED
|
@@ -44,11 +44,11 @@ const ExpandMore = (0, _styles.styled)(props => {
|
|
|
44
44
|
duration: theme.transitions.duration.shortest
|
|
45
45
|
})
|
|
46
46
|
}));
|
|
47
|
-
async function fetchCrossSell(id) {
|
|
47
|
+
async function fetchCrossSell(id, skipError = true) {
|
|
48
48
|
try {
|
|
49
49
|
const {
|
|
50
50
|
data
|
|
51
|
-
} = await _api.default.get(`/api/checkout-sessions/${id}/cross-sell`);
|
|
51
|
+
} = await _api.default.get(`/api/checkout-sessions/${id}/cross-sell?skipError=${skipError}`);
|
|
52
52
|
if (!data.error) {
|
|
53
53
|
return data;
|
|
54
54
|
}
|
|
@@ -127,7 +127,7 @@ function PaymentSummary({
|
|
|
127
127
|
const {
|
|
128
128
|
data,
|
|
129
129
|
runAsync
|
|
130
|
-
} = (0, _ahooks.useRequest)(
|
|
130
|
+
} = (0, _ahooks.useRequest)(skipError => checkoutSessionId ? fetchCrossSell(checkoutSessionId, skipError) : Promise.resolve(null));
|
|
131
131
|
const sessionDiscounts = checkoutSession?.discounts || [];
|
|
132
132
|
const allowPromotionCodes = !!checkoutSession?.allow_promotion_codes;
|
|
133
133
|
const hasDiscounts = sessionDiscounts?.length > 0;
|
|
@@ -174,15 +174,15 @@ function PaymentSummary({
|
|
|
174
174
|
}, []);
|
|
175
175
|
const handleUpsell = async (from, to) => {
|
|
176
176
|
await onUpsell(from, to);
|
|
177
|
-
runAsync();
|
|
177
|
+
runAsync(false);
|
|
178
178
|
};
|
|
179
179
|
const handleQuantityChange = async (itemId, quantity) => {
|
|
180
180
|
await onQuantityChange(itemId, quantity);
|
|
181
|
-
runAsync();
|
|
181
|
+
runAsync(false);
|
|
182
182
|
};
|
|
183
183
|
const handleDownsell = async from => {
|
|
184
184
|
await onDownsell(from);
|
|
185
|
-
runAsync();
|
|
185
|
+
runAsync(false);
|
|
186
186
|
};
|
|
187
187
|
const handleApplyCrossSell = async () => {
|
|
188
188
|
if (data) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.15",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@babel/core": "^7.27.4",
|
|
95
95
|
"@babel/preset-env": "^7.27.2",
|
|
96
96
|
"@babel/preset-react": "^7.27.1",
|
|
97
|
-
"@blocklet/payment-types": "1.20.
|
|
97
|
+
"@blocklet/payment-types": "1.20.15",
|
|
98
98
|
"@storybook/addon-essentials": "^7.6.20",
|
|
99
99
|
"@storybook/addon-interactions": "^7.6.20",
|
|
100
100
|
"@storybook/addon-links": "^7.6.20",
|
|
@@ -125,5 +125,5 @@
|
|
|
125
125
|
"vite-plugin-babel": "^1.3.1",
|
|
126
126
|
"vite-plugin-node-polyfills": "^0.23.0"
|
|
127
127
|
},
|
|
128
|
-
"gitHead": "
|
|
128
|
+
"gitHead": "d205c3b1ec7d2b819e375ed2eb8b70c9d48f0bcb"
|
|
129
129
|
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// React not needed for this component
|
|
2
|
+
import Empty from '@arcblock/ux/lib/Empty';
|
|
3
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
+
import { Box, Chip, Link, Stack, Typography } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
// 定义本地类型,避免依赖问题
|
|
7
|
+
type LocalizedText = {
|
|
8
|
+
zh: string;
|
|
9
|
+
en: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// 简单格式适合大部分业务场景
|
|
13
|
+
type SimpleSourceData = Record<string, string>;
|
|
14
|
+
|
|
15
|
+
// 结构化格式适合复杂场景
|
|
16
|
+
type StructuredSourceDataField = {
|
|
17
|
+
key: string;
|
|
18
|
+
label: string | LocalizedText;
|
|
19
|
+
value: string;
|
|
20
|
+
type?: 'text' | 'image' | 'url';
|
|
21
|
+
url?: string;
|
|
22
|
+
group?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 混合类型:支持渐进式接入
|
|
26
|
+
type SourceData = SimpleSourceData | StructuredSourceDataField[];
|
|
27
|
+
|
|
28
|
+
interface SourceDataViewerProps {
|
|
29
|
+
data: SourceData;
|
|
30
|
+
compact?: boolean;
|
|
31
|
+
maxItems?: number;
|
|
32
|
+
locale?: 'en' | 'zh';
|
|
33
|
+
showGroups?: boolean; // 是否显示分组
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function SourceDataViewer({
|
|
37
|
+
data,
|
|
38
|
+
compact = false,
|
|
39
|
+
maxItems = undefined,
|
|
40
|
+
locale: propLocale = undefined,
|
|
41
|
+
showGroups = false,
|
|
42
|
+
}: SourceDataViewerProps) {
|
|
43
|
+
const { locale: contextLocale, t } = useLocaleContext();
|
|
44
|
+
const currentLocale = propLocale || (contextLocale as 'en' | 'zh') || 'en';
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
!data ||
|
|
48
|
+
(Array.isArray(data) && data.length === 0) ||
|
|
49
|
+
(typeof data === 'object' && Object.keys(data).length === 0)
|
|
50
|
+
) {
|
|
51
|
+
return <Empty>{t('common.none')}</Empty>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const getLocalizedText = (text: string | LocalizedText): string => {
|
|
55
|
+
if (typeof text === 'string') {
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
return text[currentLocale] || text.en || '';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Type guards
|
|
63
|
+
*/
|
|
64
|
+
const isSimpleSourceData = (sourceData: SourceData): sourceData is SimpleSourceData => {
|
|
65
|
+
return typeof sourceData === 'object' && !Array.isArray(sourceData) && sourceData !== null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Auto detect URL pattern
|
|
70
|
+
*/
|
|
71
|
+
const isUrlLike = (str: string): boolean => {
|
|
72
|
+
try {
|
|
73
|
+
// eslint-disable-next-line no-new
|
|
74
|
+
new URL(str);
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return /^https?:\/\/.+/.test(str) || /^www\..+/.test(str);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert any format to normalized structured format with auto URL detection
|
|
83
|
+
*/
|
|
84
|
+
const normalizeData = (inputData: SourceData): StructuredSourceDataField[] => {
|
|
85
|
+
if (isSimpleSourceData(inputData)) {
|
|
86
|
+
return Object.entries(inputData).map(([key, value]) => {
|
|
87
|
+
const stringValue = String(value);
|
|
88
|
+
const isUrl = isUrlLike(stringValue);
|
|
89
|
+
return {
|
|
90
|
+
key,
|
|
91
|
+
label: key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
92
|
+
value: stringValue,
|
|
93
|
+
type: isUrl ? ('url' as const) : ('text' as const),
|
|
94
|
+
url: isUrl ? stringValue : undefined,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return inputData;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Render field value based on type
|
|
103
|
+
*/
|
|
104
|
+
const renderFieldValue = (field: StructuredSourceDataField) => {
|
|
105
|
+
const displayValue = field.value;
|
|
106
|
+
|
|
107
|
+
if (field.type === 'url') {
|
|
108
|
+
return (
|
|
109
|
+
<Link
|
|
110
|
+
href={field.url || field.value}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener noreferrer"
|
|
113
|
+
sx={{
|
|
114
|
+
color: 'primary.main',
|
|
115
|
+
textDecoration: 'none',
|
|
116
|
+
fontSize: '0.85rem',
|
|
117
|
+
fontWeight: 600,
|
|
118
|
+
lineHeight: 1.4,
|
|
119
|
+
'&:hover': {
|
|
120
|
+
textDecoration: 'underline',
|
|
121
|
+
color: 'primary.dark',
|
|
122
|
+
},
|
|
123
|
+
'&:visited': {
|
|
124
|
+
color: 'primary.main',
|
|
125
|
+
},
|
|
126
|
+
}}>
|
|
127
|
+
{displayValue}
|
|
128
|
+
</Link>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (field.type === 'image') {
|
|
133
|
+
return (
|
|
134
|
+
<Box
|
|
135
|
+
sx={{
|
|
136
|
+
display: 'flex',
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
gap: 1,
|
|
139
|
+
}}>
|
|
140
|
+
<Box
|
|
141
|
+
component="img"
|
|
142
|
+
src={field.url || field.value}
|
|
143
|
+
alt={displayValue}
|
|
144
|
+
sx={{
|
|
145
|
+
width: 24,
|
|
146
|
+
height: 24,
|
|
147
|
+
borderRadius: 1,
|
|
148
|
+
objectFit: 'cover',
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
|
152
|
+
{displayValue}
|
|
153
|
+
</Typography>
|
|
154
|
+
</Box>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Typography
|
|
160
|
+
variant="body2"
|
|
161
|
+
sx={{
|
|
162
|
+
fontWeight: 500,
|
|
163
|
+
color: 'text.primary',
|
|
164
|
+
}}>
|
|
165
|
+
{displayValue}
|
|
166
|
+
</Typography>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
!data ||
|
|
172
|
+
(Array.isArray(data) && data.length === 0) ||
|
|
173
|
+
(typeof data === 'object' && Object.keys(data).length === 0)
|
|
174
|
+
) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const normalizedData = normalizeData(data);
|
|
179
|
+
const displayItems = maxItems ? normalizedData.slice(0, maxItems) : normalizedData;
|
|
180
|
+
|
|
181
|
+
if (compact) {
|
|
182
|
+
return (
|
|
183
|
+
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
|
184
|
+
{displayItems.map((field) => (
|
|
185
|
+
<Chip
|
|
186
|
+
key={field.key}
|
|
187
|
+
label={`${getLocalizedText(field.label)}: ${field.value}`}
|
|
188
|
+
size="small"
|
|
189
|
+
variant="outlined"
|
|
190
|
+
sx={{ maxWidth: 200 }}
|
|
191
|
+
/>
|
|
192
|
+
))}
|
|
193
|
+
{maxItems && normalizedData.length > maxItems && (
|
|
194
|
+
<Chip label={`+${normalizedData.length - maxItems} more`} size="small" color="primary" variant="outlined" />
|
|
195
|
+
)}
|
|
196
|
+
</Stack>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (showGroups) {
|
|
201
|
+
// Group by group field
|
|
202
|
+
const groupedData = displayItems.reduce(
|
|
203
|
+
(acc, field) => {
|
|
204
|
+
const group = field.group || 'default';
|
|
205
|
+
if (!acc[group]) acc[group] = [];
|
|
206
|
+
acc[group].push(field);
|
|
207
|
+
return acc;
|
|
208
|
+
},
|
|
209
|
+
{} as Record<string, StructuredSourceDataField[]>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<Stack spacing={2.5}>
|
|
214
|
+
{Object.entries(groupedData).map(([group, fields]) => (
|
|
215
|
+
<Box key={group}>
|
|
216
|
+
{group !== 'default' && (
|
|
217
|
+
<Typography
|
|
218
|
+
variant="subtitle2"
|
|
219
|
+
sx={{
|
|
220
|
+
mb: 1.5,
|
|
221
|
+
fontWeight: 700,
|
|
222
|
+
color: 'text.primary',
|
|
223
|
+
letterSpacing: '0.5px',
|
|
224
|
+
borderBottom: '1px solid',
|
|
225
|
+
borderColor: 'divider',
|
|
226
|
+
pb: 0.5,
|
|
227
|
+
}}>
|
|
228
|
+
{group.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())}
|
|
229
|
+
</Typography>
|
|
230
|
+
)}
|
|
231
|
+
<Stack spacing={1.2} sx={{ pl: group !== 'default' ? 1 : 0 }}>
|
|
232
|
+
{fields.map((field) => (
|
|
233
|
+
<Box
|
|
234
|
+
key={field.key}
|
|
235
|
+
sx={{
|
|
236
|
+
display: 'flex',
|
|
237
|
+
flexDirection: 'row',
|
|
238
|
+
alignItems: 'flex-start',
|
|
239
|
+
gap: 3,
|
|
240
|
+
minHeight: 20,
|
|
241
|
+
}}>
|
|
242
|
+
<Typography
|
|
243
|
+
variant="body2"
|
|
244
|
+
color="text.secondary"
|
|
245
|
+
sx={{
|
|
246
|
+
fontSize: '0.8rem',
|
|
247
|
+
minWidth: 100,
|
|
248
|
+
fontWeight: 600,
|
|
249
|
+
lineHeight: 1.4,
|
|
250
|
+
}}>
|
|
251
|
+
{getLocalizedText(field.label)}
|
|
252
|
+
</Typography>
|
|
253
|
+
<Box sx={{ flex: 1, minWidth: 0, wordBreak: 'break-all', whiteSpace: 'wrap' }}>
|
|
254
|
+
{renderFieldValue(field)}
|
|
255
|
+
</Box>
|
|
256
|
+
</Box>
|
|
257
|
+
))}
|
|
258
|
+
</Stack>
|
|
259
|
+
</Box>
|
|
260
|
+
))}
|
|
261
|
+
</Stack>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Stack spacing={1.5}>
|
|
267
|
+
{displayItems.map((field) => (
|
|
268
|
+
<Box
|
|
269
|
+
key={field.key}
|
|
270
|
+
sx={{
|
|
271
|
+
display: 'flex',
|
|
272
|
+
flexDirection: 'row',
|
|
273
|
+
alignItems: 'flex-start',
|
|
274
|
+
gap: 3,
|
|
275
|
+
minHeight: 20,
|
|
276
|
+
}}>
|
|
277
|
+
<Typography
|
|
278
|
+
variant="body2"
|
|
279
|
+
color="text.secondary"
|
|
280
|
+
sx={{
|
|
281
|
+
fontSize: '0.8rem',
|
|
282
|
+
minWidth: 100,
|
|
283
|
+
fontWeight: 600,
|
|
284
|
+
lineHeight: 1.4,
|
|
285
|
+
}}>
|
|
286
|
+
{getLocalizedText(field.label)}
|
|
287
|
+
</Typography>
|
|
288
|
+
<Box sx={{ flex: 1, minWidth: 0 }}>{renderFieldValue(field)}</Box>
|
|
289
|
+
</Box>
|
|
290
|
+
))}
|
|
291
|
+
</Stack>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default SourceDataViewer;
|