Skip to content

Commit

Permalink
Merge pull request #1475 from undb-xyz/feature/merge-data
Browse files Browse the repository at this point in the history
feature: merge data
  • Loading branch information
nichenqin authored Sep 1, 2023
2 parents 7d05587 + a37ea4b commit 6c943d1
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 2 deletions.
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

0 comments on commit 6c943d1

Please sign in to comment.