@firecms/data_import_export 3.0.0-canary.80 → 3.0.0-canary.82

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 (50) hide show
  1. package/LICENSE +2 -1
  2. package/dist/index.d.ts +2 -5
  3. package/dist/index.es.js +12 -1364
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +29 -2
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/useImportExportPlugin.d.ts +5 -3
  8. package/package.json +5 -7
  9. package/src/index.ts +2 -5
  10. package/src/useImportExportPlugin.tsx +9 -5
  11. package/dist/components/DataNewPropertiesMapping.d.ts +0 -13
  12. package/dist/components/ImportFileUpload.d.ts +0 -3
  13. package/dist/components/ImportNewPropertyFieldPreview.d.ts +0 -10
  14. package/dist/components/ImportSaveInProgress.d.ts +0 -8
  15. package/dist/components/index.d.ts +0 -4
  16. package/dist/export_import/BasicExportAction.d.ts +0 -7
  17. package/dist/export_import/ExportCollectionAction.d.ts +0 -11
  18. package/dist/export_import/ImportCollectionAction.d.ts +0 -15
  19. package/dist/export_import/export.d.ts +0 -21
  20. package/dist/export_import/index.d.ts +0 -4
  21. package/dist/hooks/index.d.ts +0 -1
  22. package/dist/hooks/useImportConfig.d.ts +0 -2
  23. package/dist/types/column_mapping.d.ts +0 -20
  24. package/dist/types/index.d.ts +0 -1
  25. package/dist/utils/data.d.ts +0 -4
  26. package/dist/utils/file_headers.d.ts +0 -1
  27. package/dist/utils/file_to_json.d.ts +0 -16
  28. package/dist/utils/get_import_inference_type.d.ts +0 -2
  29. package/dist/utils/get_properties_mapping.d.ts +0 -0
  30. package/dist/utils/index.d.ts +0 -3
  31. package/src/components/DataNewPropertiesMapping.tsx +0 -241
  32. package/src/components/ImportFileUpload.tsx +0 -42
  33. package/src/components/ImportNewPropertyFieldPreview.tsx +0 -60
  34. package/src/components/ImportSaveInProgress.tsx +0 -122
  35. package/src/components/index.ts +0 -4
  36. package/src/export_import/BasicExportAction.tsx +0 -147
  37. package/src/export_import/ExportCollectionAction.tsx +0 -288
  38. package/src/export_import/ImportCollectionAction.tsx +0 -442
  39. package/src/export_import/export.ts +0 -234
  40. package/src/export_import/index.ts +0 -4
  41. package/src/hooks/index.ts +0 -1
  42. package/src/hooks/useImportConfig.tsx +0 -34
  43. package/src/types/column_mapping.ts +0 -32
  44. package/src/types/index.ts +0 -1
  45. package/src/utils/data.ts +0 -133
  46. package/src/utils/file_headers.ts +0 -90
  47. package/src/utils/file_to_json.ts +0 -106
  48. package/src/utils/get_import_inference_type.ts +0 -27
  49. package/src/utils/get_properties_mapping.ts +0 -63
  50. package/src/utils/index.ts +0 -3
@@ -1,241 +0,0 @@
1
- import { getPropertyInPath, PropertiesOrBuilders, Property } from "@firecms/core";
2
- import {
3
- BooleanSwitchWithLabel,
4
- ChevronRightIcon,
5
- DateTimeField,
6
- ExpandablePanel,
7
- Select,
8
- SelectItem,
9
- Table,
10
- TableBody,
11
- TableCell,
12
- TableHeader,
13
- TableRow,
14
- TextField,
15
- Typography
16
- } from "@firecms/ui";
17
- import { ImportConfig } from "../types";
18
- import { getIn, setIn } from "@firecms/formex";
19
-
20
- export interface DataPropertyMappingProps {
21
- importConfig: ImportConfig;
22
- destinationProperties: Record<string, Property>;
23
- buildPropertyView?: (props: {
24
- isIdColumn: boolean,
25
- property: Property | null,
26
- propertyKey: string | null,
27
- importKey: string
28
- }) => React.ReactNode;
29
- }
30
-
31
- export function DataNewPropertiesMapping({
32
- importConfig,
33
- destinationProperties,
34
- buildPropertyView
35
- }: DataPropertyMappingProps) {
36
-
37
- const headersMapping = importConfig.headersMapping;
38
- const headingsOrder = importConfig.headingsOrder;
39
- const idColumn = importConfig.idColumn;
40
- const originProperties = importConfig.originProperties;
41
-
42
- return (
43
- <>
44
-
45
- <IdSelectField idColumn={idColumn}
46
- headersMapping={headersMapping}
47
- onChange={(value) => importConfig.setIdColumn(value ?? undefined)}/>
48
-
49
- <div className={"h-4"}/>
50
-
51
- <Table style={{
52
- tableLayout: "fixed"
53
- }}>
54
- <TableHeader>
55
- <TableCell header={true} style={{ width: "20%" }}>
56
- Column in file
57
- </TableCell>
58
- <TableCell header={true}>
59
- </TableCell>
60
- <TableCell header={true} style={{ width: "75%" }}>
61
- Map to Property
62
- </TableCell>
63
- </TableHeader>
64
- <TableBody>
65
- {destinationProperties &&
66
- headingsOrder.map((importKey) => {
67
- const mappedKey = headersMapping[importKey];
68
- const propertyKey = headersMapping[importKey];
69
- const property = mappedKey ? getPropertyInPath(destinationProperties, mappedKey) as Property : null;
70
-
71
- const originProperty = getPropertyInPath(originProperties, importKey) as Property | undefined;
72
- const originDataType = originProperty
73
- ? (originProperty.dataType === "array" && typeof originProperty.of === "object"
74
- ? `${originProperty.dataType} - ${(originProperty.of as Property).dataType}`
75
- : originProperty.dataType)
76
- : undefined;
77
- return <TableRow key={importKey} style={{ height: "90px" }}>
78
- <TableCell style={{ width: "20%" }}>
79
- <Typography variant={"body2"}>{importKey}</Typography>
80
- {originProperty && <Typography
81
- variant={"caption"}
82
- color={"secondary"}
83
- >{originDataType}</Typography>}
84
- </TableCell>
85
- <TableCell>
86
- <ChevronRightIcon/>
87
- </TableCell>
88
- <TableCell className={importKey === idColumn ? "text-center" : undefined}
89
- style={{ width: "75%" }}>
90
- {buildPropertyView?.({
91
- isIdColumn: importKey === idColumn,
92
- property,
93
- propertyKey,
94
- importKey
95
- })}
96
- </TableCell>
97
- </TableRow>;
98
- }
99
- )}
100
- </TableBody>
101
- </Table>
102
-
103
- <ExpandablePanel title="Default values" initiallyExpanded={false} className={"p-4 mt-4"}>
104
-
105
- <div className={"text-sm text-slate-500 dark:text-slate-300 font-medium ml-3.5 mb-1"}>
106
- You can select a default value for unmapped columns and empty values:
107
- </div>
108
- <Table style={{
109
- tableLayout: "fixed"
110
- }}>
111
- <TableHeader>
112
- <TableCell header={true} style={{ width: "30%" }}>
113
- Property
114
- </TableCell>
115
- <TableCell header={true}>
116
- </TableCell>
117
- <TableCell header={true} style={{ width: "65%" }}>
118
- Default value
119
- </TableCell>
120
- </TableHeader>
121
- <TableBody>
122
- {destinationProperties &&
123
- getAllPropertyKeys(destinationProperties).map((key) => {
124
- const property = getPropertyInPath(destinationProperties, key);
125
- if (typeof property !== "object" || property === null) {
126
- return null;
127
- }
128
- if (!["number", "string", "boolean", "map"].includes(property.dataType)) {
129
- return null;
130
- }
131
- return <TableRow key={key} style={{ height: "70px" }}>
132
- <TableCell style={{ width: "20%" }}>
133
- <Typography variant={"body2"}>{key}</Typography>
134
- </TableCell>
135
- <TableCell>
136
- <ChevronRightIcon/>
137
- </TableCell>
138
- <TableCell className={key === idColumn ? "text-center" : undefined}
139
- style={{ width: "75%" }}>
140
- <DefaultValuesField property={property}
141
- defaultValue={getIn(importConfig.defaultValues, key)}
142
- onValueChange={(value) => {
143
- const newValues = setIn(importConfig.defaultValues, key, value);
144
- importConfig.setDefaultValues(newValues);
145
- }}/>
146
- </TableCell>
147
- </TableRow>;
148
- }
149
- )}
150
- </TableBody>
151
- </Table>
152
- </ExpandablePanel>
153
- </>
154
- );
155
- }
156
-
157
- function getAllPropertyKeys(properties: PropertiesOrBuilders, currentKey?: string): string[] {
158
- return Object.entries(properties).reduce((acc, [key, property]) => {
159
- const accumulatedKey = currentKey ? `${currentKey}.${key}` : key;
160
- if (typeof property !== "function" && property.dataType === "map" && property.properties) {
161
- const childProperties = getAllPropertyKeys(property.properties, accumulatedKey);
162
- return [...acc, ...childProperties];
163
- }
164
- return [...acc, accumulatedKey];
165
- }, [] as string[]);
166
- }
167
-
168
- function IdSelectField({
169
- idColumn,
170
- headersMapping,
171
- onChange
172
- }: {
173
- idColumn?: string,
174
- headersMapping: Record<string, string | null>;
175
- onChange: (value: string | null) => void
176
- }) {
177
- return <div>
178
- <Select
179
- size={"small"}
180
- value={idColumn ?? ""}
181
- onChange={(event) => {
182
- const value = event.target.value;
183
- onChange(value === "__none__" ? null : value);
184
- }}
185
- placeholder={"Autogenerate ID"}
186
- renderValue={(value) => {
187
- return <Typography variant={"body2"}>
188
- {value !== "__none__" ? value : "Autogenerate ID"}
189
- </Typography>;
190
- }}
191
- label={"Column that will be used as ID for each document"}>
192
- <SelectItem value={"__none__"}>Autogenerate ID</SelectItem>
193
- {Object.entries(headersMapping).map(([key, value]) => {
194
- return <SelectItem key={key} value={key}>{key}</SelectItem>;
195
- })}
196
- </Select>
197
- </div>;
198
- }
199
-
200
- function DefaultValuesField({
201
- property,
202
- onValueChange,
203
- defaultValue
204
- }: { property: Property, onValueChange: (value: any) => void, defaultValue?: any }) {
205
- if (property.dataType === "string") {
206
- return <TextField size={"small"}
207
- placeholder={"Default value"}
208
- value={defaultValue ?? ""}
209
- onChange={(event) => onValueChange(event.target.value)}/>;
210
- } else if (property.dataType === "number") {
211
- return <TextField size={"small"}
212
- type={"number"}
213
- value={defaultValue ?? ""}
214
- placeholder={"Default value"}
215
- onChange={(event) => onValueChange(event.target.value)}/>;
216
- } else if (property.dataType === "boolean") {
217
- return <BooleanSwitchWithLabel
218
- value={defaultValue ?? null}
219
- allowIndeterminate={true}
220
- size={"small"}
221
- onValueChange={(v: boolean | null) => onValueChange(v === null ? undefined : v)}
222
- label={defaultValue === undefined
223
- ? "Do not set value"
224
- : defaultValue === true
225
- ? "Set value to true"
226
- : "Set value to false"}
227
- />
228
- } else if (property.dataType === "date") {
229
- return <DateTimeField
230
- mode={property.mode ?? "date"}
231
- size={"small"}
232
- value={defaultValue ?? undefined}
233
- onChange={(dateValue: Date | undefined) => {
234
- onValueChange(dateValue);
235
- }}
236
- clearable={true}
237
- />
238
- }
239
-
240
- return null;
241
- }
@@ -1,42 +0,0 @@
1
- import { FileUpload, UploadIcon } from "@firecms/ui";
2
- import { convertFileToJson } from "../utils/file_to_json";
3
- import { useSnackbarController } from "@firecms/core";
4
-
5
- export function ImportFileUpload({ onDataAdded }: {
6
- onDataAdded: (data: object[], propertiesOrder?: string[]) => void
7
- }) {
8
- const snackbarController = useSnackbarController();
9
- return <FileUpload
10
- accept={{
11
- "text/*": [".csv", ".xls", ".xlsx"],
12
- "application/vnd.ms-excel": [".xls", ".xlsx"],
13
- "application/msexcel": [".xls", ".xlsx"],
14
- "application/vnd.ms-office": [".xls", ".xlsx"],
15
- "application/xls": [".xls", ".xlsx"],
16
- "application/x-xls": [".xls", ".xlsx"],
17
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xls", ".xlsx"],
18
- "application/json": [".json"],
19
- }}
20
- preventDropOnDocument={true}
21
- size={"small"}
22
- maxFiles={1}
23
- uploadDescription={<><UploadIcon/>Drag and drop a file here or click to upload</>}
24
- onFilesAdded={(files: File[]) => {
25
- if (files.length > 0) {
26
- convertFileToJson(files[0])
27
- .then(({
28
- data,
29
- propertiesOrder
30
- }) => {
31
- onDataAdded(data, propertiesOrder);
32
- })
33
- .catch((error) => {
34
- console.error("Error parsing file", error);
35
- snackbarController.open({
36
- type: "error",
37
- message: error.message
38
- });
39
- });
40
- }
41
- }}/>
42
- }
@@ -1,60 +0,0 @@
1
- import React from "react";
2
- import {
3
- ErrorBoundary,
4
- getFieldConfig,
5
- Property,
6
- PropertyConfigBadge,
7
- useCustomizationController
8
- } from "@firecms/core";
9
- import { EditIcon, IconButton, TextField, } from "@firecms/ui";
10
-
11
- export function ImportNewPropertyFieldPreview({
12
- propertyKey,
13
- property,
14
- onEditClick,
15
- includeName = true,
16
- onPropertyNameChanged,
17
- propertyTypeView
18
- }: {
19
- propertyKey: string | null,
20
- property: Property | null
21
- includeName?: boolean,
22
- onEditClick?: () => void,
23
- onPropertyNameChanged?: (propertyKey: string, value: string) => void,
24
- propertyTypeView?: React.ReactNode
25
- }) {
26
-
27
- const { propertyConfigs } = useCustomizationController();
28
- const widget = property ? getFieldConfig(property, propertyConfigs) : null;
29
-
30
- return <ErrorBoundary>
31
- <div
32
- className="flex flex-row w-full items-center">
33
-
34
- <div className={"mx-4"}>
35
- {propertyTypeView ?? <PropertyConfigBadge propertyConfig={widget ?? undefined}/>}
36
- </div>
37
-
38
- <div className="w-full flex flex-col grow">
39
-
40
- <div className={"flex flex-row items-center gap-2"}>
41
- {includeName &&
42
- <TextField
43
- size={"small"}
44
- className={"text-base grow"}
45
- value={property?.name ?? ""}
46
- onChange={(e) => {
47
- if (onPropertyNameChanged && propertyKey)
48
- onPropertyNameChanged(propertyKey, e.target.value);
49
- }}/>}
50
-
51
- <IconButton onClick={onEditClick} size={"small"}>
52
- <EditIcon size={"small"}/>
53
- </IconButton>
54
- </div>
55
-
56
- </div>
57
-
58
- </div>
59
- </ErrorBoundary>
60
- }
@@ -1,122 +0,0 @@
1
- import { DataSource, Entity, EntityCollection, useDataSource } from "@firecms/core";
2
- import { Button, CenteredView, CircularProgress, Typography, } from "@firecms/ui";
3
- import { useEffect, useRef, useState } from "react";
4
- import { ImportConfig } from "../types";
5
-
6
- export function ImportSaveInProgress<C extends EntityCollection>
7
- ({
8
- path,
9
- importConfig,
10
- collection,
11
- onImportSuccess
12
- }:
13
- {
14
- path: string,
15
- importConfig: ImportConfig,
16
- collection: C,
17
- onImportSuccess: (collection: C) => void
18
- }) {
19
-
20
- const [errorSaving, setErrorSaving] = useState<Error | undefined>(undefined);
21
- const dataSource = useDataSource();
22
-
23
- const savingRef = useRef<boolean>(false);
24
-
25
- const [processedEntities, setProcessedEntities] = useState<number>(0);
26
-
27
- function save() {
28
-
29
- if (savingRef.current)
30
- return;
31
-
32
- savingRef.current = true;
33
-
34
- saveDataBatch(
35
- dataSource,
36
- collection,
37
- path,
38
- importConfig.entities,
39
- 0,
40
- 25,
41
- setProcessedEntities
42
- ).then(() => {
43
- onImportSuccess(collection);
44
- savingRef.current = false;
45
- }).catch((e) => {
46
- setErrorSaving(e);
47
- savingRef.current = false;
48
- });
49
- }
50
-
51
- useEffect(() => {
52
- save();
53
- }, []);
54
-
55
- if (errorSaving) {
56
- return (
57
- <CenteredView className={"flex flex-col gap-4 items-center"}>
58
- <Typography variant={"h6"}>
59
- Error saving data
60
- </Typography>
61
-
62
- <Typography variant={"body2"} color={"error"}>
63
- {errorSaving.message}
64
- </Typography>
65
- <Button
66
- onClick={save}
67
- variant={"outlined"}>
68
- Retry
69
- </Button>
70
- </CenteredView>
71
- );
72
- }
73
-
74
- return (
75
- <CenteredView className={"flex flex-col gap-4 items-center"}>
76
- <CircularProgress/>
77
-
78
- <Typography variant={"h6"}>
79
- Saving data
80
- </Typography>
81
-
82
- <Typography variant={"body2"}>
83
- {processedEntities}/{importConfig.entities.length} entities saved
84
- </Typography>
85
-
86
- <Typography variant={"caption"}>
87
- Do not close this tab or the import will be interrupted.
88
- </Typography>
89
-
90
- </CenteredView>
91
- );
92
-
93
- }
94
-
95
- function saveDataBatch(dataSource: DataSource,
96
- collection: EntityCollection,
97
- path: string,
98
- data: Partial<Entity<any>>[],
99
- offset = 0,
100
- batchSize = 25,
101
- onProgressUpdate: (progress: number) => void): Promise<void> {
102
-
103
- console.debug("Saving imported data", offset, batchSize);
104
-
105
- const batch = data.slice(offset, offset + batchSize);
106
- return Promise.all(batch.map(d =>
107
- dataSource.saveEntity({
108
- path,
109
- values: d.values,
110
- entityId: d.id,
111
- collection,
112
- status: "new"
113
- })))
114
- .then(() => {
115
- if (offset + batchSize < data.length) {
116
- onProgressUpdate(offset + batchSize);
117
- return saveDataBatch(dataSource, collection, path, data, offset + batchSize, batchSize, onProgressUpdate);
118
- }
119
- onProgressUpdate(data.length);
120
- return Promise.resolve();
121
- });
122
- }
@@ -1,4 +0,0 @@
1
- export * from "./DataNewPropertiesMapping";
2
- export * from "./ImportFileUpload";
3
- export * from "./ImportNewPropertyFieldPreview";
4
- export * from "./ImportSaveInProgress";
@@ -1,147 +0,0 @@
1
- import React, { useCallback } from "react";
2
-
3
- import { Entity, ResolvedProperties } from "@firecms/core";
4
- import {
5
- BooleanSwitchWithLabel,
6
- Button,
7
- cls,
8
- Dialog,
9
- DialogActions,
10
- DialogContent,
11
- focusedMixin,
12
- GetAppIcon,
13
- IconButton,
14
- Tooltip,
15
- Typography
16
- } from "@firecms/ui";
17
- import { downloadEntitiesExport } from "./export";
18
-
19
- export type BasicExportActionProps = {
20
- data: Entity<any>[];
21
- properties: ResolvedProperties;
22
- propertiesOrder?: string[];
23
- }
24
-
25
- export function BasicExportAction({
26
- data,
27
- properties,
28
- propertiesOrder
29
- }: BasicExportActionProps) {
30
-
31
- const dateRef = React.useRef<Date>(new Date());
32
- const [flattenArrays, setFlattenArrays] = React.useState<boolean>(true);
33
- const [exportType, setExportType] = React.useState<"csv" | "json">("csv");
34
- const [dateExportType, setDateExportType] = React.useState<"timestamp" | "string">("string");
35
-
36
- const [open, setOpen] = React.useState(false);
37
-
38
- const handleClickOpen = useCallback(() => {
39
- setOpen(true);
40
- }, [setOpen]);
41
-
42
- const handleClose = useCallback(() => {
43
- setOpen(false);
44
- }, [setOpen]);
45
-
46
- const onOkClicked = useCallback(() => {
47
- downloadEntitiesExport({
48
- data,
49
- additionalData: [],
50
- properties,
51
- propertiesOrder,
52
- name: "export.csv",
53
- flattenArrays,
54
- additionalHeaders: [],
55
- exportType,
56
- dateExportType
57
- });
58
- handleClose();
59
- }, []);
60
-
61
- return <>
62
-
63
- <Tooltip title={"Export"}>
64
- <IconButton color={"primary"} onClick={handleClickOpen}>
65
- <GetAppIcon/>
66
- </IconButton>
67
- </Tooltip>
68
-
69
- <Dialog
70
- open={open}
71
- onOpenChange={setOpen}
72
- maxWidth={"xl"}>
73
- <DialogContent className={"flex flex-col gap-4 my-4"}>
74
-
75
- <Typography variant={"h6"}>Export data</Typography>
76
-
77
- <div>Download the the content of this table as a CSV</div>
78
-
79
- <div className={"flex flex-row gap-4"}>
80
- <div className={"p-4 flex flex-col"}>
81
- <div className="flex items-center">
82
- <input id="radio-csv" type="radio" value="csv" name="exportType"
83
- checked={exportType === "csv"}
84
- onChange={() => setExportType("csv")}
85
- className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
86
- <label htmlFor="radio-csv"
87
- className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">CSV</label>
88
- </div>
89
- <div className="flex items-center">
90
- <input id="radio-json" type="radio" value="json" name="exportType"
91
- checked={exportType === "json"}
92
- onChange={() => setExportType("json")}
93
- className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
94
- <label htmlFor="radio-json"
95
- className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">JSON</label>
96
- </div>
97
- </div>
98
-
99
- <div className={"p-4 flex flex-col"}>
100
- <div className="flex items-center">
101
- <input id="radio-timestamp" type="radio" value="timestamp" name="dateExportType"
102
- checked={dateExportType === "timestamp"}
103
- onChange={() => setDateExportType("timestamp")}
104
- className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
105
- <label htmlFor="radio-timestamp"
106
- className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
107
- timestamps ({dateRef.current.getTime()})</label>
108
- </div>
109
- <div className="flex items-center">
110
- <input id="radio-string" type="radio" value="string" name="dateExportType"
111
- checked={dateExportType === "string"}
112
- onChange={() => setDateExportType("string")}
113
- className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
114
- <label htmlFor="radio-string"
115
- className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
116
- strings ({dateRef.current.toISOString()})</label>
117
- </div>
118
- </div>
119
- </div>
120
-
121
- <BooleanSwitchWithLabel
122
- size={"small"}
123
- disabled={exportType !== "csv"}
124
- value={flattenArrays}
125
- onValueChange={setFlattenArrays}
126
- label={"Flatten arrays"}/>
127
-
128
- </DialogContent>
129
-
130
- <DialogActions>
131
-
132
- <Button onClick={handleClose}
133
- variant={"text"}>
134
- Cancel
135
- </Button>
136
-
137
- <Button variant="filled"
138
- onClick={onOkClicked}>
139
- Download
140
- </Button>
141
-
142
- </DialogActions>
143
-
144
- </Dialog>
145
-
146
- </>;
147
- }