mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-13 16:18:51 +08:00
Merge branch 'devel' into CB-4335-implement-required-flag-to-all-forms-in-the-app
This commit is contained in:
+9
-3
@@ -22,9 +22,7 @@ import org.jkiss.dbeaver.model.DBPDataSource;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
import org.jkiss.dbeaver.model.data.DBDAttributeBinding;
|
||||
import org.jkiss.dbeaver.model.data.DBDRowIdentifier;
|
||||
import org.jkiss.dbeaver.model.struct.DBSAttributeBase;
|
||||
import org.jkiss.dbeaver.model.struct.DBSDataContainer;
|
||||
import org.jkiss.dbeaver.model.struct.DBSTypedObject;
|
||||
import org.jkiss.dbeaver.model.struct.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -126,4 +124,12 @@ public class WebSQLResultsInfo {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean canRefreshResults() {
|
||||
DBSEntity entity = getDefaultRowIdentifier().getEntity();
|
||||
// FIXME: do not refresh documents for now. Can be solved by extracting document ID attributes
|
||||
// FIXME: but it will require to provide dynamic document metadata.
|
||||
return entity == null || entity.getDataSource() == null ||
|
||||
(!(entity instanceof DBSDocumentContainer) && !entity.getDataSource().getInfo().isDynamicMetadata());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+26
-1
@@ -58,6 +58,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Web SQL processor.
|
||||
@@ -417,13 +418,22 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
DBCExecutionPurpose.UTIL,
|
||||
"Refresh row(s) after insert/update")
|
||||
) {
|
||||
boolean canRefreshResults = resultsInfo.canRefreshResults();
|
||||
for (Object[] row : newResultSetRows) {
|
||||
if (row.length == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!canRefreshResults) {
|
||||
makeWebCellRow(resultsInfo, row, dataFormat);
|
||||
continue;
|
||||
}
|
||||
List<DBDAttributeConstraint> constraints = new ArrayList<>();
|
||||
boolean hasKey = true;
|
||||
for (DBDAttributeBinding attr : resultsInfo.getAttributes()) {
|
||||
// get attributes only from row identifiers
|
||||
Set<DBDAttributeBinding> idAttributes = resultsInfo.getRowIdentifiers().stream()
|
||||
.flatMap(r -> r.getAttributes().stream())
|
||||
.collect(Collectors.toSet());
|
||||
for (DBDAttributeBinding attr : idAttributes) {
|
||||
if (attr.getRowIdentifier() == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -439,6 +449,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
}
|
||||
if (!hasKey) {
|
||||
// No key value for this row
|
||||
makeWebCellRow(resultsInfo, row, dataFormat);
|
||||
continue;
|
||||
}
|
||||
DBDDataFilter filter = new DBDDataFilter(constraints);
|
||||
@@ -457,6 +468,20 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private void makeWebCellRow(
|
||||
@NotNull WebSQLResultsInfo resultsInfo,
|
||||
@NotNull Object[] row,
|
||||
@Nullable WebDataFormat dataFormat
|
||||
) throws DBCException {
|
||||
for (int i = 0; i < row.length; i++) {
|
||||
row[i] = WebSQLUtils.makeWebCellValue(
|
||||
webSession,
|
||||
resultsInfo.getAttributeByPosition(i),
|
||||
row[i],
|
||||
dataFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public String generateResultsDataUpdateScript(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull WebSQLContextInfo contextInfo,
|
||||
|
||||
@@ -72,12 +72,12 @@ export const Switch: SwitchType = observer(function Switch({
|
||||
}
|
||||
|
||||
return (
|
||||
<Field data-testid="field" title={rest.title}>
|
||||
<div data-testid="switch-body" className={styles.switchBody}>
|
||||
<div data-testid="switch-control" className={s(styles, { switchControl: true, disabled: disabled, checked: checkboxState.checked })}>
|
||||
<div data-testid="switch-control-track" className={styles.switchControlTrack} />
|
||||
<div data-testid="switch-control-underlay" className={styles.switchControlUnderlay}>
|
||||
<div data-testid="switch-control-thumb" className={styles.switchControlThumb} />
|
||||
<Field title={rest.title}>
|
||||
<div className={styles.switchBody}>
|
||||
<div className={s(styles, { switchControl: true, disabled: disabled, checked: checkboxState.checked })}>
|
||||
<div className={styles.switchControlTrack} />
|
||||
<div className={styles.switchControlUnderlay}>
|
||||
<div className={styles.switchControlThumb} />
|
||||
<input
|
||||
{...rest}
|
||||
type="checkbox"
|
||||
@@ -86,13 +86,12 @@ export const Switch: SwitchType = observer(function Switch({
|
||||
aria-checked={checkboxState.checked}
|
||||
checked={checkboxState.checked}
|
||||
disabled={disabled}
|
||||
data-testid="switch-input"
|
||||
className={styles.switchInput}
|
||||
onChange={checkboxState.change}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FieldLabel htmlFor={id || value || name} data-testid="field-label" className={styles.fieldLabel}>
|
||||
<FieldLabel htmlFor={id || value || name} className={styles.fieldLabel}>
|
||||
{children}
|
||||
</FieldLabel>
|
||||
</div>
|
||||
|
||||
@@ -293,13 +293,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
|
||||
<MenuButton {...menu} disabled={readOnly || disabled || hideMenu} className={styles.menuButton}>
|
||||
<Icon name="arrow" viewBox="0 0 16 16" className={s(styles, { icon: true, focus })} />
|
||||
</MenuButton>
|
||||
<Menu
|
||||
{...menu}
|
||||
ref={menuRef}
|
||||
aria-label={propertyName}
|
||||
className={s(styles, { menu: true })}
|
||||
modal
|
||||
>
|
||||
<Menu {...menu} ref={menuRef} aria-label={propertyName} className={s(styles, { menu: true })} modal>
|
||||
{!filteredItems.length ? (
|
||||
<MenuItem id="placeholder" disabled {...menu} className={s(styles, { menuItem: true })}>
|
||||
{translate('combobox_no_results_placeholder')}
|
||||
@@ -322,11 +316,11 @@ export const Combobox: ComboboxType = observer(function Combobox({
|
||||
onClick={event => handleSelect(event.currentTarget.id)}
|
||||
>
|
||||
{iconSelector && (
|
||||
<div data-testid="item-icon" className={s(styles, { itemIcon: true })}>
|
||||
<div className={s(styles, { itemIcon: true })}>
|
||||
{icon && typeof icon === 'string' ? <IconOrImage icon={icon} className={s(styles, { iconOrImage: true })} /> : icon}
|
||||
</div>
|
||||
)}
|
||||
<div data-testid="item-value">{valueSelector(item)}</div>
|
||||
<div>{valueSelector(item)}</div>
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -192,11 +192,7 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
|
||||
<Icon name="copy" viewBox="0 0 32 32" className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
{icon && (
|
||||
<div data-testid="icon-container" className={s(styles, { customIconContainer: true })}>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
{icon && <div className={s(styles, { customIconContainer: true })}>{icon}</div>}
|
||||
</div>
|
||||
{(description || passwordType) && <FieldDescription invalid={error}>{description}</FieldDescription>}
|
||||
</Field>
|
||||
|
||||
@@ -17,14 +17,12 @@ interface Props {
|
||||
export const SnackbarBody: React.FC<React.PropsWithChildren<Props>> = function SnackbarBody({ title, className, children }) {
|
||||
const styles = useS(style);
|
||||
return (
|
||||
<div data-testid="notification-body" className={s(styles, { notificationBody: true }, className)}>
|
||||
<div data-testid="body-text-block" className={styles.bodyTextBlock}>
|
||||
<h2 data-testid="text-block-title" title={title} className={styles.textBlockTitle}>
|
||||
<div className={s(styles, { notificationBody: true }, className)}>
|
||||
<div className={styles.bodyTextBlock}>
|
||||
<h2 title={title} className={styles.textBlockTitle}>
|
||||
{title}
|
||||
</h2>
|
||||
<div data-testid="message" className={styles.message}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={styles.message}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,9 +15,5 @@ interface Props {
|
||||
|
||||
export const SnackbarContent: React.FC<React.PropsWithChildren<Props>> = function SnackbarContent({ children, className }) {
|
||||
const styles = useS(style);
|
||||
return (
|
||||
<div data-testid="notification-content" className={s(styles, { notificationContent: true }, className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={s(styles, { notificationContent: true }, className)}>{children}</div>;
|
||||
};
|
||||
|
||||
@@ -19,13 +19,9 @@ export const SnackbarFooter: React.FC<React.PropsWithChildren<Props>> = function
|
||||
const timeStringFromTimestamp = new Date(timestamp).toLocaleTimeString();
|
||||
|
||||
return (
|
||||
<div data-testid="notification-footer" className={s(styles, { notificationFooter: true }, className)}>
|
||||
<span data-testid="footer-time" className={styles.footerTime}>
|
||||
{timeStringFromTimestamp}
|
||||
</span>
|
||||
<div data-testid="actions" className={styles.actions}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={s(styles, { notificationFooter: true }, className)}>
|
||||
<span className={styles.footerTime}>{timeStringFromTimestamp}</span>
|
||||
<div className={styles.actions}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ interface SnackbarStatusProps {
|
||||
export const SnackbarStatus: React.FC<SnackbarStatusProps> = function SnackbarStatus({ status, className }) {
|
||||
const styles = useS(style);
|
||||
return status === ENotificationType.Loading ? (
|
||||
<div data-testid="loader-container" className={s(styles, { loaderContainer: true }, className)}>
|
||||
<div className={s(styles, { loaderContainer: true }, className)}>
|
||||
<Loader className={styles.loader} hideMessage />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -34,7 +34,7 @@ export const SnackbarWrapper: React.FC<React.PropsWithChildren<Props>> = functio
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-testid="notification" className={s(styles, { notification: true, mounted, closing }, className)}>
|
||||
<div className={s(styles, { notification: true, mounted, closing }, className)}>
|
||||
{children}
|
||||
{!persistent && onClose && (
|
||||
<IconButton name="cross" viewBox="0 0 16 16" className={s(styles, { iconButton: true, large: true })} onClick={onClose} />
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const testingAttributes = require('../dist/babel-plugins/TestingAttributes.js');
|
||||
|
||||
module.exports = api => {
|
||||
const devMode = !api.env('production');
|
||||
@@ -28,6 +26,6 @@ module.exports = api => {
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [[testingAttributes, {}], [require('@reshadow/babel'), {}], devMode && require.resolve('react-refresh/babel')].filter(Boolean),
|
||||
plugins: [[require('@reshadow/babel'), {}], devMode && require.resolve('react-refresh/babel')].filter(Boolean),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"core-cli-validate-dependencies": "./bin/validate-dependencies.js",
|
||||
"typescript-resolve-references": "./bin/typescript-resolve-references.js"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Based on https://github.com/akameco/babel-plugin-react-data-testid
|
||||
*/
|
||||
import type { NodePath, PluginObj, Visitor } from '@babel/core';
|
||||
import syntaxJsx from '@babel/plugin-syntax-jsx';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
type FunctionType = t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression;
|
||||
|
||||
function addTestIdAttribute(node: t.JSXOpeningElement, name: string): void {
|
||||
if (
|
||||
t.isJSXIdentifier(node.name) &&
|
||||
node.name.name !== 'Fragment' &&
|
||||
node.name.name !== '_Fragment' &&
|
||||
!hasDataAttribute(node, DEFAULT_DATA_TESTID)
|
||||
) {
|
||||
const dataAttribute = createDataAttribute(name, DEFAULT_DATA_TESTID);
|
||||
const indexOf = node.attributes.findIndex(attribute => t.isJSXSpreadAttribute(attribute));
|
||||
if (indexOf !== -1) {
|
||||
node.attributes.splice(indexOf, 0, dataAttribute);
|
||||
} else {
|
||||
node.attributes.push(dataAttribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nameForReactComponent(path: NodePath<FunctionType>): t.Identifier | null {
|
||||
const { parentPath } = path;
|
||||
if (!t.isArrowFunctionExpression(path.node) && t.isIdentifier(path.node.id)) {
|
||||
return path.node.id;
|
||||
}
|
||||
if (t.isVariableDeclarator(parentPath.node)) {
|
||||
// @ts-expect-error expected
|
||||
return parentPath.node.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const DEFAULT_DATA_TESTID = 'data-testid';
|
||||
|
||||
function createDataAttribute(name: string, attributeName: string) {
|
||||
return t.jsxAttribute(t.jsxIdentifier(attributeName), t.stringLiteral(name));
|
||||
}
|
||||
|
||||
function hasDataAttribute(node: t.JSXOpeningElement, attributeName: string): boolean {
|
||||
return node.attributes.some(attribute => t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: attributeName }));
|
||||
}
|
||||
|
||||
type VisitorState = { name: string };
|
||||
|
||||
const returnStatementVisitor: Visitor<VisitorState> = {
|
||||
JSXFragment(path) {
|
||||
path.skip();
|
||||
},
|
||||
JSXElement(path, { name }) {
|
||||
const openingElement = path.get('openingElement');
|
||||
|
||||
path.skip();
|
||||
|
||||
addTestIdAttribute(openingElement.node, name);
|
||||
},
|
||||
};
|
||||
|
||||
const functionVisitor: Visitor<VisitorState> = {
|
||||
ReturnStatement(path, state) {
|
||||
const arg = path.get('argument');
|
||||
if (!arg.isIdentifier()) {
|
||||
path.traverse(returnStatementVisitor, state);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getElementName(node: t.JSXOpeningElement['name']): string {
|
||||
if (t.isJSXNamespacedName(node)) {
|
||||
return [getElementName(node.namespace), getElementName(node.name)].join(':');
|
||||
}
|
||||
|
||||
if (t.isJSXIdentifier(node)) {
|
||||
return node.name;
|
||||
}
|
||||
|
||||
return [getElementName(node.object), getElementName(node.property)].join('.');
|
||||
}
|
||||
|
||||
export default function plugin(): PluginObj {
|
||||
return {
|
||||
name: 'cloudbeaver-data-testid',
|
||||
inherits: syntaxJsx,
|
||||
visitor: {
|
||||
'FunctionExpression|ArrowFunctionExpression|FunctionDeclaration': (p: NodePath<FunctionType>) => {
|
||||
const identifier = nameForReactComponent(p);
|
||||
if (!identifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.isArrowFunctionExpression()) {
|
||||
p.traverse(returnStatementVisitor, { name: identifier.name });
|
||||
} else {
|
||||
p.traverse(functionVisitor, { name: identifier.name });
|
||||
}
|
||||
},
|
||||
CallExpression(p) {
|
||||
p.traverse({
|
||||
JSXOpeningElement({ node }) {
|
||||
if (t.isJSXOpeningFragment(node)) {
|
||||
return;
|
||||
}
|
||||
addTestIdAttribute(node, getElementName(node.name));
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
} as PluginObj;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src",
|
||||
"tests",
|
||||
"bin",
|
||||
"configs"
|
||||
],
|
||||
"exclude": ["**/node_modules", "**/lib"]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"module": "CommonJS",
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/**/*", "dist/**/*", "tests/**/*"]
|
||||
}
|
||||
+1
-1
@@ -38,7 +38,7 @@ const NavTreeLimitMessage: NavigationNodeRendererComponent = observer(function N
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s(styles, { loadMoreBox: true })} data-testid="load-more-box">
|
||||
<div className={s(styles, { loadMoreBox: true })}>
|
||||
<Link title={translate('app_navigationTree_limited', undefined, { limit: limit })} onClick={loadMore}>
|
||||
{translate('ui_load_more')}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user