Merge branch 'devel' into CB-4335-implement-required-flag-to-all-forms-in-the-app

This commit is contained in:
s.teleshev
2023-12-06 10:26:21 +01:00
16 changed files with 58 additions and 198 deletions
@@ -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());
}
}
@@ -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),
};
};
-3
View File
@@ -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"]
}
-13
View File
@@ -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/**/*"]
}
@@ -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>