Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: merge data #1475

Merged
merged 8 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions apps/frontend/src/lib/import/MergeData.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<script lang="ts">
import { t } from '$lib/i18n'
import { mergeDataModal } from '$lib/store/modal'
import { Alert, Badge, Button, Dropzone, Modal, Tooltip } from 'flowbite-svelte'
import { parse, type SheetData } from './import.helper'
import { getTable } from '$lib/store/table'
import { castFieldValue, type IFieldType, type IMutateRecordValueSchema } from '@undb/core'
import { trpc } from '$lib/trpc/client'
import { includes, isEmpty } from 'lodash-es'
import FieldIcon from '$lib/field/FieldIcon.svelte'

const table = getTable()

let data: SheetData | undefined

const unsupportedMergeType: IFieldType[] = ['reference', 'attachment', 'parent', 'tree', 'collaborator']

const createRecords = trpc().record.buldCreate.mutation({
onSuccess(data, variables, context) {
mergeDataModal.close()
},
})

$: header = data?.[0] ?? []
$: importHeaders = header.map((title) => schema.get(String(title))!).filter(Boolean)
$: unsupportedFields = importHeaders.filter((field) => unsupportedMergeType.includes(field.type))
$: body = data?.slice(1)

$: schema = $table.schema.toNameMap()

$: records = body
?.map((values) => {
return values.reduce((prev, value, index) => {
const title = header?.[index]
if (!title) return prev

const field = schema.get(String(title))
if (!field || field.controlled) return prev

const type = field.type

// TODO: support these field types
if (unsupportedMergeType.includes(type)) return prev

prev[field.id.value] = type ? castFieldValue(type, value) : value
return prev
}, {} as IMutateRecordValueSchema)
})
.filter((value) => !isEmpty(value))

let ext: string | undefined
let file: File | undefined

const dropHandle = async (event: DragEvent) => {
event.preventDefault()
const files = event.dataTransfer?.files
if (!!files?.length) {
file = files[0]
}
}

const handleChange = async (event: Event) => {
const target = event.target as HTMLInputElement
const files = target.files
if (!!files?.length) {
file = files[0]
}
}

const handleFile = async (file: File | undefined) => {
if (!file) return

const parsed = await parse(file)
data = parsed.data
ext = parsed.extension
}

$: if (file) {
handleFile(file)
}
</script>

<Modal class="w-full" bind:open={$mergeDataModal.open}>
<Dropzone
accept=".csv, .json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
id="dropzone"
on:drop={dropHandle}
on:dragover={(event) => {
event.preventDefault()
}}
on:change={handleChange}
>
<svg
aria-hidden="true"
class="mb-3 w-10 h-10 text-gray-400 dark:text-gray-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
{@html $t('click to upload or dnd', { ns: 'common' })}
</p>
</Dropzone>

{#if !!unsupportedFields.length}
<Alert color="yellow">
{$t('unsupport merge')}
<div class="flex items-center gap-2 mt-2">
{#each unsupportedFields as field}
<Badge color="dark" class="inline-flex items-center gap-2">
<FieldIcon type={field.type} />
{field.name.value}
</Badge>
<Tooltip>
{$t(field.type)}
</Tooltip>
{/each}
</div>
</Alert>
{/if}

<div class="flex justify-end items-center gap-2">
<Button size="xs" type="button" outline color="alternative" on:click={() => mergeDataModal.close()}>
{$t('Cancel', { ns: 'common' })}
</Button>
<Button
size="xs"
disabled={!data || $createRecords.isLoading}
on:click={() => {
if (!records?.length) return
$createRecords.mutate({
tableId: $table.id.value,
records: records.map((record) => ({ values: record })),
})
}}
>
<div class="flex items-center">
{$t('Confirm', { ns: 'common' })}
</div>
</Button>
{#if records?.length}
<Tooltip>
{$t('merge record count', { count: records.length })}
</Tooltip>
{/if}
</div>
</Modal>
3 changes: 3 additions & 0 deletions apps/frontend/src/lib/store/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const createOptionModal = createModal(CREATE_OPTION)
const UPDATE_OPTION = Symbol('UPDATE_OPTION')
export const updateOptionModal = createModal(UPDATE_OPTION)

const MERGE_DATA_MODAL = Symbol('MERGE_DATA_MODAL')
export const mergeDataModal = createModal(MERGE_DATA_MODAL)

const IMPORT_DATA_MODAL = Symbol('IMPORT_DATA_MODAL')
export const importDataModal = createModal(IMPORT_DATA_MODAL)

Expand Down
13 changes: 12 additions & 1 deletion apps/frontend/src/lib/table/TableMenu.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { t } from '$lib/i18n'
import { webhookListDrawer } from '$lib/store/drawer'
import { erdModal, rlsModal } from '$lib/store/modal'
import { erdModal, mergeDataModal, rlsModal } from '$lib/store/modal'
import { currentRLSS } from '$lib/store/table'
import { Badge, Dropdown, DropdownItem } from 'flowbite-svelte'
import { hasPermission } from '$lib/store/authz'
Expand Down Expand Up @@ -47,4 +47,15 @@
{/if}
</DropdownItem>
{/if}
{#if $hasPermission('table:merge_data')}
<DropdownItem
on:click={() => {
mergeDataModal.open()
}}
class="text-xs font-normal inline-flex items-center gap-2"
>
<i class="ti ti-database-import text-gray-600 dark:text-gray-50" />
<span>{$t('merge data')}</span>
</DropdownItem>
{/if}
</Dropdown>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import { match } from 'ts-pattern'
import RLSModal from '$lib/authz/rls/RLSModal.svelte'
import FlsModal from '$lib/authz/fls/FLSModal.svelte'
import MergeData from '$lib/import/MergeData.svelte'

const table = getTable()
export let data: PageData
Expand Down Expand Up @@ -85,6 +86,7 @@

<TableIndex />

<MergeData />
{#key $table}
<UpdateTable data={data.updateTable} />
{/key}
Expand Down
5 changes: 5 additions & 0 deletions packages/authz/src/rbac/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const tableActions = z.enum([
'table:list_form',
'table:create_form',
'table:update_form',
'table:merge_data',
])
export const recordActions = z.enum(['record:create', 'record:update', 'record:delete', 'record:list_trash'])
export const webhookActions = z.enum(['webhook:create', 'webhook:update', 'webhook:delete'])
Expand Down Expand Up @@ -81,6 +82,7 @@ export const permissions: Record<IRoles, Record<PermissionAction, boolean>> = {
'table:set_view_field': true,
'table:delete_view': true,
'table:duplicate_view': true,
'table:merge_data': true,
'record:create': true,
'record:delete': true,
'record:update': true,
Expand Down Expand Up @@ -131,6 +133,7 @@ export const permissions: Record<IRoles, Record<PermissionAction, boolean>> = {
'table:set_view_field': true,
'table:delete_view': true,
'table:duplicate_view': true,
'table:merge_data': true,
'table:create_form': true,
'table:update_form': true,
'record:create': true,
Expand Down Expand Up @@ -185,6 +188,7 @@ export const permissions: Record<IRoles, Record<PermissionAction, boolean>> = {
'table:list_form': true,
'table:create_form': true,
'table:update_form': true,
'table:merge_data': true,
'record:create': true,
'record:delete': true,
'record:update': true,
Expand Down Expand Up @@ -237,6 +241,7 @@ export const permissions: Record<IRoles, Record<PermissionAction, boolean>> = {
'table:list_form': false,
'table:create_form': false,
'table:update_form': false,
'table:merge_data': false,
'record:create': false,
'record:delete': false,
'record:update': false,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/table/field/field.util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TFunction } from 'i18next'
import { isBoolean, isNumber, isPlainObject, isString, uniq } from 'lodash-es'
import { castArray, isBoolean, isNumber, isPlainObject, isString, uniq } from 'lodash-es'
import { match } from 'ts-pattern'
import { z } from 'zod'
import { Options } from '../option'
Expand Down Expand Up @@ -708,5 +708,6 @@ export const castFieldValue = (type: IFieldType, value: string | number | null |
.otherwise(Boolean),
)
.with('select', () => value || null)
.with('multi-select', 'reference', 'tree', 'collaborator', () => (value ? castArray(value) : null))
.otherwise(() => value)
}
6 changes: 6 additions & 0 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,9 @@ export const config: InitOptions = {
'set date': 'set date',
'recycle bin': 'recycle bin',
'show form conditions': 'show conditions',
'merge data': 'merge data',
'unsupport merge': 'unsupport field types to merge into table',
'merge record count': 'will merge {{count}} record(s)',
},
webhook: {
Webhook: 'Webhook',
Expand Down Expand Up @@ -767,6 +770,9 @@ export const config: InitOptions = {
'set date': '设置日期',
'recycle bin': '回收站',
'show form conditions': '显示过滤',
'merge data': '合并数据',
'unsupport merge': '不支持部分列合并到表格',
'merge record count': '合并 {{count}} 条记录',
},
webhook: {
Webhook: 'Webhook',
Expand Down
9 changes: 9 additions & 0 deletions packages/trpc/src/router/record.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BulkDeleteRecordsCommand,
BulkDuplicateRecordsCommand,
CreateRecordCommand,
CreateRecordsCommand,
DeleteRecordCommand,
DuplicateRecordCommand,
GetForeignRecordsQuery,
Expand Down Expand Up @@ -42,6 +43,14 @@ export const createRecordRouter =
const cmd = new CreateRecordCommand(input)
return commandBus.execute(cmd)
}),
buldCreate: procedure
.use(authz('record:create'))
.input(z.any())
.output(z.any())
.mutation(({ input }) => {
const cmd = new CreateRecordsCommand(input)
return commandBus.execute(cmd)
}),
duplicate: procedure
.use(authz('record:create'))
.input(duplicateRecordCommandInput)
Expand Down