diff --git a/admin-server/docs/docs.go b/admin-server/docs/docs.go index 546df409..cab76ebf 100644 --- a/admin-server/docs/docs.go +++ b/admin-server/docs/docs.go @@ -328,7 +328,7 @@ const docTemplate = `{ "schema": { "type": "object", "additionalProperties": { - "type": "string" + "$ref": "#/definitions/types.UploadColumnMapping" } } } @@ -787,6 +787,15 @@ const docTemplate = `{ } } }, + "types.UploadColumnMapping": { + "type": "object", + "properties": { + "template_column_id": { + "type": "string", + "example": "a1ed136d-33ce-4b7e-a7a4-8a5ccfe54cd5" + } + } + }, "types.UploadHeaderRowSelection": { "type": "object", "properties": { diff --git a/admin-server/docs/swagger.json b/admin-server/docs/swagger.json index 745db97e..1040e9fe 100644 --- a/admin-server/docs/swagger.json +++ b/admin-server/docs/swagger.json @@ -321,7 +321,7 @@ "schema": { "type": "object", "additionalProperties": { - "type": "string" + "$ref": "#/definitions/types.UploadColumnMapping" } } } @@ -780,6 +780,15 @@ } } }, + "types.UploadColumnMapping": { + "type": "object", + "properties": { + "template_column_id": { + "type": "string", + "example": "a1ed136d-33ce-4b7e-a7a4-8a5ccfe54cd5" + } + } + }, "types.UploadHeaderRowSelection": { "type": "object", "properties": { diff --git a/admin-server/docs/swagger.yaml b/admin-server/docs/swagger.yaml index 95accf89..3ff4b5b1 100644 --- a/admin-server/docs/swagger.yaml +++ b/admin-server/docs/swagger.yaml @@ -260,6 +260,12 @@ definitions: example: a1ed136d-33ce-4b7e-a7a4-8a5ccfe54cd5 type: string type: object + types.UploadColumnMapping: + properties: + template_column_id: + example: a1ed136d-33ce-4b7e-a7a4-8a5ccfe54cd5 + type: string + type: object types.UploadHeaderRowSelection: properties: index: @@ -518,7 +524,7 @@ paths: required: true schema: additionalProperties: - type: string + $ref: '#/definitions/types.UploadColumnMapping' type: object responses: "200": diff --git a/admin-server/go/pkg/types/importer.go b/admin-server/go/pkg/types/importer.go index d4297e4c..9e148c10 100644 --- a/admin-server/go/pkg/types/importer.go +++ b/admin-server/go/pkg/types/importer.go @@ -79,6 +79,10 @@ type UploadHeaderRowSelection struct { Index *int `json:"index" example:"0"` } +type UploadColumnMapping struct { + TemplateColumnID string `json:"template_column_id" example:"a1ed136d-33ce-4b7e-a7a4-8a5ccfe54cd5"` +} + type UploadRow struct { Index int `json:"index" example:"0"` Values map[int]string `json:"values"` diff --git a/admin-server/go/pkg/web/file_import_routes.go b/admin-server/go/pkg/web/file_import_routes.go index 8ba39ba7..f7122a27 100644 --- a/admin-server/go/pkg/web/file_import_routes.go +++ b/admin-server/go/pkg/web/file_import_routes.go @@ -344,8 +344,8 @@ func importerSetHeaderRow(c *gin.Context) { // @Success 200 {object} types.Res // @Failure 400 {object} types.Res // @Router /file-import/v1/upload/{id}/set-column-mapping [post] -// @Param id path string true "Upload ID" -// @Param body body map[string]string true "Request body" +// @Param id path string true "Upload ID" +// @Param body body map[string]types.UploadColumnMapping true "Request body" func importerSetColumnMapping(c *gin.Context) { id := c.Param("id") if len(id) == 0 { @@ -353,14 +353,19 @@ func importerSetColumnMapping(c *gin.Context) { return } - // Non-schemaless: Upload column ID -> Template column ID - // Schemaless: Upload column ID -> User-provided key (i.e. first_name) (only from the request, this will be updated to IDs after the template is generated) - columnMapping := make(map[string]string) - if err := c.ShouldBindJSON(&columnMapping); err != nil { + columnMappingRequest := make(map[string]types.UploadColumnMapping) + if err := c.ShouldBindJSON(&columnMappingRequest); err != nil { tf.Log.Warnw("Could not bind JSON", "error", err) c.AbortWithStatusJSON(http.StatusBadRequest, types.Res{Err: err.Error()}) return } + + // Non-schemaless: Upload column ID -> Template column ID + // Schemaless: Upload column ID -> User-provided key (i.e. first_name) (only from the request, this will be updated to IDs after the template is generated) + columnMapping := make(map[string]string) + for k, mappingSelection := range columnMappingRequest { + columnMapping[k] = mappingSelection.TemplateColumnID + } if len(columnMapping) == 0 { c.AbortWithStatusJSON(http.StatusBadRequest, types.Res{Err: "Please select at least one destination column"}) return diff --git a/docs/api-reference/download-import.mdx b/docs/api-reference/download-import.mdx index 98d0467d..a58cda65 100644 --- a/docs/api-reference/download-import.mdx +++ b/docs/api-reference/download-import.mdx @@ -2,3 +2,5 @@ title: "Download Import CSV" openapi: "GET /import/{id}/download" --- + +Retrieve an import as a CSV file. diff --git a/docs/api-reference/get-import-rows.mdx b/docs/api-reference/get-import-rows.mdx index 0771e3bb..17d1c097 100644 --- a/docs/api-reference/get-import-rows.mdx +++ b/docs/api-reference/get-import-rows.mdx @@ -3,8 +3,31 @@ title: "Get Import Rows" openapi: "GET /import/{id}/rows" --- -Retrieve the rows of an import as JSON. This endpoint supports pagination by using a limit/offset. If the limit and offset are not provided, it will return the first 1000 rows of the import. +Retrieve the rows of an import as JSON. This endpoint supports pagination by using a limit/offset. If the limit and offset are not provided, it will return the first 1,000 rows of the import. To use the limit/offset, start by setting the offset to 0 and the limit to 100 to get the first 100 rows of data. To get the next 100 rows, set the offset to 100 while keeping the limit the same. Continue increasing the offset by 100 until no more rows are returned. -Note: the max limit is 1000. +The maximum `limit` is 1000. + +### Example Response + +```json +[ + { + "index": 0, + "values": { + "age": 23, + "email": "maria@example.com", + "first_name": "Maria" + } + }, + { + "index": 1, + "values": { + "age": 32, + "email": "robert@example.com", + "first_name": "Robert" + } + } +] +``` diff --git a/docs/api-reference/get-import.mdx b/docs/api-reference/get-import.mdx index 3c1751cd..6a40c8cb 100644 --- a/docs/api-reference/get-import.mdx +++ b/docs/api-reference/get-import.mdx @@ -1,4 +1,63 @@ --- -title: "Get Import Metadata" +title: "Get Import" openapi: "GET /import/{id}" --- + +Retrieve the row data, column definitions, and other information about the import. + +The number of rows included is limited to 10,000. If there are more than 10,000 rows, an `error` will be set and + the data should be retrieved using the [/rows](/api-reference/get-import-rows) endpoint. + +### Example Response + +```json +{ + "id": "da5554e3-6c87-41b2-9366-5449a2f15b53", + "importer_id": "a0fadb1d-9888-4fcb-b185-25b984bcb227", + "num_rows": 2, + "num_columns": 3, + "num_processed_values": 5, + "metadata": { + "user_id": 1234, + "user_email": "user@example.com", + "environment": "staging" + }, + "created_at": 1698172312, + "error": null, + "columns": [ + { + "data_type": "number", + "key": "age", + "name": "Age" + }, + { + "data_type": "string", + "key": "email", + "name": "Email" + }, + { + "data_type": "string", + "key": "first_name", + "name": "First Name" + } + ], + "rows": [ + { + "index": 0, + "values": { + "age": 23, + "email": "maria@example.com", + "first_name": "Maria" + } + }, + { + "index": 1, + "values": { + "age": 32, + "email": "robert@example.com", + "first_name": "Robert" + } + } + ] +} +``` diff --git a/docs/assets/webhooks-transformations-filter.jpg b/docs/assets/webhooks-transformations-filter.jpg new file mode 100644 index 00000000..b88a1b44 Binary files /dev/null and b/docs/assets/webhooks-transformations-filter.jpg differ diff --git a/docs/assets/webhooks-transformations-transform.jpg b/docs/assets/webhooks-transformations-transform.jpg new file mode 100644 index 00000000..bf49a0ac Binary files /dev/null and b/docs/assets/webhooks-transformations-transform.jpg differ diff --git a/docs/assets/webhooks-transformations.jpg b/docs/assets/webhooks-transformations.jpg new file mode 100644 index 00000000..cfb64868 Binary files /dev/null and b/docs/assets/webhooks-transformations.jpg differ diff --git a/docs/mint.json b/docs/mint.json index d6a053a3..a8c878dd 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -92,8 +92,8 @@ { "group": "API", "pages": [ - "api-reference/get-import-rows", "api-reference/get-import", + "api-reference/get-import-rows", "api-reference/download-import", "api-reference/create-importer", "api-reference/delete-importer" diff --git a/docs/retrieve-data.mdx b/docs/retrieve-data.mdx index 977ceb7d..dd12f808 100644 --- a/docs/retrieve-data.mdx +++ b/docs/retrieve-data.mdx @@ -16,14 +16,16 @@ onComplete: (data) => console.log(data) Feature available in TableFlow Cloud. -TableFlow provides an API to paginate the row data of an import, view metadata about an import, or download an import directly as a CSV. +TableFlow can send webhooks to your application when an import has been completed. The webhook will contain the row data, column definitions, and other information about the import. +- [Webhook Setup Guide](/webhooks) + +TableFlow also provides an API to retrieve import data, paginate the row data of an import (used for large imports), or download an import directly as a CSV. + +- [Get Import](/api-reference/get-import) - [Get Import Rows](/api-reference/get-import-rows) -- [Get Import Metadata](/api-reference/get-import) - [Download Import CSV](/api-reference/download-import) -You can also use [Webhooks](/webhooks) to get notified when an import has been completed. This allows your backend application to load the imported data as soon as an import has completed. - ### Option 3: Admin Dashboard diff --git a/docs/sdk/javascript.mdx b/docs/sdk/javascript.mdx index 375e0b18..97c6d803 100644 --- a/docs/sdk/javascript.mdx +++ b/docs/sdk/javascript.mdx @@ -302,68 +302,62 @@ uploadButton.addEventListener("click", () => { - Callback function that fires when a user completes an import. It returns `data`, an object that contains the row data - and information about the import such as the number of rows. The number of rows returned is limited to 10,000. If - there are more than 10,000 rows, an `error` will be set and the data should be retrieved using the - [API](/api-reference/get-import-rows). + Callback function that fires when a user completes an import. It returns `data`, an object that contains the row data, + column definitions, and other information about the import. + + The number of rows included is limited to 10,000. If there are more than 10,000 rows, an `error` will be set and + the data should be retrieved using the [API](/api-reference/get-import-rows). + ```jsx onComplete={(data) => console.log(data)} ``` Example `data`: ```json { - "id": "170f9ae1-c109-4e26-83a1-b31f2baa81b2", - "upload_id": "4f7ec0b5-16ef-4d0e-8b6a-0c182815a131", + "id": "da5554e3-6c87-41b2-9366-5449a2f15b53", "importer_id": "a0fadb1d-9888-4fcb-b185-25b984bcb227", - "num_rows": 4, - "num_columns": 4, - "num_processed_values": 16, + "num_rows": 2, + "num_columns": 3, + "num_processed_values": 5, "metadata": { - "user_id": 1234, - "user_email": "test@example.com", - "environment": "dev" + "user_id": 1234, + "user_email": "user@example.com", + "environment": "staging" }, - "is_stored": true, - "has_errors": false, - "num_error_rows": 0, - "num_valid_rows": 4, "created_at": 1698172312, "error": null, + "columns": [ + { + "data_type": "number", + "key": "age", + "name": "Age" + }, + { + "data_type": "string", + "key": "email", + "name": "Email" + }, + { + "data_type": "string", + "key": "first_name", + "name": "First Name" + } + ], "rows": [ { "index": 0, "values": { - "age": "23", + "age": 23, "email": "maria@example.com", - "first_name": "Maria", - "last_name": "Martinez" + "first_name": "Maria" } }, { "index": 1, "values": { - "age": "32", + "age": 32, "email": "robert@example.com", - "first_name": "Robert", - "last_name": "Jones" - } - }, - { - "index": 2, - "values": { - "age": "30", - "email": "mary@example.com", - "first_name": "Mary", - "last_name": "Zhang" - } - }, - { - "index": 3, - "values": { - "age": "24", - "email": "jamie@example.com", - "first_name": "Jamie", - "last_name": "Miller" + "first_name": "Robert" } } ] diff --git a/docs/sdk/react.mdx b/docs/sdk/react.mdx index 5141d8d8..081cdb7b 100644 --- a/docs/sdk/react.mdx +++ b/docs/sdk/react.mdx @@ -256,68 +256,62 @@ function MyComponent() { - Callback function that fires when a user completes an import. It returns `data`, an object that contains the row data - and information about the import such as the number of rows. The number of rows returned is limited to 10,000. If - there are more than 10,000 rows, an `error` will be set and the data should be retrieved using the - [API](/api-reference/get-import-rows). + Callback function that fires when a user completes an import. It returns `data`, an object that contains the row data, + column definitions, and other information about the import. + + The number of rows included is limited to 10,000. If there are more than 10,000 rows, an `error` will be set and + the data should be retrieved using the [API](/api-reference/get-import-rows). + ```jsx onComplete={(data) => console.log(data)} ``` Example `data`: ```json { - "id": "170f9ae1-c109-4e26-83a1-b31f2baa81b2", - "upload_id": "4f7ec0b5-16ef-4d0e-8b6a-0c182815a131", + "id": "da5554e3-6c87-41b2-9366-5449a2f15b53", "importer_id": "a0fadb1d-9888-4fcb-b185-25b984bcb227", - "num_rows": 4, - "num_columns": 4, - "num_processed_values": 16, + "num_rows": 2, + "num_columns": 3, + "num_processed_values": 5, "metadata": { - "user_id": 1234, - "user_email": "test@example.com", - "environment": "dev" + "user_id": 1234, + "user_email": "user@example.com", + "environment": "staging" }, - "is_stored": true, - "has_errors": false, - "num_error_rows": 0, - "num_valid_rows": 4, "created_at": 1698172312, "error": null, + "columns": [ + { + "data_type": "number", + "key": "age", + "name": "Age" + }, + { + "data_type": "string", + "key": "email", + "name": "Email" + }, + { + "data_type": "string", + "key": "first_name", + "name": "First Name" + } + ], "rows": [ { "index": 0, "values": { - "age": "23", + "age": 23, "email": "maria@example.com", - "first_name": "Maria", - "last_name": "Martinez" + "first_name": "Maria" } }, { "index": 1, "values": { - "age": "32", + "age": 32, "email": "robert@example.com", - "first_name": "Robert", - "last_name": "Jones" - } - }, - { - "index": 2, - "values": { - "age": "30", - "email": "mary@example.com", - "first_name": "Mary", - "last_name": "Zhang" - } - }, - { - "index": 3, - "values": { - "age": "24", - "email": "jamie@example.com", - "first_name": "Jamie", - "last_name": "Miller" + "first_name": "Robert" } } ] diff --git a/docs/webhooks.mdx b/docs/webhooks.mdx index d52509d6..69135f42 100644 --- a/docs/webhooks.mdx +++ b/docs/webhooks.mdx @@ -4,11 +4,13 @@ title: Webhooks Feature available in TableFlow Cloud. -TableFlow uses webhooks to push real-time notifications about your data imports. For example, you can use webhooks on TableFlow to get notified when a user completes a file import: +TableFlow uses webhooks to push real-time notifications for your data imports. For example, you can use webhooks on TableFlow to get notified when a user completes a file import: 1. Your user imports a CSV or Excel file using the embedded TableFlow importer on your web app 2. TableFlow notifies your system, via a webhook, that a data import has been completed -3. Your system can use the API to [paginate through the imported data](/api-reference/get-import-rows), or [download the data directly as a CSV file](/api-reference/download-import). +3. The webhook will contain the row data, column definitions, and other information about the import + +The number of rows included is limited to 10,000. If there are more than 10,000 rows, an `error` will be set and the data should be retrieved using the [API](/api-reference/get-import-rows). ## Configure your application to receive webhooks @@ -39,3 +41,44 @@ You’ll be able to see the webhook received on [Svix Play](https://play.svix.co After configuring your application to receive webhooks, all subscribed events will be sent to the endpoint. You can now import a file to test out your new webhook! + +## Transforming and filtering webhooks + +You can use the transformations feature to modify the payload of webhooks or cancel it entirely based on data in the payload. + +To add a transformation, select "Enable" and "Edit transformation" under the "Advanced" tab of an endpoint: + +![Transformations](/assets/webhooks-transformations.jpg) + + +#### Transform + +You can use transformations to modify the payload to be in the format you need, or add additional parameters. + + +![Transformations](/assets/webhooks-transformations-transform.jpg) + + +```javascript +function handler(webhook) { + webhook.payload.myExtraProperty = 'Foo'; + return webhook; +} +``` + +#### Filter + +To filter webhooks from being sent to an endpoint, you just need to set `webhook.cancel = true`. In this example we filter webhooks to an endpoint based on the import `metadata` (a parameter you can provide the [SDK](/sdk/react#properties)): + + +![Transformations](/assets/webhooks-transformations-filter.jpg) + + +```javascript +function handler(webhook) { + if (webhook.payload.metadata?.environment !== 'development') { + webhook.cancel = true; + } + return webhook; +} +``` diff --git a/importer-ui/src/api/types/index.ts b/importer-ui/src/api/types/index.ts index 6546628e..87607691 100644 --- a/importer-ui/src/api/types/index.ts +++ b/importer-ui/src/api/types/index.ts @@ -70,6 +70,7 @@ export type UploadColumn = { name: string; sample_data: string[]; suggested_template_column_id: string; + suggested_data_type: string; }; export type UploadRow = { @@ -90,7 +91,6 @@ export type Import = { num_columns: number; num_processed_values: number; num_rows: number; - upload_id: string; workspace_id: string; importer?: Importer; }; diff --git a/importer-ui/src/api/usePostUpload.ts b/importer-ui/src/api/usePostUploadSetColumnMapping.ts similarity index 84% rename from importer-ui/src/api/usePostUpload.ts rename to importer-ui/src/api/usePostUploadSetColumnMapping.ts index c7192bb5..872ffbfa 100644 --- a/importer-ui/src/api/usePostUpload.ts +++ b/importer-ui/src/api/usePostUploadSetColumnMapping.ts @@ -4,7 +4,7 @@ import { post } from "./api"; type ColumnMap = { [key: string]: string }; -export default function usePostUpload(uploadId: string): UseMutationResult> { +export default function usePostUploadSetColumnMapping(uploadId: string): UseMutationResult> { return useMutation((columns: any) => mutateColumnMap(uploadId, columns)); } diff --git a/importer-ui/src/components/Table/types/index.ts b/importer-ui/src/components/Table/types/index.ts index 0e3ee431..c50b8c73 100644 --- a/importer-ui/src/components/Table/types/index.ts +++ b/importer-ui/src/components/Table/types/index.ts @@ -5,44 +5,46 @@ type Style = { readonly [key: string]: string }; type Primitive = string | number | boolean | null | undefined; export type TableComposite = { - raw: Primitive; - content: Primitive | React.ReactElement; - tooltip?: string; - captionInfo?: string; + raw: Primitive; + content: Primitive | React.ReactElement; + tooltip?: string; + captionInfo?: string; }; export type TableValue = Primitive | TableComposite; export type TableDatum = { - [key: string]: TableValue; + [key: string]: TableValue; }; export type TableData = TableDatum[]; +export type ColumnAlignment = "left" | "center" | "right" | ""; + export type TableProps = { - data: TableData; - keyAsId?: string; - theme?: Style; - mergeThemes?: boolean; - highlightColumns?: string[]; - hideColumns?: string[]; - emptyState?: ReactElement; - heading?: ReactElement; - background?: "zebra" | "dark" | "light"; - columnWidths?: string[]; - columnAlignments?: ("left" | "center" | "right" | "")[]; - fixHeader?: boolean; - onRowClick?: (row: TableDatum) => void; + data: TableData; + keyAsId?: string; + theme?: Style; + mergeThemes?: boolean; + highlightColumns?: string[]; + hideColumns?: string[]; + emptyState?: ReactElement; + heading?: ReactElement; + background?: "zebra" | "dark" | "light"; + columnWidths?: string[]; + columnAlignments?: ColumnAlignment[]; + fixHeader?: boolean; + onRowClick?: (row: TableDatum) => void; }; export type RowProps = { - datum: TableDatum; - isHeading?: boolean; - onClick?: (row: TableDatum) => void; + datum: TableDatum; + isHeading?: boolean; + onClick?: (row: TableDatum) => void; }; export type CellProps = PropsWithChildren<{ - cellClass?: string; - cellStyle: Style; - tooltip?: string; + cellClass?: string; + cellStyle: Style; + tooltip?: string; }>; diff --git a/importer-ui/src/features/main/index.tsx b/importer-ui/src/features/main/index.tsx index 7bf99fe9..e3090c0f 100644 --- a/importer-ui/src/features/main/index.tsx +++ b/importer-ui/src/features/main/index.tsx @@ -44,6 +44,7 @@ export default function Main() { template: sdkDefinedTemplate, schemaless, schemalessReadOnly, + schemalessDataTypes, showDownloadTemplateButton, customStyles, cssOverrides, @@ -88,7 +89,7 @@ export default function Main() { const [selectedHeaderRow, setSelectedHeaderRow] = useState(null); const [uploadFromHeaderRowSelection, setUploadFromHeaderRowSelection] = useState(null); const [columnsOrder, setColumnsOrder] = useState(); - const [columnsValues, seColumnsValues] = useState({}); + const [columnsValues, setColumnsValues] = useState({}); // Stepper handler const { currentStep, setStep, goNext, goBack, stepper, setStorageStep } = useStepNavigation(StepEnum.Upload, skipHeader, importerId, tusId); @@ -256,7 +257,8 @@ export default function Main() { onCancel={skipHeader ? reload : () => goBack(StepEnum.RowSelection)} schemaless={schemaless} schemalessReadOnly={schemalessReadOnly} - setColumnsValues={seColumnsValues} + schemalessDataTypes={schemalessDataTypes} + setColumnsValues={setColumnsValues} columnsValues={columnsValues} isLoading={reviewIsLoading || (!reviewIsStored && enabledReview)} onLoad={() => setEnabledReview(false)} diff --git a/importer-ui/src/features/map-columns/hooks/useMapColumnsTable.tsx b/importer-ui/src/features/map-columns/hooks/useMapColumnsTable.tsx index bc1dbbb3..75e5b03b 100644 --- a/importer-ui/src/features/map-columns/hooks/useMapColumnsTable.tsx +++ b/importer-ui/src/features/map-columns/hooks/useMapColumnsTable.tsx @@ -11,6 +11,7 @@ type Include = { template: string; use: boolean; selected?: boolean; + dataType?: string; }; export default function useMapColumnsTable( @@ -18,6 +19,7 @@ export default function useMapColumnsTable( templateColumns: TemplateColumn[] = [], schemaless?: boolean, schemalessReadOnly?: boolean, + schemalessDataTypes?: boolean, columnsValues: { [key: string]: Include } = {} ) { useEffect(() => { @@ -27,6 +29,13 @@ export default function useMapColumnsTable( }); }, []); + const dataTypes: { [key: string]: InputOption } = { + Text: { value: "string", required: false }, + Number: { value: "number", required: false }, + Date: { value: "date", required: false }, + "True/False": { value: "boolean", required: false }, + }; + const [values, setValues] = useState<{ [key: string]: Include }>(() => { return items.reduce( (acc, uc) => ({ @@ -35,6 +44,7 @@ export default function useMapColumnsTable( template: uc?.suggested_template_column_id || "", use: !!uc?.suggested_template_column_id, selected: !!uc?.suggested_template_column_id, + dataType: uc?.suggested_data_type || "string", }, }), {} @@ -67,6 +77,16 @@ export default function useMapColumnsTable( setValues((prev) => ({ ...prev, [id]: { ...prev[id], template: value, use: !!value } })); }; + const handleDataTypeChange = (id: string, dataType: string) => { + setValues((prev) => ({ + ...prev, + [id]: { + ...prev[id], + dataType, + }, + })); + }; + const rows = useMemo(() => { return items.map((item) => { const { id, name, sample_data } = item; @@ -76,8 +96,12 @@ export default function useMapColumnsTable( .replace(/\s/g, "_") .replace(/[^a-zA-Z0-9_]/g, "") .toLowerCase(); + let suggestedDataType = "string"; + if (suggestion.dataType && suggestion.dataType !== "") { + suggestedDataType = suggestion.dataType; + } - return { + const result: any = { "Your File Column": { raw: name || false, content: name || - empty -, @@ -113,6 +137,23 @@ export default function useMapColumnsTable( /> ), }, + ...(schemalessDataTypes + ? { + "Data Type": { + raw: "", + content: ( + handleDataTypeChange(id, dataType)} + selectedValues={[]} // [{ template: "string", selected: true }] + updateSelectedValues={() => {}} + /> + ), + }, + } + : {}), Include: { raw: false, content: ( @@ -124,6 +165,7 @@ export default function useMapColumnsTable( ), }, }; + return result; }); }, [values]); return { rows, formValues: values }; @@ -144,5 +186,5 @@ const SchemalessInput = ({ value, setValues, readOnly }: { value: string; setVal setValues(transformedValue); }; - return ; + return ; }; diff --git a/importer-ui/src/features/map-columns/index.tsx b/importer-ui/src/features/map-columns/index.tsx index b4ac2ef6..08205e5d 100644 --- a/importer-ui/src/features/map-columns/index.tsx +++ b/importer-ui/src/features/map-columns/index.tsx @@ -2,7 +2,8 @@ import { FormEvent, useEffect, useState } from "react"; import { Button } from "@chakra-ui/button"; import Errors from "../../components/Errors"; import Table from "../../components/Table"; -import usePostUpload from "../../api/usePostUpload"; +import { ColumnAlignment } from "../../components/Table/types"; +import usePostUploadSetColumnMapping from "../../api/usePostUploadSetColumnMapping"; import useMapColumnsTable from "./hooks/useMapColumnsTable"; import { MapColumnsProps } from "./types"; import style from "./style/MapColumns.module.scss"; @@ -15,26 +16,40 @@ export default function MapColumns({ skipHeaderRowSelection, schemaless, schemalessReadOnly, + schemalessDataTypes, setColumnsValues, columnsValues, isLoading, onLoad, }: MapColumnsProps) { - const { rows, formValues } = useMapColumnsTable(upload?.upload_columns, template?.columns, schemaless, schemalessReadOnly, columnsValues); - const { mutate, error, isSuccess, isLoading: isLoadingPost } = usePostUpload(upload?.id || ""); + const { rows, formValues } = useMapColumnsTable( + upload?.upload_columns, + template?.columns, + schemaless, + schemalessReadOnly, + schemalessDataTypes, + columnsValues + ); + const { mutate, error, isSuccess, isLoading: isLoadingPost } = usePostUploadSetColumnMapping(upload?.id || ""); const [selectedColumns, setSelectedColumns] = useState([]); const onSubmit = (e: FormEvent) => { e.preventDefault(); setColumnsValues(formValues); - const columns = Object.keys(formValues).reduce((acc, key) => { - const { template, use } = formValues[key]; - return { ...acc, ...(use ? { [key]: template } : {}) }; - }, {}); + + const columns: any = {}; + const columnsToSubmit: any = {}; + Object.keys(formValues).forEach((key) => { + const { template, use, dataType } = formValues[key]; + if (use) { + columns[key] = template; + columnsToSubmit[key] = { template_column_id: template, data_type: schemalessDataTypes ? dataType : "" }; + } + }); setSelectedColumns(columns); - mutate(columns); + mutate(columnsToSubmit); }; useEffect(() => { @@ -48,13 +63,19 @@ export default function MapColumns({ }, [isSuccess, error, isLoading, isLoadingPost]); if (!rows || !rows?.length) return null; + let columnWidths = ["20%", "30%", "30%", "20%"]; + let columnAlignments: ColumnAlignment[] = ["", "", "", "center"]; + if (schemalessDataTypes) { + columnWidths = ["20%", "23%", "23%", "22%", "12%"]; + columnAlignments = ["", "", "", "", "center"]; + } return (
{upload ? (
- +
) : ( <>Loading... diff --git a/importer-ui/src/features/map-columns/style/MapColumns.module.scss b/importer-ui/src/features/map-columns/style/MapColumns.module.scss index 4a1b42e8..0ed9ad47 100644 --- a/importer-ui/src/features/map-columns/style/MapColumns.module.scss +++ b/importer-ui/src/features/map-columns/style/MapColumns.module.scss @@ -29,7 +29,7 @@ line-height: 1; white-space: nowrap; - &>small { + & > small { background-color: var(--color-input-background); font-family: monospace; padding: var(--m-xxxxs); @@ -37,7 +37,7 @@ font-size: var(--font-size-xs); display: inline-block; - &+small { + & + small { margin-left: var(--m-xxxxs); } } @@ -54,4 +54,8 @@ display: flex; justify-content: center; max-width: 60vw; -} \ No newline at end of file +} + +.schemalessTextInput { + width: 210px; +} diff --git a/importer-ui/src/features/map-columns/types/index.ts b/importer-ui/src/features/map-columns/types/index.ts index 4531e98b..46de3503 100644 --- a/importer-ui/src/features/map-columns/types/index.ts +++ b/importer-ui/src/features/map-columns/types/index.ts @@ -18,6 +18,7 @@ export type MapColumnsProps = { skipHeaderRowSelection?: boolean; schemaless?: boolean; schemalessReadOnly?: boolean; + schemalessDataTypes?: boolean; setColumnsValues: Dispatch>; columnsValues: FormValues; isLoading?: boolean; diff --git a/importer-ui/src/hooks/useSearchParams.ts b/importer-ui/src/hooks/useSearchParams.ts index 79b777ed..632832eb 100644 --- a/importer-ui/src/hooks/useSearchParams.ts +++ b/importer-ui/src/hooks/useSearchParams.ts @@ -15,6 +15,7 @@ type SearchParams = Record< | "cssOverrides" | "schemaless" | "schemalessReadOnly" + | "schemalessDataTypes" | "showDownloadTemplateButton", string >; diff --git a/importer-ui/src/providers/Embed.tsx b/importer-ui/src/providers/Embed.tsx index ff370252..ef8ae451 100644 --- a/importer-ui/src/providers/Embed.tsx +++ b/importer-ui/src/providers/Embed.tsx @@ -23,6 +23,7 @@ export default function Embed({ children }: EmbedProps) { cssOverrides, schemaless, schemalessReadOnly, + schemalessDataTypes, showDownloadTemplateButton, } = useSearchParams(); @@ -58,6 +59,7 @@ export default function Embed({ children }: EmbedProps) { isModal: strToDefaultBoolean(isModal, true), schemaless: strToOptionalBoolean(schemaless), schemalessReadOnly: strToOptionalBoolean(schemalessReadOnly), + schemalessDataTypes: strToOptionalBoolean(schemalessDataTypes), showDownloadTemplateButton: strToDefaultBoolean(showDownloadTemplateButton, true), customStyles: validateJSON(customStyles, "customStyles"), cssOverrides: validateJSON(cssOverrides, "cssOverrides"), diff --git a/importer-ui/src/stores/embed.ts b/importer-ui/src/stores/embed.ts index 2c7d3cf5..ecd036ef 100644 --- a/importer-ui/src/stores/embed.ts +++ b/importer-ui/src/stores/embed.ts @@ -12,6 +12,7 @@ type EmbedParams = { skipHeaderRowSelection?: boolean; schemaless?: boolean; schemalessReadOnly?: boolean; + schemalessDataTypes?: boolean; showDownloadTemplateButton: boolean; customStyles?: string; cssOverrides?: string; @@ -22,7 +23,7 @@ type ParamsStore = { setEmbedParams: (embedParams: EmbedParams) => void; }; -const useEmbedStore = create()((set) => ({ +const useEmbedStore = create((set) => ({ embedParams: { importerId: "", metadata: "", @@ -36,15 +37,24 @@ const useEmbedStore = create()((set) => ({ cssOverrides: "", schemaless: false, schemalessReadOnly: false, + schemalessDataTypes: false, }, - setEmbedParams: (embedParams) => + setEmbedParams: (embedParams) => { + const { schemaless, ...params } = embedParams; + const updatedParams = { + ...params, + schemaless, + schemalessReadOnly: schemaless ? embedParams.schemalessReadOnly : false, + schemalessDataTypes: schemaless ? embedParams.schemalessDataTypes : false, + }; + set((state) => ({ embedParams: { ...state.embedParams, - ...embedParams, - importerId: embedParams.importerId === undefined || embedParams.importerId?.trim() === "" ? "0" : embedParams.importerId, + ...updatedParams, + importerId: updatedParams.importerId === undefined || updatedParams.importerId?.trim() === "" ? "0" : updatedParams.importerId, }, - })), + })); + }, })); - export default useEmbedStore; diff --git a/js-sdk/package.json b/js-sdk/package.json index befb5ffe..a57fe712 100644 --- a/js-sdk/package.json +++ b/js-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tableflow/js", - "version": "1.23.0", + "version": "1.24.0", "description": "The JavaScript SDK for TableFlow. Embed an importer to collect and transform CSV files in your application.", "scripts": { "build": "rollup -c --bundleConfigAsCjs false", diff --git a/js-sdk/src/index.stories.ts b/js-sdk/src/index.stories.ts index ce08d39e..d88147f1 100644 --- a/js-sdk/src/index.stories.ts +++ b/js-sdk/src/index.stories.ts @@ -28,6 +28,7 @@ const meta = { showDownloadTemplateButton: { control: "boolean" }, schemaless: { control: "boolean" }, schemalessReadOnly: { control: "boolean" }, + schemalessDataTypes: { control: "boolean" }, waitOnComplete: { control: "boolean" }, }, } satisfies Meta; diff --git a/js-sdk/src/index.ts b/js-sdk/src/index.ts index e0244574..1885064e 100644 --- a/js-sdk/src/index.ts +++ b/js-sdk/src/index.ts @@ -24,6 +24,7 @@ export default function createTableFlowImporter({ cssOverrides, schemaless, schemalessReadOnly, + schemalessDataTypes, }: TableFlowImporterProps) { // CSS classes const baseClass = "TableFlowImporter"; @@ -65,6 +66,7 @@ export default function createTableFlowImporter({ skipHeaderRowSelection: parseOptionalBoolean(skipHeaderRowSelection), schemaless: parseOptionalBoolean(schemaless), schemalessReadOnly: parseOptionalBoolean(schemalessReadOnly), + schemalessDataTypes: parseOptionalBoolean(schemalessDataTypes), }; const uploaderUrl = getUploaderUrl(urlParams, hostUrl); diff --git a/js-sdk/src/types/index.ts b/js-sdk/src/types/index.ts index 0c037c94..2f92515d 100644 --- a/js-sdk/src/types/index.ts +++ b/js-sdk/src/types/index.ts @@ -21,4 +21,5 @@ export type TableFlowImporterProps = HTMLDialogElement & { skipHeaderRowSelection?: boolean; schemaless?: boolean; schemalessReadOnly?: boolean; + schemalessDataTypes?: boolean; } & ModalParams; diff --git a/react-sdk/package.json b/react-sdk/package.json index 8005b951..21914a67 100644 --- a/react-sdk/package.json +++ b/react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@tableflow/react", - "version": "1.32.0", + "version": "1.33.0", "description": "The React SDK for TableFlow. Embed an importer to collect and transform CSV files in your application.", "main": "build/index.js", "module": "build/index.esm.js", diff --git a/react-sdk/src/components/TableFlowImporter/index.tsx b/react-sdk/src/components/TableFlowImporter/index.tsx index 0fecfcd5..0ae61dcd 100644 --- a/react-sdk/src/components/TableFlowImporter/index.tsx +++ b/react-sdk/src/components/TableFlowImporter/index.tsx @@ -24,6 +24,7 @@ export default function TableFlowImporter({ cssOverrides, schemaless, schemalessReadOnly, + schemalessDataTypes, ...props }: TableFlowImporterProps) { const ref = useRef(null); @@ -59,6 +60,7 @@ export default function TableFlowImporter({ skipHeaderRowSelection: parseOptionalBoolean(skipHeaderRowSelection), schemaless: parseOptionalBoolean(schemaless), schemalessReadOnly: parseOptionalBoolean(schemalessReadOnly), + schemalessDataTypes: parseOptionalBoolean(schemalessDataTypes), }; const searchParams = new URLSearchParams(urlParams); const defaultImporterUrl = "https://importer.tableflow.com"; diff --git a/react-sdk/src/components/TableFlowImporter/types/index.ts b/react-sdk/src/components/TableFlowImporter/types/index.ts index bcc0632f..62ab700b 100644 --- a/react-sdk/src/components/TableFlowImporter/types/index.ts +++ b/react-sdk/src/components/TableFlowImporter/types/index.ts @@ -23,4 +23,5 @@ export type TableFlowImporterProps = (HTMLAttributes & HTMLAt skipHeaderRowSelection?: boolean; schemaless?: boolean; schemalessReadOnly?: boolean; + schemalessDataTypes?: boolean; } & ModalParams; diff --git a/react-sdk/src/settings/defaults.ts b/react-sdk/src/settings/defaults.ts index f450245c..5694d70e 100644 --- a/react-sdk/src/settings/defaults.ts +++ b/react-sdk/src/settings/defaults.ts @@ -16,6 +16,7 @@ const defaults: TableFlowImporterProps = { // }, // schemaless: false, // schemalessReadOnly: true, + // schemalessDataTypes: true, darkMode: true, onComplete: (data) => console.log("onComplete", data), // customStyles: {