Display original data for tables (#10413)

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2026-01-18 13:10:47 +07:00
committed by GitHub
parent 8bc8183cb8
commit 7a8a75b8b4
3 changed files with 188 additions and 7 deletions
@@ -0,0 +1,93 @@
<!--
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import presentation, { Card } from '@hcengineering/presentation'
import textEditor from '@hcengineering/text-editor'
import { Component, Loading } from '@hcengineering/ui'
import type { BuildModelKey, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
export let _class: Ref<Class<Doc>>
export let config: Array<string | BuildModelKey> | undefined = undefined
export let documentIds: string[]
export let query: DocumentQuery<Doc> | undefined = undefined
export let viewlet: Viewlet | undefined = undefined
export let viewletWithLookup: any | undefined = undefined
const dispatch = createEventDispatcher()
// Build query from documentIds if available (preserves order), otherwise use provided query
let tableQuery: DocumentQuery<Doc>
$: tableQuery = documentIds.length > 0 ? { _id: { $in: documentIds as any[] } } : (query ?? {})
// Use config from viewlet if available, otherwise use metadata config
// RelationshipTable doesn't use viewlets, so config comes directly from metadata
// Ensure config is never empty - use default empty string key for default presenter
$: viewletConfig = viewlet?.config
$: tableConfig =
viewletConfig !== undefined && viewletConfig.length > 0
? viewletConfig
: config !== undefined && config.length > 0
? config
: ['']
// Get component from viewlet descriptor lookup
$: viewletComponent = viewletWithLookup?.$lookup?.descriptor?.component
function handleClose (): void {
dispatch('close')
}
</script>
<Card
label={textEditor.string.SeeOriginalData}
fullSize={true}
canSave={true}
okLabel={presentation.string.Ok}
okAction={handleClose}
onCancel={handleClose}
on:close={handleClose}
>
<div class="original-table-container">
{#if viewletComponent}
<Component
is={viewletComponent}
props={{
_class,
config: tableConfig,
query: tableQuery,
totalQuery: tableQuery,
options: viewlet?.options,
readonly: true,
viewlet,
viewOptions: viewlet?.viewOptions,
viewOptionsConfig: viewlet?.viewOptions?.other
}}
/>
{:else}
<Loading />
{/if}
</div>
</Card>
<style lang="scss">
.original-table-container {
padding: 1rem;
overflow: auto;
max-height: 80vh;
}
</style>
@@ -13,17 +13,93 @@
//
import { type Editor } from '@tiptap/core'
import type { Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { showPopup } from '@hcengineering/ui'
import view, { type Viewlet } from '@hcengineering/view'
import { findTable } from '../utils'
import { getTableMetadata } from '../tableMetadata'
import OriginalTableDataViewer from './OriginalTableDataViewer.svelte'
/**
* Show original table data from when it was first created
*/
export async function seeOriginalTableData (editor: Editor): Promise<void> {
const table = findTable(editor.state.selection)
if (table === undefined) return
if (table === undefined) {
console.warn('No table found to show original data')
return
}
const metadata = getTableMetadata(table.node)
if (metadata === null || metadata === undefined) return
// Empty handler for now
console.log('See original data:', metadata)
if (metadata === null || metadata === undefined) {
console.warn('Table has no metadata to show original data')
return
}
try {
const client = getClient()
const hierarchy = client.getHierarchy()
// Convert string cardClass to Ref<Class<Doc>> and validate
const cardClassRef = metadata.cardClass as any
const cardClass = hierarchy.getClass(cardClassRef)
if (cardClass == null) {
console.warn('Invalid cardClass in table metadata:', metadata.cardClass)
return
}
// Validate that we have documentIds or query to fetch original data
if (
(metadata.documentIds === undefined || metadata.documentIds.length === 0) &&
(metadata.query === null || metadata.query === undefined)
) {
console.warn('Table metadata has no documentIds or query to show original data')
return
}
// Load viewlet with descriptor lookup if viewletId is provided (to get config and component)
// RelationshipTable has viewletId: undefined (as per buildRelationshipTableMetadata)
let viewlet: Viewlet | undefined
let viewletWithLookup: any | undefined
if (metadata.viewletId !== undefined) {
const viewlets = await client.findAll(
view.class.Viewlet,
{ _id: metadata.viewletId as Ref<Viewlet> },
{ lookup: { descriptor: view.class.ViewletDescriptor } }
)
viewlet = viewlets[0]
viewletWithLookup = viewlets[0]
} else {
// For RelationshipTable (no viewletId), load Table descriptor as fallback
// Table component can display both regular and relationship tables
const descriptors = await client.findAll(view.class.ViewletDescriptor, {
_id: view.viewlet.Table
})
if (descriptors.length > 0) {
viewletWithLookup = { $lookup: { descriptor: descriptors[0] } }
}
}
// Use documentIds if available (preserves original order), otherwise use query
const documentIds = metadata.documentIds ?? []
// Show original data in popup
// Note: Component is resolved from viewlet descriptor or Table descriptor as fallback
// RelationshipTable metadata has viewletId: undefined, so config comes from metadata.config
showPopup(
OriginalTableDataViewer,
{
_class: cardClassRef as Ref<Class<Doc>>,
config: metadata.config,
documentIds: documentIds.length > 0 ? documentIds : [],
query: documentIds.length === 0 ? metadata.query : undefined,
viewlet,
viewletWithLookup
},
'center'
)
} catch (error) {
console.error('Failed to show original table data:', error)
}
}
@@ -496,12 +496,24 @@ export interface TableMetadata {
/**
* Build metadata object from props and documents
* If viewlet is not provided, tries to find a default viewlet for the class
*/
function buildTableMetadata (props: CopyAsMarkdownTableProps, docs: Doc[]): TableMetadata {
async function buildTableMetadata (
props: CopyAsMarkdownTableProps,
docs: Doc[],
client?: Client
): Promise<TableMetadata> {
// If viewlet is not provided, try to find a default viewlet for the class
let viewletId: Ref<Viewlet> | undefined = props.viewlet?._id
if (viewletId === undefined && client !== undefined) {
const { viewlet } = await loadViewletConfig(client, client.getHierarchy(), props.cardClass, undefined, props.config)
viewletId = viewlet?._id
}
return {
version: '1.0',
cardClass: props.cardClass,
viewletId: props.viewlet?._id,
viewletId,
config: props.config,
query: props.query,
documentIds: docs.map((d) => d._id),
@@ -689,7 +701,7 @@ export async function CopyAsMarkdownTable (
}
// Build metadata for table refresh/diff functionality
const metadata = buildTableMetadata(props, docs)
const metadata = await buildTableMetadata(props, docs, client)
await copyMarkdown(markdown, metadata)
const language = getCurrentLanguage()