mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge pull request #13406 from supabase/feat/import-csv-existing
Feat/import csv existing
This commit is contained in:
25 files changed
+615
-197
No files matched your search
@@ -18,7 +18,7 @@ interface Props {
|
||||
|
||||
export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props) {
|
||||
const ref = useRef(null)
|
||||
const [swiperInstace, setSwiperInstance] = useState<SwiperCore | any | null>(null)
|
||||
const [swiperInstance, setSwiperInstance] = useState<SwiperCore | any | null>(null)
|
||||
const { width } = useWindowSize()
|
||||
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7'
|
||||
const BUCKET_FOLDER_VERSION = 'v3'
|
||||
@@ -28,8 +28,8 @@ export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props)
|
||||
}/${BUCKET_FOLDER_VERSION}/${username}.png`
|
||||
|
||||
useEffect(() => {
|
||||
// trigger autplay if viewport resize
|
||||
if (swiperInstace) swiperInstace?.autoplay?.run()
|
||||
// trigger autoplay if viewport resize
|
||||
if (swiperInstance) swiperInstance?.autoplay?.run()
|
||||
}, [width])
|
||||
|
||||
return (
|
||||
|
||||
@@ -369,6 +369,7 @@ export default {
|
||||
transition
|
||||
shadow-sm
|
||||
rounded
|
||||
border
|
||||
focus:outline-none
|
||||
focus-visible:ring
|
||||
focus-visible:ring-scale-700
|
||||
@@ -376,13 +377,13 @@ export default {
|
||||
`,
|
||||
inactive: `
|
||||
bg-scale-200
|
||||
border border-scale-700 hover:border-scale-900
|
||||
border-scale-700 hover:border-scale-900
|
||||
text-scale-900 hover:text-scale-1200
|
||||
`,
|
||||
active: `
|
||||
bg-scale-1200
|
||||
text-scale-200
|
||||
border-scale-1200
|
||||
bg-scale-600
|
||||
text-scale-1200
|
||||
border-scale-800
|
||||
`,
|
||||
},
|
||||
'rounded-pills': {
|
||||
|
||||
@@ -66,6 +66,11 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
showCustomChildren,
|
||||
customHeader,
|
||||
children,
|
||||
onAddRow,
|
||||
onAddColumn,
|
||||
updateTableRow,
|
||||
onEditForeignKeyColumnValue,
|
||||
onImportData,
|
||||
} = props
|
||||
const dispatch = useDispatch()
|
||||
const state = useTrackedState()
|
||||
@@ -79,15 +84,13 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
const sorts = formatSortURLParams(sort as string[])
|
||||
const filters = formatFilterURLParams(filter as string[])
|
||||
|
||||
const table = props.table
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const { data, isLoading, isRefetching } = useTableRowsQuery(
|
||||
{
|
||||
queryKey: [table.schema, table.name],
|
||||
queryKey: [props.table.schema, props.table.name],
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
table,
|
||||
table: props.table,
|
||||
sorts,
|
||||
filters,
|
||||
page: state.page,
|
||||
@@ -194,17 +197,18 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
if (!state.table || refreshView || refreshTable) {
|
||||
initializeData()
|
||||
}
|
||||
}, [state.metaService, state.table, table, props.schema])
|
||||
}, [state.metaService, state.table, props.table, props.schema])
|
||||
|
||||
return (
|
||||
<div className="sb-grid">
|
||||
<Header
|
||||
table={table}
|
||||
table={props.table}
|
||||
sorts={sorts}
|
||||
filters={filters}
|
||||
isRefetching={isRefetching}
|
||||
onAddRow={editable ? props.onAddRow : undefined}
|
||||
onAddColumn={editable ? props.onAddColumn : undefined}
|
||||
onAddRow={editable ? onAddRow : undefined}
|
||||
onAddColumn={editable ? onAddColumn : undefined}
|
||||
onImportData={editable ? onImportData : undefined}
|
||||
headerActions={headerActions}
|
||||
customHeader={customHeader}
|
||||
/>
|
||||
@@ -216,8 +220,8 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
ref={gridRef}
|
||||
{...gridProps}
|
||||
rows={data?.rows ?? []}
|
||||
updateRow={props.updateTableRow}
|
||||
onEditForeignKeyColumnValue={props.onEditForeignKeyColumnValue}
|
||||
updateRow={updateTableRow}
|
||||
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
|
||||
/>
|
||||
<Footer isLoading={isLoading || isRefetching} />
|
||||
<Shortcuts gridRef={gridRef} />
|
||||
@@ -225,7 +229,7 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
)}
|
||||
|
||||
{mounted &&
|
||||
createPortal(<RowContextMenu table={table} rows={data?.rows ?? []} />, document.body)}
|
||||
createPortal(<RowContextMenu table={props.table} rows={data?.rows ?? []} />, document.body)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,6 +1,15 @@
|
||||
import { saveAs } from 'file-saver'
|
||||
import { useState, ReactNode } from 'react'
|
||||
import { Button, IconDownload, IconX, IconTrash, Dropdown, IconChevronDown } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
IconDownload,
|
||||
IconX,
|
||||
IconTrash,
|
||||
Dropdown,
|
||||
IconChevronDown,
|
||||
IconFileText,
|
||||
IconArrowUp,
|
||||
} from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useStore, useUrlState } from 'hooks'
|
||||
@@ -18,6 +27,7 @@ import { useTableRowTruncateMutation } from 'data/table-rows/table-row-truncate-
|
||||
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
|
||||
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
|
||||
import RLSBannerWarning from './RLSBannerWarning'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// [Joshen] CSV exports require this guard as a fail-safe if the table is
|
||||
// just too large for a browser to keep all the rows in memory before
|
||||
@@ -31,6 +41,7 @@ export type HeaderProps = {
|
||||
isRefetching: boolean
|
||||
onAddColumn?: () => void
|
||||
onAddRow?: () => void
|
||||
onImportData?: () => void
|
||||
headerActions?: ReactNode
|
||||
customHeader: ReactNode
|
||||
}
|
||||
@@ -41,6 +52,7 @@ const Header = ({
|
||||
filters,
|
||||
onAddColumn,
|
||||
onAddRow,
|
||||
onImportData,
|
||||
headerActions,
|
||||
customHeader,
|
||||
isRefetching,
|
||||
@@ -63,6 +75,7 @@ const Header = ({
|
||||
isRefetching={isRefetching}
|
||||
onAddColumn={onAddColumn}
|
||||
onAddRow={onAddRow}
|
||||
onImportData={onImportData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -81,8 +94,15 @@ type DefaultHeaderProps = {
|
||||
isRefetching: boolean
|
||||
onAddColumn?: () => void
|
||||
onAddRow?: () => void
|
||||
onImportData?: () => void
|
||||
}
|
||||
const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHeaderProps) => {
|
||||
const DefaultHeader = ({
|
||||
table,
|
||||
isRefetching,
|
||||
onAddColumn,
|
||||
onAddRow,
|
||||
onImportData,
|
||||
}: DefaultHeaderProps) => {
|
||||
const canAddNew = onAddRow !== undefined || onAddColumn !== undefined
|
||||
|
||||
// [Joshen] Using this logic to block both column and row creation/update/delete
|
||||
@@ -115,7 +135,6 @@ const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHe
|
||||
key="add-row"
|
||||
className="group"
|
||||
onClick={onAddRow}
|
||||
disabled={onAddRow === undefined}
|
||||
icon={
|
||||
<div className="-mt-2 pr-1.5">
|
||||
<div className="border border-scale-1000 w-[15px] h-[4px]" />
|
||||
@@ -142,7 +161,6 @@ const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHe
|
||||
key="add-column"
|
||||
className="group"
|
||||
onClick={onAddColumn}
|
||||
disabled={onAddColumn === undefined}
|
||||
icon={
|
||||
<div className="flex -mt-2 pr-1.5">
|
||||
<div className="border border-scale-1000 w-[4px] h-[15px]" />
|
||||
@@ -163,6 +181,33 @@ const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHe
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
: []),
|
||||
...(onImportData !== undefined
|
||||
? [
|
||||
<Dropdown.Item
|
||||
key="import-data"
|
||||
className="group"
|
||||
onClick={onImportData}
|
||||
icon={
|
||||
<div className="relative -mt-2">
|
||||
<IconFileText className="-translate-x-[2px]" />
|
||||
<IconArrowUp
|
||||
className={clsx(
|
||||
'transition duration-200 absolute bottom-0 right-0 translate-y-1 opacity-0 bg-brand-700 rounded-full',
|
||||
'group-hover:translate-y-0 group-hover:text-brand-900 group-hover:opacity-100'
|
||||
)}
|
||||
strokeWidth={3}
|
||||
size={12}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="">
|
||||
<p>Import data from CSV</p>
|
||||
<p className="text-scale-1000">Insert new rows from a CSV</p>
|
||||
</div>
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
<Button size="tiny" icon={<IconChevronDown size={14} strokeWidth={1.5} />}>
|
||||
|
||||
@@ -96,6 +96,11 @@ export interface SupabaseGridProps {
|
||||
* Custom component passed as children
|
||||
*/
|
||||
children?: ReactNode
|
||||
|
||||
/**
|
||||
* show import csv data button if available
|
||||
*/
|
||||
onImportData?: () => void
|
||||
}
|
||||
|
||||
export interface SupabaseGridRef {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useStore, useUrlState } from 'hooks'
|
||||
import { entityTypeKeys } from 'data/entity-types/keys'
|
||||
import { useTableRowCreateMutation } from 'data/table-rows/table-row-create-mutation'
|
||||
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
|
||||
import { RowEditor, ColumnEditor, TableEditor } from '.'
|
||||
import { RowEditor, ColumnEditor, TableEditor, SpreadsheetImport } from '.'
|
||||
import { ImportContent } from './TableEditor/TableEditor.types'
|
||||
import {
|
||||
ColumnField,
|
||||
@@ -38,7 +38,7 @@ export interface SidePanelEditorProps {
|
||||
row: any
|
||||
column: any
|
||||
}
|
||||
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector'
|
||||
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
|
||||
isDuplicating?: boolean
|
||||
closePanel: () => void
|
||||
onRowCreated?: (row: Dictionary<any>) => void
|
||||
@@ -74,9 +74,14 @@ const SidePanelEditor = ({
|
||||
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
|
||||
|
||||
const tables = meta.tables.list()
|
||||
const enumArrayColumns = (selectedTable?.columns ?? [])
|
||||
.filter((column) => {
|
||||
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
|
||||
})
|
||||
.map((column) => column.name)
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const { mutateAsync: createTableRow } = useTableRowCreateMutation()
|
||||
const { mutateAsync: createTableRows } = useTableRowCreateMutation()
|
||||
const { mutateAsync: updateTableRow } = useTableRowUpdateMutation({
|
||||
async onMutate({ projectRef, table, configuration, payload }) {
|
||||
closePanel()
|
||||
@@ -146,16 +151,9 @@ const SidePanelEditor = ({
|
||||
}
|
||||
|
||||
let saveRowError = false
|
||||
// @ts-ignore
|
||||
const enumArrayColumns = selectedTable.columns
|
||||
.filter((column) => {
|
||||
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
|
||||
})
|
||||
.map((column) => column.name)
|
||||
|
||||
if (isNewRecord) {
|
||||
try {
|
||||
const result = await createTableRow({
|
||||
const result = await createTableRows({
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
table: selectedTable as any,
|
||||
@@ -444,6 +442,54 @@ const SidePanelEditor = ({
|
||||
resolve()
|
||||
}
|
||||
|
||||
const onImportData = async (importContent: ImportContent) => {
|
||||
if (!project || selectedTable === undefined) {
|
||||
return console.error('no project or table selected')
|
||||
}
|
||||
|
||||
const { file, rowCount, selectedHeaders, resolve } = importContent
|
||||
const toastId = ui.setNotification({
|
||||
category: 'loading',
|
||||
message: `Adding ${rowCount.toLocaleString()} rows to ${selectedTable.name}`,
|
||||
})
|
||||
const { error }: any = await meta.insertRowsViaSpreadsheet(
|
||||
file,
|
||||
selectedTable,
|
||||
selectedHeaders,
|
||||
(progress: number) => {
|
||||
ui.setNotification({
|
||||
id: toastId,
|
||||
progress,
|
||||
category: 'loading',
|
||||
message: `Adding ${rowCount.toLocaleString()} rows to ${selectedTable.name}`,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
id: toastId,
|
||||
category: 'error',
|
||||
message: `Failed to import data: ${error.message}`,
|
||||
})
|
||||
resolve()
|
||||
} else {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(
|
||||
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
|
||||
),
|
||||
])
|
||||
ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
message: `Successfully imported ${rowCount} rows of data into ${selectedTable.name}`,
|
||||
})
|
||||
resolve()
|
||||
closePanel()
|
||||
}
|
||||
}
|
||||
|
||||
const onClosePanel = () => {
|
||||
if (isEdited) {
|
||||
setIsClosingPanel(true)
|
||||
@@ -499,6 +545,13 @@ const SidePanelEditor = ({
|
||||
closePanel={onClosePanel}
|
||||
onSelect={onSaveForeignRow}
|
||||
/>
|
||||
<SpreadsheetImport
|
||||
visible={sidePanelKey === 'csv-import'}
|
||||
selectedTable={selectedTableToEdit}
|
||||
saveContent={onImportData}
|
||||
closePanel={onClosePanel}
|
||||
updateEditorDirty={setIsEdited}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
header="Confirm to close"
|
||||
|
||||
+10
-10
@@ -34,7 +34,7 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-scale-1100">
|
||||
Upload a CSV or TSV file. The first row should be the headers of the table, and your
|
||||
@@ -42,13 +42,13 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
<span className="text-code">-</span>) or underscores (<span className="text-code">_</span>
|
||||
).
|
||||
</p>
|
||||
<p className="text-xs text-scale-900">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Tip: Datetime columns should be formatted as YYYY-MM-DD HH:mm:ss
|
||||
</p>
|
||||
</div>
|
||||
{!uploadedFile ? (
|
||||
<div
|
||||
className={`flex h-48 cursor-pointer items-center justify-center rounded-md border border-dashed dark:border-gray-500 ${
|
||||
className={`flex h-48 cursor-pointer items-center justify-center rounded-md border border-dashed dark:border-gray-700 ${
|
||||
isDraggedOver ? 'bg-gray-500' : ''
|
||||
}`}
|
||||
onDragOver={onDragOver}
|
||||
@@ -56,15 +56,15 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
onDrop={onDrop}
|
||||
onClick={() => (uploadButtonRef.current as any)?.click()}
|
||||
>
|
||||
<p>
|
||||
Drag and drop, or <span className="text-green-1000">browse</span> your files
|
||||
<p className="text-sm">
|
||||
Drag and drop, or <span className="text-brand-900">browse</span> your files
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-32 flex-col items-center justify-center space-y-2 rounded-md border border-dashed dark:border-gray-500">
|
||||
<div className="flex h-28 flex-col items-center justify-center space-y-2 rounded-md border border-dashed dark:border-gray-700">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconFileText size={14} strokeWidth={1.5} />
|
||||
<h3 className="text-base text-scale-1200">{uploadedFile.name}</h3>
|
||||
<p className="text-sm text-scale-1200">{uploadedFile.name}</p>
|
||||
</div>
|
||||
{parseProgress === 100 ? (
|
||||
<Button type="outline" onClick={removeUploadedFile}>
|
||||
@@ -76,9 +76,9 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
<SparkBar
|
||||
value={parseProgress}
|
||||
max={100}
|
||||
type={'horizontal'}
|
||||
barClass={'bg-green-500'}
|
||||
labelBottom="Parsing file..."
|
||||
type="horizontal"
|
||||
barClass="bg-green-900"
|
||||
labelBottom="Checking file..."
|
||||
labelTop={`${parseProgress}%`}
|
||||
/>
|
||||
</div>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import clsx from 'clsx'
|
||||
import { useState } from 'react'
|
||||
import { SidePanel, Button, IconChevronDown, Collapsible } from 'ui'
|
||||
import { SpreadsheetData } from './SpreadsheetImport.types'
|
||||
|
||||
interface SpreadSheetImportConfigurationProps {
|
||||
spreadsheetData: SpreadsheetData
|
||||
selectedHeaders: string[]
|
||||
onToggleHeader: (header: string) => void
|
||||
}
|
||||
|
||||
const SpreadsheetImportConfiguration = ({
|
||||
spreadsheetData,
|
||||
selectedHeaders,
|
||||
onToggleHeader,
|
||||
}: SpreadSheetImportConfigurationProps) => {
|
||||
const [expandConfiguration, setExpandConfiguration] = useState(false)
|
||||
|
||||
return (
|
||||
<Collapsible open={expandConfiguration} onOpenChange={setExpandConfiguration} className={''}>
|
||||
<Collapsible.Trigger asChild>
|
||||
<SidePanel.Content>
|
||||
<div className="py-1 flex items-center justify-between">
|
||||
<p className="text-sm">Configure import data</p>
|
||||
<Button
|
||||
type="text"
|
||||
icon={
|
||||
<IconChevronDown
|
||||
size={18}
|
||||
strokeWidth={2}
|
||||
className={clsx('text-scale-1100', expandConfiguration && 'rotate-180')}
|
||||
/>
|
||||
}
|
||||
className="px-1"
|
||||
onClick={() => setExpandConfiguration(!expandConfiguration)}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<SidePanel.Content>
|
||||
<div className="py-2 space-y-3">
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">Select which columns to import</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
By default, all columns are selected to be imported from your CSV
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center flex-wrap gap-2 pl-0.5 pb-0.5">
|
||||
{spreadsheetData.headers.map((header) => {
|
||||
const isSelected = selectedHeaders.includes(header)
|
||||
return (
|
||||
<Button
|
||||
key={header}
|
||||
type={isSelected ? 'primary' : 'default'}
|
||||
className={clsx('transition', isSelected ? 'opacity-100' : 'opacity-75')}
|
||||
onClick={() => onToggleHeader(header)}
|
||||
>
|
||||
{header}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpreadsheetImportConfiguration
|
||||
+1
-1
@@ -15,7 +15,7 @@ const SpreadSheetTextInput: FC<Props> = ({ input, onInputChange }) => (
|
||||
include any special characters other than hyphens (<code>-</code>) or underscores (
|
||||
<code>_</code>).
|
||||
</p>
|
||||
<p className="text-xs text-scale-900">
|
||||
<p className="text-sm text-scale-900">
|
||||
Tip: Datetime columns should be formatted as YYYY-MM-DD HH:mm:ss
|
||||
</p>
|
||||
</div>
|
||||
+1
-4
@@ -7,10 +7,7 @@ export const UPLOAD_FILE_TYPES = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
]
|
||||
|
||||
export const UPLOAD_FILE_EXTENSIONS = [
|
||||
'csv',
|
||||
'tsv',
|
||||
]
|
||||
export const UPLOAD_FILE_EXTENSIONS = ['csv', 'tsv']
|
||||
|
||||
export const EMPTY_SPREADSHEET_DATA: SpreadsheetData = {
|
||||
headers: [],
|
||||
+87
-88
@@ -1,12 +1,12 @@
|
||||
import { useCallback, useState, FC, useEffect } from 'react'
|
||||
import { debounce, includes } from 'lodash'
|
||||
import { SidePanel, Tabs, IconArrowRight, IconChevronRight } from 'ui'
|
||||
import { debounce, includes, noop } from 'lodash'
|
||||
import { SidePanel, Tabs } from 'ui'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import ActionBar from '../../ActionBar'
|
||||
import ActionBar from '../ActionBar'
|
||||
import SpreadSheetTextInput from './SpreadSheetTextInput'
|
||||
import SpreadSheetFileUpload from './SpreadSheetFileUpload'
|
||||
import SpreadsheetPreview from './SpreadsheetPreview'
|
||||
import { SpreadsheetData } from './SpreadsheetImport.types'
|
||||
import {
|
||||
acceptedFileExtension,
|
||||
@@ -14,31 +14,36 @@ import {
|
||||
parseSpreadsheetText,
|
||||
} from './SpreadsheetImport.utils'
|
||||
import { UPLOAD_FILE_TYPES, EMPTY_SPREADSHEET_DATA } from './SpreadsheetImport.constants'
|
||||
import { ImportContent } from '../TableEditor/TableEditor.types'
|
||||
import SpreadsheetImportConfiguration from './SpreadSheetImportConfiguration'
|
||||
import SpreadsheetImportPreview from './SpreadsheetImportPreview'
|
||||
|
||||
interface Props {
|
||||
debounceDuration?: number
|
||||
headers?: string[]
|
||||
rows?: any[]
|
||||
visible: boolean
|
||||
saveContent: (prefillData: any) => void
|
||||
selectedTable?: PostgresTable
|
||||
saveContent: (prefillData: ImportContent) => void
|
||||
closePanel: () => void
|
||||
updateEditorDirty?: (value: boolean) => void
|
||||
}
|
||||
|
||||
const SpreadsheetImport: FC<Props> = ({
|
||||
visible = false,
|
||||
debounceDuration = 250,
|
||||
headers = [],
|
||||
rows = [],
|
||||
selectedTable,
|
||||
saveContent,
|
||||
closePanel,
|
||||
visible = false,
|
||||
updateEditorDirty = noop,
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (headers.length === 0) {
|
||||
resetSpreadsheetImport()
|
||||
}
|
||||
if (visible && headers.length === 0) {
|
||||
resetSpreadsheetImport()
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
@@ -52,7 +57,13 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
columnTypeMap: {},
|
||||
})
|
||||
const [errors, setErrors] = useState<any>([])
|
||||
const [expandedErrors, setExpandedErrors] = useState<string[]>([])
|
||||
const [selectedHeaders, setSelectedHeaders] = useState<string[]>([])
|
||||
|
||||
const selectedTableColumns = (selectedTable?.columns ?? []).map((column) => column.name)
|
||||
const incompatibleHeaders = selectedHeaders.filter(
|
||||
(header) => !selectedTableColumns.includes(header)
|
||||
)
|
||||
const isCompatible = selectedTable !== undefined ? incompatibleHeaders.length === 0 : true
|
||||
|
||||
const onProgressUpdate = (progress: number) => {
|
||||
setParseProgress(progress)
|
||||
@@ -68,6 +79,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
message: 'Sorry! We only accept CSV or TSV file types, please upload another file.',
|
||||
})
|
||||
} else {
|
||||
updateEditorDirty(true)
|
||||
setUploadedFile(file)
|
||||
const { headers, rowCount, columnTypeMap, errors, previewRows } = await parseSpreadsheet(
|
||||
file,
|
||||
@@ -83,6 +95,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
}
|
||||
|
||||
setErrors(errors)
|
||||
setSelectedHeaders(headers)
|
||||
setSpreadsheetData({ headers, rows: previewRows, rowCount, columnTypeMap })
|
||||
}
|
||||
event.target.value = ''
|
||||
@@ -93,7 +106,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
setSpreadsheetData(EMPTY_SPREADSHEET_DATA)
|
||||
setUploadedFile(null)
|
||||
setErrors([])
|
||||
setExpandedErrors([])
|
||||
updateEditorDirty(false)
|
||||
}
|
||||
|
||||
const readSpreadsheetText = async (text: string) => {
|
||||
@@ -108,6 +121,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
})
|
||||
}
|
||||
setErrors(errors)
|
||||
setSelectedHeaders(headers)
|
||||
setSpreadsheetData({ headers, rows, rowCount: rows.length, columnTypeMap })
|
||||
} else {
|
||||
setSpreadsheetData(EMPTY_SPREADSHEET_DATA)
|
||||
@@ -120,11 +134,28 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
handler(event.target.value)
|
||||
}
|
||||
|
||||
const onSelectExpandError = (key: string) => {
|
||||
if (expandedErrors.includes(key)) {
|
||||
setExpandedErrors(expandedErrors.filter((error) => error !== key))
|
||||
const onToggleHeader = (header: string) => {
|
||||
const updatedSelectedHeaders = selectedHeaders.includes(header)
|
||||
? selectedHeaders.filter((h) => h !== header)
|
||||
: selectedHeaders.concat([header])
|
||||
setSelectedHeaders(updatedSelectedHeaders)
|
||||
}
|
||||
|
||||
const onConfirm = (resolve: () => void) => {
|
||||
if (selectedHeaders.length === 0) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Please select at least one header from your CSV',
|
||||
})
|
||||
resolve()
|
||||
} else if (!isCompatible) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'The data that you are trying to import is incompatible with your table structure',
|
||||
})
|
||||
resolve()
|
||||
} else {
|
||||
setExpandedErrors(expandedErrors.concat([key]))
|
||||
saveContent({ file: uploadedFile, ...spreadsheetData, selectedHeaders, resolve })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,24 +164,34 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
size="large"
|
||||
visible={visible}
|
||||
align="right"
|
||||
header="Add content to new table"
|
||||
header={
|
||||
selectedTable !== undefined ? (
|
||||
<>
|
||||
Add data to{' '}
|
||||
<code className="text-sm">
|
||||
{selectedTable.schema}.{selectedTable.name}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
'Add content to new table'
|
||||
)
|
||||
}
|
||||
onCancel={() => closePanel()}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Cancel"
|
||||
applyButtonLabel="Save"
|
||||
applyButtonLabel={selectedTable === undefined ? 'Save' : 'Import data'}
|
||||
closePanel={closePanel}
|
||||
applyFunction={() => {
|
||||
saveContent({
|
||||
file: uploadedFile,
|
||||
...spreadsheetData,
|
||||
})
|
||||
}}
|
||||
applyFunction={onConfirm}
|
||||
/>
|
||||
}
|
||||
onInteractOutside={(event) => {
|
||||
const isToast = (event.target as Element)?.closest('#toast')
|
||||
if (isToast) event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<SidePanel.Content>
|
||||
<div className="flex flex-col">
|
||||
<div className="pt-6">
|
||||
<Tabs block type="pills">
|
||||
<Tabs.Panel id="fileUpload" label="Upload CSV">
|
||||
<SpreadSheetFileUpload
|
||||
@@ -165,70 +206,28 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{spreadsheetData.headers.length > 0 && (
|
||||
<div className="space-y-5 py-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p>Content Preview</p>
|
||||
<p className="text-scale-1000">
|
||||
Your table will have {spreadsheetData.rowCount.toLocaleString()} rows and the
|
||||
following {spreadsheetData.headers.length} columns.
|
||||
</p>
|
||||
<p className="text-scale-1000">
|
||||
Here is a preview of your table (up to the first 20 columns and first 20 rows).
|
||||
</p>
|
||||
</div>
|
||||
<SpreadsheetPreview headers={spreadsheetData.headers} rows={spreadsheetData.rows} />
|
||||
</div>
|
||||
{errors.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p>Issues found in spreadsheet</p>
|
||||
<p className="text-scale-1000">
|
||||
Your table can still be created nonetheless despite issues in the following
|
||||
rows.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{errors.map((error: any, idx: number) => {
|
||||
const key = `import-error-${idx}`
|
||||
const isExpanded = expandedErrors.includes(key)
|
||||
return (
|
||||
<div key={key} className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconChevronRight
|
||||
className={`transform cursor-pointer ${isExpanded ? 'rotate-90' : ''}`}
|
||||
size={14}
|
||||
onClick={() => onSelectExpandError(key)}
|
||||
/>
|
||||
<p className="w-14">Row: {error.row}</p>
|
||||
<p>{error.message}</p>
|
||||
{error.data?.__parsed_extra && (
|
||||
<>
|
||||
<IconArrowRight size={14} />
|
||||
<p>Extra field(s):</p>
|
||||
{error.data?.__parsed_extra.map((value: any, i: number) => (
|
||||
<code key={i} className="text-sm">{value}</code>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<SpreadsheetPreview
|
||||
headers={spreadsheetData.headers}
|
||||
rows={[error.data]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
{spreadsheetData.headers.length > 0 && (
|
||||
<>
|
||||
<div className="pt-4">
|
||||
<SidePanel.Separator />
|
||||
</div>
|
||||
<SpreadsheetImportConfiguration
|
||||
spreadsheetData={spreadsheetData}
|
||||
selectedHeaders={selectedHeaders}
|
||||
onToggleHeader={onToggleHeader}
|
||||
/>
|
||||
<SidePanel.Separator />
|
||||
<SpreadsheetImportPreview
|
||||
selectedTable={selectedTable}
|
||||
spreadsheetData={spreadsheetData}
|
||||
errors={errors}
|
||||
selectedHeaders={selectedHeaders}
|
||||
incompatibleHeaders={incompatibleHeaders}
|
||||
/>
|
||||
<SidePanel.Separator />
|
||||
</>
|
||||
)}
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+200
@@ -0,0 +1,200 @@
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
SidePanel,
|
||||
Badge,
|
||||
Button,
|
||||
Collapsible,
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconArrowRight,
|
||||
IconAlertCircle,
|
||||
} from 'ui'
|
||||
|
||||
import { SpreadsheetData } from './SpreadsheetImport.types'
|
||||
import SpreadsheetPreviewGrid from './SpreadsheetPreviewGrid'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
const MAX_ROWS = 20
|
||||
const MAX_HEADERS = 20
|
||||
|
||||
interface SpreadsheetImportPreviewProps {
|
||||
selectedTable?: PostgresTable
|
||||
spreadsheetData: SpreadsheetData
|
||||
errors?: any[]
|
||||
selectedHeaders: string[]
|
||||
incompatibleHeaders: string[]
|
||||
}
|
||||
|
||||
const SpreadsheetImportPreview = ({
|
||||
selectedTable,
|
||||
spreadsheetData,
|
||||
errors = [],
|
||||
selectedHeaders,
|
||||
incompatibleHeaders,
|
||||
}: SpreadsheetImportPreviewProps) => {
|
||||
const [expandPreview, setExpandPreview] = useState(false)
|
||||
const [expandedErrors, setExpandedErrors] = useState<string[]>([])
|
||||
|
||||
const { headers, rows } = spreadsheetData
|
||||
const previewHeaders = headers
|
||||
.filter((header) => selectedHeaders.includes(header))
|
||||
.slice(0, MAX_HEADERS)
|
||||
const previewRows = rows.slice(0, MAX_ROWS)
|
||||
|
||||
const isCompatible = selectedTable !== undefined ? incompatibleHeaders.length === 0 : true
|
||||
|
||||
useEffect(() => {
|
||||
setExpandPreview(true)
|
||||
}, [spreadsheetData])
|
||||
|
||||
const onSelectExpandError = (key: string) => {
|
||||
if (expandedErrors.includes(key)) {
|
||||
setExpandedErrors(expandedErrors.filter((error) => error !== key))
|
||||
} else {
|
||||
setExpandedErrors(expandedErrors.concat([key]))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible open={expandPreview} onOpenChange={setExpandPreview} className={''}>
|
||||
<Collapsible.Trigger asChild>
|
||||
<SidePanel.Content>
|
||||
<div className="py-1 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm">Preview data to be imported</p>
|
||||
{!isCompatible && <Badge color="red">Data incompatible</Badge>}
|
||||
{errors.length > 0 && <Badge color="yellow">{errors.length} issues found</Badge>}
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={
|
||||
<IconChevronDown
|
||||
size={18}
|
||||
strokeWidth={2}
|
||||
className={clsx('text-scale-1100', expandPreview && 'rotate-180')}
|
||||
/>
|
||||
}
|
||||
className="px-1"
|
||||
onClick={() => setExpandPreview(!expandPreview)}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<SidePanel.Content>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-scale-1000">
|
||||
{selectedTable === undefined
|
||||
? `Your table will have ${spreadsheetData.rowCount.toLocaleString()} rows and the
|
||||
following ${spreadsheetData.headers.length} columns.`
|
||||
: `A total of ${spreadsheetData.rowCount.toLocaleString()} rows will be added to the table "${
|
||||
selectedTable.name
|
||||
}"`}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
Here is a preview of the data that will be added (up to the first 20 columns and first
|
||||
20 rows).
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
{previewHeaders.length > 0 && previewRows.length > 0 ? (
|
||||
<SpreadsheetPreviewGrid height={350} headers={previewHeaders} rows={previewRows} />
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-4 border border-scale-600 rounded-md space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} className="text-scale-1000" />
|
||||
<p className="text-sm text-scale-1000">
|
||||
{previewHeaders.length === 0
|
||||
? 'No headers have been selected'
|
||||
: previewRows.length === 0
|
||||
? 'Your CSV contains no data'
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(!isCompatible || errors.length > 0) && (
|
||||
<div className="space-y-2 my-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm">Issues found in spreadsheet</p>
|
||||
{isCompatible && (
|
||||
<p className="text-sm text-scale-1000">
|
||||
{selectedTable !== undefined
|
||||
? 'This CSV can still be imported into your table despite issues in the following rows.'
|
||||
: 'Your table can still be created nonetheless despite issues in the following rows.'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{!isCompatible && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="w-[14px] h-[14px] flex items-center justify-center translate-y-[3px]">
|
||||
<div className="w-[6px] h-[6px] rounded-full bg-scale-1000" />
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
This CSV <span className="text-red-900">cannot</span> be imported into your
|
||||
table due to incompatible headers:
|
||||
<br />
|
||||
The column{incompatibleHeaders.length > 1 ? 's' : ''}{' '}
|
||||
{incompatibleHeaders.map((x) => `"${x}"`).join(', ')}{' '}
|
||||
{incompatibleHeaders.length > 1 ? 'are' : 'is'} not present in your table
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{errors.map((error: any, idx: number) => {
|
||||
const key = `import-error-${idx}`
|
||||
const isExpanded = expandedErrors.includes(key)
|
||||
|
||||
return (
|
||||
<div key={key} className="space-y-2">
|
||||
<div
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
onClick={() => onSelectExpandError(key)}
|
||||
>
|
||||
{error.data !== undefined ? (
|
||||
<IconChevronRight
|
||||
size={14}
|
||||
className={`transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[14px] h-[14px] flex items-center justify-center">
|
||||
<div className="w-[6px] h-[6px] rounded-full bg-scale-1000" />
|
||||
</div>
|
||||
)}
|
||||
{error.data !== undefined && (
|
||||
<p className="text-sm w-14">Row: {error.row}</p>
|
||||
)}
|
||||
<p className="text-sm">{error.message}</p>
|
||||
{error.data?.__parsed_extra && (
|
||||
<>
|
||||
<IconArrowRight size={14} />
|
||||
<p className="text-sm">Extra field(s):</p>
|
||||
{error.data?.__parsed_extra.map((value: any, i: number) => (
|
||||
<code key={i} className="text-xs">
|
||||
{value}
|
||||
</code>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{error.data !== undefined && isExpanded && (
|
||||
<SpreadsheetPreviewGrid
|
||||
headers={spreadsheetData.headers}
|
||||
rows={[error.data]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpreadsheetImportPreview
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import clsx from 'clsx'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
|
||||
const MAX_ROWS = 20
|
||||
const MAX_HEADERS = 20
|
||||
|
||||
interface SpreadsheetPreviewGridProps {
|
||||
headers: string[]
|
||||
rows?: any[]
|
||||
height?: number
|
||||
}
|
||||
|
||||
const SpreadsheetPreviewGrid = ({
|
||||
headers = [],
|
||||
rows = [],
|
||||
height,
|
||||
}: SpreadsheetPreviewGridProps) => {
|
||||
const previewHeaders = headers.slice(0, MAX_HEADERS)
|
||||
const previewRows = rows.slice(0, MAX_ROWS)
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
columns={previewHeaders.map((header) => {
|
||||
const columnValues = previewRows.map((row) => row[header]?.toString() ?? '')
|
||||
const maxLength = Math.max(...columnValues.map((el) => el.length))
|
||||
const maxWidth = maxLength > 20 ? 200 : maxLength * 10
|
||||
|
||||
return {
|
||||
key: header,
|
||||
name: header,
|
||||
width: maxWidth,
|
||||
resizable: true,
|
||||
headerRenderer: () => (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-xs">{header}</p>
|
||||
</div>
|
||||
),
|
||||
formatter: ({ row }: { row: any }) => {
|
||||
const isEmpty = !row[header]
|
||||
return (
|
||||
<span className={clsx('text-sm flex items-center', isEmpty && 'text-scale-1000')}>
|
||||
{isEmpty ? 'NULL' : row[header]}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
}
|
||||
})}
|
||||
rows={previewRows}
|
||||
className="!border-l !border-r"
|
||||
style={{ height: height || `${34 + 34 * (previewRows.length || 1)}px` }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpreadsheetPreviewGrid
|
||||
+4
-6
@@ -163,12 +163,10 @@ const ColumnManagement: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{hasImportContent && (
|
||||
<div className="my-2 opacity-75">
|
||||
<p>
|
||||
Your table will be created with {importContent?.rowCount?.toLocaleString()} rows and
|
||||
the following {columns.length} columns.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-scale-1000 my-2">
|
||||
Your table will be created with {importContent?.rowCount?.toLocaleString()} rows and the
|
||||
following {columns.length} columns.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{primaryKeyColumns.length === 0 && (
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { FC } from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
|
||||
interface Props {
|
||||
headers: string[]
|
||||
rows?: any[]
|
||||
}
|
||||
|
||||
const MAX_ROWS = 20
|
||||
const MAX_HEADERS = 20
|
||||
|
||||
const SpreadsheetPreview: FC<Props> = ({ headers = [], rows = [] }) => {
|
||||
const previewHeaders = headers.slice(0, MAX_HEADERS)
|
||||
const previewRows = rows.slice(0, MAX_ROWS)
|
||||
return (
|
||||
<DataGrid
|
||||
columns={previewHeaders.map((header) => {
|
||||
return {
|
||||
key: header,
|
||||
name: header,
|
||||
width: header.length * 10,
|
||||
resizable: true,
|
||||
headerRenderer: () => (
|
||||
<div className="flex items-center justify-center font-mono h-full">
|
||||
<p className="text-sm">{header}</p>
|
||||
</div>
|
||||
),
|
||||
formatter: ({ row }: { row: any }) => (
|
||||
<span className="font-mono text-xs">
|
||||
{isUndefined(row[header]) ? 'NULL' : row[header]}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
})}
|
||||
rows={previewRows}
|
||||
className="!border-l !border-r"
|
||||
style={{ height: `${34 + 34 * (previewRows.length || 1)}px` }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpreadsheetPreview
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import Link from 'next/link'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { isUndefined, isEmpty } from 'lodash'
|
||||
import { Badge, Checkbox, SidePanel, Input, Alert, IconBookOpen, Button } from 'ui'
|
||||
@@ -7,7 +8,7 @@ import { useStore } from 'hooks'
|
||||
import ActionBar from '../ActionBar'
|
||||
import HeaderTitle from './HeaderTitle'
|
||||
import ColumnManagement from './ColumnManagement'
|
||||
import SpreadsheetImport from './SpreadsheetImport/SpreadsheetImport'
|
||||
import { SpreadsheetImport } from '../'
|
||||
import { ColumnField, CreateTablePayload, UpdateTablePayload } from '../SidePanelEditor.types'
|
||||
import { DEFAULT_COLUMNS } from './TableEditor.constants'
|
||||
import { TableField, ImportContent } from './TableEditor.types'
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
} from './TableEditor.utils'
|
||||
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Props {
|
||||
table?: PostgresTable
|
||||
|
||||
+2
@@ -16,4 +16,6 @@ export interface ImportContent {
|
||||
rowCount: number
|
||||
rows: object[]
|
||||
columnTypeMap: Dictionary<any>
|
||||
selectedHeaders: string[]
|
||||
resolve: () => void
|
||||
}
|
||||
+7
-4
@@ -55,9 +55,12 @@ export const generateTableFieldFromPostgresTable = (
|
||||
}
|
||||
|
||||
export const formatImportedContentToColumnFields = (importContent: ImportContent) => {
|
||||
const columnFields = importContent.headers.map((header: string) => {
|
||||
const columnType = importContent.columnTypeMap[header]
|
||||
return generateColumnField({ name: header, format: columnType })
|
||||
})
|
||||
const { headers, selectedHeaders, columnTypeMap } = importContent
|
||||
const columnFields = headers
|
||||
.filter((header) => selectedHeaders.includes(header))
|
||||
.map((header) => {
|
||||
const columnType = columnTypeMap[header]
|
||||
return generateColumnField({ name: header, format: columnType })
|
||||
})
|
||||
return columnFields
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import SidePanelEditor from './SidePanelEditor'
|
||||
import RowEditor from './RowEditor/RowEditor'
|
||||
import ColumnEditor from './ColumnEditor/ColumnEditor'
|
||||
import TableEditor from './TableEditor/TableEditor'
|
||||
import SpreadsheetImport from './SpreadsheetImport/SpreadsheetImport'
|
||||
import ForeignKeySelector from './ForeignKeySelector/ForeignKeySelector'
|
||||
|
||||
export { RowEditor, ColumnEditor, TableEditor, ForeignKeySelector }
|
||||
export { RowEditor, ColumnEditor, TableEditor, ForeignKeySelector, SpreadsheetImport }
|
||||
export default SidePanelEditor
|
||||
@@ -44,7 +44,7 @@ export interface TableGridEditorProps {
|
||||
selectedTable: any // PostgresTable | SchemaView
|
||||
|
||||
/** Determines what side panel editor to show */
|
||||
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector'
|
||||
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
|
||||
/** Toggles if we're duplicating a table */
|
||||
isDuplicating: boolean
|
||||
/** Selected entities if we're editing a row, column or table */
|
||||
@@ -70,6 +70,7 @@ export interface TableGridEditorProps {
|
||||
column: any
|
||||
}) => void
|
||||
onClosePanel: () => void
|
||||
onImportData: () => void
|
||||
}
|
||||
|
||||
const TableGridEditor = ({
|
||||
@@ -93,6 +94,7 @@ const TableGridEditor = ({
|
||||
onExpandJSONEditor = noop,
|
||||
onEditForeignKeyColumnValue = noop,
|
||||
onClosePanel = noop,
|
||||
onImportData = noop,
|
||||
}: TableGridEditorProps) => {
|
||||
const { meta, ui, vault } = useStore()
|
||||
const router = useRouter()
|
||||
@@ -381,6 +383,7 @@ const TableGridEditor = ({
|
||||
onAddRow={onAddRow}
|
||||
updateTableRow={updateTableRow}
|
||||
onEditRow={onEditRow}
|
||||
onImportData={onImportData}
|
||||
onError={onError}
|
||||
onSqlQuery={onSqlQuery}
|
||||
onExpandJSONEditor={onExpandJSONEditor}
|
||||
|
||||
@@ -44,7 +44,7 @@ const TableEditorPage: NextPageWithLayout = () => {
|
||||
const [selectedTableToDelete, setSelectedTableToDelete] = useState<PostgresTable>()
|
||||
|
||||
const [sidePanelKey, setSidePanelKey] = useState<
|
||||
'row' | 'column' | 'table' | 'json' | 'foreign-row-selector'
|
||||
'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
|
||||
>()
|
||||
const [selectedRowToEdit, setSelectedRowToEdit] = useState<Dictionary<any>>()
|
||||
const [selectedColumnToEdit, setSelectedColumnToEdit] = useState<PostgresColumn>()
|
||||
@@ -137,6 +137,16 @@ const TableEditorPage: NextPageWithLayout = () => {
|
||||
setSelectedValueForJsonEdit({ column, row, jsonString: JSON.stringify(row[column]) || '' })
|
||||
}
|
||||
|
||||
const onImportData = () => {
|
||||
if (id) {
|
||||
setSidePanelKey('csv-import')
|
||||
const table = meta.tables.byId(id)
|
||||
setSelectedTableToEdit(table)
|
||||
} else {
|
||||
console.error('Table ID not found')
|
||||
}
|
||||
}
|
||||
|
||||
const onEditForeignKeyColumnValue = ({
|
||||
foreignKey,
|
||||
row,
|
||||
@@ -280,6 +290,7 @@ const TableEditorPage: NextPageWithLayout = () => {
|
||||
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
|
||||
onClosePanel={onClosePanel}
|
||||
theme={ui.themeOption == 'dark' ? 'dark' : 'light'}
|
||||
onImportData={onImportData}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
danger
|
||||
|
||||
@@ -129,7 +129,12 @@ export interface IMetaStore {
|
||||
columns: ColumnField[],
|
||||
isRealtimeEnabled: boolean
|
||||
) => any
|
||||
|
||||
insertRowsViaSpreadsheet: (
|
||||
file: any,
|
||||
table: PostgresTable,
|
||||
selectedHeaders: string[],
|
||||
onProgressUpdate: (progress: number) => void
|
||||
) => void
|
||||
setProjectDetails: (details: { ref: string; connectionString?: string }) => void
|
||||
}
|
||||
export default class MetaStore implements IMetaStore {
|
||||
@@ -652,6 +657,7 @@ export default class MetaStore implements IMetaStore {
|
||||
const { error }: any = await this.insertRowsViaSpreadsheet(
|
||||
importContent.file,
|
||||
table,
|
||||
importContent.selectedHeaders,
|
||||
(progress: number) => {
|
||||
this.rootStore.ui.setNotification({
|
||||
id: toastId,
|
||||
@@ -834,6 +840,7 @@ export default class MetaStore implements IMetaStore {
|
||||
async insertRowsViaSpreadsheet(
|
||||
file: any,
|
||||
table: PostgresTable,
|
||||
selectedHeaders: string[],
|
||||
onProgressUpdate: (progress: number) => void
|
||||
) {
|
||||
let chunkNumber = 0
|
||||
@@ -849,9 +856,15 @@ export default class MetaStore implements IMetaStore {
|
||||
chunk: async (results: any, parser: any) => {
|
||||
parser.pause()
|
||||
|
||||
const formattedData = results.data.map((row: any) => {
|
||||
const formattedRow: any = {}
|
||||
selectedHeaders.forEach((header) => (formattedRow[header] = row[header]))
|
||||
return formattedRow
|
||||
})
|
||||
|
||||
const insertQuery = new Query()
|
||||
.from(table.name, table.schema)
|
||||
.insert(results.data)
|
||||
.insert(formattedData)
|
||||
.toSql()
|
||||
const res = await this.query(insertQuery)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inferColumnType } from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils'
|
||||
import { inferColumnType } from 'components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils'
|
||||
|
||||
describe('SpreadsheedImport.utils: inferColumnType', () => {
|
||||
test('should default column type to text if no rows to infer from', () => {
|
||||
|
||||
Reference in new issue
Block a user