-
Notifications
You must be signed in to change notification settings - Fork 392
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
CB-4539 fix: value panel auto type detection #2320
Changes from 4 commits
cf058f6
183068f
c472562
06635c2
dcca8ca
85756a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; | |
import { useMemo } from 'react'; | ||
import styled, { css } from 'reshadow'; | ||
|
||
import { Button, s, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; | ||
import { ActionIconButton, Button, Container, Fill, Group, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; | ||
import { useService } from '@cloudbeaver/core-di'; | ||
import { NotificationService } from '@cloudbeaver/core-events'; | ||
import { QuotasService } from '@cloudbeaver/core-root'; | ||
|
@@ -26,33 +26,13 @@ import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValu | |
import { QuotaPlaceholder } from '../QuotaPlaceholder'; | ||
import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES'; | ||
import { getTypeExtension } from './getTypeExtension'; | ||
import moduleStyles from './TextValuePresentation.m.css'; | ||
import { TextValuePresentationService } from './TextValuePresentationService'; | ||
import { useTextValue } from './useTextValue'; | ||
|
||
const styles = css` | ||
Tab { | ||
composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global; | ||
} | ||
container { | ||
display: flex; | ||
gap: 8px; | ||
flex-direction: column; | ||
overflow: auto; | ||
flex: 1; | ||
} | ||
actions { | ||
display: flex; | ||
justify-content: center; | ||
flex: 0; | ||
} | ||
EditorLoader { | ||
border-radius: var(--theme-group-element-radius); | ||
} | ||
EditorLoader { | ||
flex: 1; | ||
overflow: auto; | ||
} | ||
TabList { | ||
composes: theme-border-color-background theme-background-background from global; | ||
overflow: auto; | ||
|
@@ -68,9 +48,7 @@ const styles = css` | |
} | ||
`; | ||
|
||
const TEXT_PLAIN_TYPE = 'text/plain'; | ||
const TEXT_JSON_TYPE = 'text/json'; | ||
const APPLICATION_JSON_TYPE = 'application/json'; | ||
const DEFAULT_CONTENT_TYPE = 'text/plain'; | ||
|
||
export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelProps<any, IDatabaseResultSet>> = observer( | ||
function TextValuePresentation({ model, resultIndex, dataFormat }) { | ||
|
@@ -79,7 +57,6 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr | |
const quotasService = useService(QuotasService); | ||
const textValuePresentationService = useService(TextValuePresentationService); | ||
const style = useStyles(styles, UNDERLINE_TAB_STYLES, VALUE_PANEL_TOOLS_STYLES); | ||
const moduleStyle = useS(moduleStyles); | ||
const selection = model.source.getAction(resultIndex, ResultSetSelectAction); | ||
const activeElements = selection.getActiveElements(); | ||
const firstSelectedCell = activeElements?.[0]; | ||
|
@@ -95,27 +72,53 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr | |
const contentValue = firstSelectedCell ? formatAction.get(firstSelectedCell) : null; | ||
const state = useTabLocalState(() => | ||
observable({ | ||
currentContentType: TEXT_PLAIN_TYPE, | ||
|
||
setContentType(contentType: string) { | ||
if (contentType === TEXT_JSON_TYPE) { | ||
contentType = APPLICATION_JSON_TYPE; | ||
} | ||
lineWrapping: null as boolean | null, | ||
currentContentType: null as string | null, | ||
|
||
setContentType(contentType: string | null) { | ||
this.currentContentType = contentType; | ||
}, | ||
handleTabOpen(tabId: string) { | ||
// currentContentType may be selected automatically we don't want to change state in this case | ||
if (tabId !== this.currentContentType) { | ||
this.setContentType(tabId); | ||
} | ||
setLineWrapping(lineWrapping: boolean | null) { | ||
this.lineWrapping = lineWrapping; | ||
}, | ||
}), | ||
); | ||
|
||
let contentType = state.currentContentType; | ||
let autoContentType = DEFAULT_CONTENT_TYPE; | ||
let autoLineWrapping = false; | ||
|
||
if (isResultSetContentValue(contentValue)) { | ||
if (contentValue.contentType) { | ||
switch (contentValue.contentType) { | ||
case 'text/json': | ||
autoContentType = 'application/json'; | ||
break; | ||
case 'application/octet-stream': | ||
autoContentType = 'application/octet-stream;type=base64'; | ||
autoLineWrapping = true; | ||
break; | ||
default: | ||
autoContentType = contentValue.contentType; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
if (contentType === null) { | ||
contentType = autoContentType ?? DEFAULT_CONTENT_TYPE; | ||
} | ||
|
||
if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === contentType)) { | ||
contentType = activeTabs[0].key; | ||
} | ||
|
||
const lineWrapping = state.lineWrapping ?? autoLineWrapping; | ||
|
||
const { textValue, isTruncated, isTextColumn, pasteFullText } = useTextValue({ | ||
model, | ||
resultIndex, | ||
currentContentType: state.currentContentType, | ||
currentContentType: contentType, | ||
}); | ||
const isSelectedCellReadonly = firstSelectedCell && (formatAction.isReadOnly(firstSelectedCell) || formatAction.isBinary(firstSelectedCell)); | ||
const isReadonlyByResultIndex = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || !firstSelectedCell; | ||
|
@@ -125,16 +128,16 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr | |
const limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength')); | ||
const canSave = firstSelectedCell && contentAction.isDownloadable(firstSelectedCell); | ||
const shouldShowPasteButton = isTextColumn && isTruncated; | ||
const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); | ||
const typeExtension = useMemo(() => getTypeExtension(contentType!) ?? [], [contentType]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we go with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no we shouldn't because this is typescript check and we will never have contentType = null at this point (only if we will broke something) |
||
const extensions = useCodemirrorExtensions(undefined, typeExtension); | ||
|
||
function handleChange(newValue: string) { | ||
function valueChangeHandler(newValue: string) { | ||
if (firstSelectedCell && !isReadonly) { | ||
editAction.set(firstSelectedCell, newValue); | ||
} | ||
} | ||
|
||
async function save() { | ||
async function saveHandler() { | ||
if (!firstSelectedCell) { | ||
return; | ||
} | ||
|
@@ -146,41 +149,66 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr | |
} | ||
} | ||
|
||
if (!activeTabs.some(tab => tab.key === state.currentContentType)) { | ||
const contentType = activeTabs.length > 0 && activeTabs[0].key ? activeTabs[0].key : TEXT_PLAIN_TYPE; | ||
state.setContentType(contentType); | ||
async function selectTabHandler(tabId: string) { | ||
// currentContentType may be selected automatically we don't want to change state in this case | ||
if (tabId !== contentType) { | ||
state.setContentType(tabId); | ||
} | ||
} | ||
|
||
function toggleLineWrappingHandler() { | ||
state.setLineWrapping(!lineWrapping); | ||
} | ||
|
||
return styled(style)( | ||
<container> | ||
<actions> | ||
<TabsState | ||
dataFormat={dataFormat} | ||
resultIndex={resultIndex} | ||
container={textValuePresentationService.tabs} | ||
currentTabId={state.currentContentType} | ||
model={model} | ||
lazy | ||
onChange={tab => state.handleTabOpen(tab.tabId)} | ||
> | ||
<TabList style={[BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]} /> | ||
</TabsState> | ||
</actions> | ||
<EditorLoader key={isReadonly ? '1' : '0'} value={textValue} readonly={isReadonly} extensions={extensions} onChange={handleChange} /> | ||
{isTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />} | ||
<div className={s(moduleStyle, { toolsContainer: true })}> | ||
<Container vertical gap dense overflow> | ||
<Container keepSize center overflow> | ||
<Container keepSize> | ||
Comment on lines
+165
to
+166
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose something went wrong inside container component if we have to do this twice Container thing in order to get a correct layout There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in this case it's because i don't want to add extra styles to |
||
<TabsState | ||
dataFormat={dataFormat} | ||
resultIndex={resultIndex} | ||
container={textValuePresentationService.tabs} | ||
currentTabId={contentType} | ||
model={model} | ||
lazy | ||
onChange={tab => selectTabHandler(tab.tabId)} | ||
> | ||
<TabList style={[BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]} /> | ||
</TabsState> | ||
</Container> | ||
</Container> | ||
<Group maximum box> | ||
<EditorLoader | ||
key={isReadonly ? '1' : '0'} | ||
value={textValue} | ||
lineWrapping={lineWrapping} | ||
readonly={isReadonly} | ||
extensions={extensions} | ||
onChange={valueChangeHandler} | ||
/> | ||
{isTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />} | ||
</Group> | ||
<Container keepSize center overflow> | ||
{canSave && ( | ||
<Button disabled={model.isLoading()} onClick={save}> | ||
{translate('ui_download')} | ||
</Button> | ||
<ActionIconButton title={translate('ui_download')} name="/icons/export.svg" disabled={model.isLoading()} img onClick={saveHandler} /> | ||
)} | ||
<ActionIconButton | ||
title={translate( | ||
lineWrapping ? 'data_viewer_presentation_value_text_line_wrapping_no_wrap' : 'data_viewer_presentation_value_text_line_wrapping_wrap', | ||
)} | ||
name={lineWrapping ? 'img-original-size' : 'img-fit-size'} | ||
onClick={toggleLineWrappingHandler} | ||
/> | ||
<Fill /> | ||
{shouldShowPasteButton && ( | ||
<Button disabled={model.isLoading()} onClick={pasteFullText}> | ||
{translate('data_viewer_presentation_value_content_full_text_button')} | ||
</Button> | ||
<Container keepSize> | ||
<Button disabled={model.isLoading()} onClick={pasteFullText}> | ||
{translate('data_viewer_presentation_value_content_full_text_button')} | ||
</Button> | ||
</Container> | ||
)} | ||
</div> | ||
</container>, | ||
</Container> | ||
</Container>, | ||
); | ||
}, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'application/octet-stream;type=hex':
- should we consider this case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we will detect tab automatically later, by now we just set base64 as default for all binary data