mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-01 19:21:31 +08:00
Compare commits
1 Commits
release/0.
...
assets/pr-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e78a4d3a2c |
BIN
.github/pr-597-screenshots/current-dev-sidebar-hover-card-no-native-title.jpg
vendored
Normal file
BIN
.github/pr-597-screenshots/current-dev-sidebar-hover-card-no-native-title.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
.github/pr-597-screenshots/current-dev-sidebar-hover-card.jpg
vendored
Normal file
BIN
.github/pr-597-screenshots/current-dev-sidebar-hover-card.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
47
.github/workflows/dev-build.yml
vendored
47
.github/workflows/dev-build.yml
vendored
@@ -588,45 +588,7 @@ jobs:
|
||||
export GOCACHE="${RUNNER_TEMP}/go-build-${{ matrix.os_name }}-${{ matrix.arch_name }}-${REVISION_HASH}"
|
||||
mkdir -p "$GOCACHE"
|
||||
echo "🧭 可选驱动使用隔离 GOCACHE:$GOCACHE"
|
||||
normalize_driver_token() {
|
||||
local value
|
||||
value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
||||
case "$value" in
|
||||
"") return 1 ;;
|
||||
doris|diros) echo "doris" ;;
|
||||
open_gauss|open-gauss) echo "opengauss" ;;
|
||||
gaussdb|gauss_db|gauss-db) echo "gaussdb" ;;
|
||||
elastic|elasticsearch) echo "elasticsearch" ;;
|
||||
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse)
|
||||
echo "$value"
|
||||
;;
|
||||
*)
|
||||
echo "❌ 不支持的 driver-agent:${1:-}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_driver_name() {
|
||||
case "$1" in
|
||||
doris) echo "diros" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -a RAW_DRIVERS=()
|
||||
declare -a DRIVERS=()
|
||||
IFS=',' read -r -a RAW_DRIVERS <<< "$CHANGED_DRIVER_AGENTS"
|
||||
for RAW_DRIVER in "${RAW_DRIVERS[@]}"; do
|
||||
DRIVER="$(normalize_driver_token "$RAW_DRIVER")" || continue
|
||||
DRIVERS+=("$DRIVER")
|
||||
done
|
||||
if [ ${#DRIVERS[@]} -eq 0 ]; then
|
||||
echo "🧭 没有需要构建的 driver-agent"
|
||||
exit 0
|
||||
fi
|
||||
NORMALIZED_CHANGED_DRIVER_AGENTS="$(IFS=,; echo "${DRIVERS[*]}")"
|
||||
echo "🧭 归一后的 driver-agent 构建列表:${NORMALIZED_CHANGED_DRIVER_AGENTS}"
|
||||
IFS=',' read -r -a DRIVERS <<< "$CHANGED_DRIVER_AGENTS"
|
||||
OUTDIR="drivers/${{ matrix.os_name }}"
|
||||
mkdir -p "$OUTDIR"
|
||||
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
|
||||
@@ -682,7 +644,10 @@ jobs:
|
||||
}
|
||||
|
||||
for DRIVER in "${DRIVERS[@]}"; do
|
||||
BUILD_DRIVER="$(build_driver_name "$DRIVER")"
|
||||
BUILD_DRIVER="$DRIVER"
|
||||
if [ "$DRIVER" = "doris" ]; then
|
||||
BUILD_DRIVER="diros"
|
||||
fi
|
||||
if [ "$DRIVER" = "duckdb" ] && [ "$GOOS" = "windows" ] && [ "$GOARCH" != "amd64" ]; then
|
||||
echo "⚠️ 跳过 DuckDB driver(当前平台 ${GOOS}/${GOARCH} 不受支持,仅支持 windows/amd64)"
|
||||
continue
|
||||
@@ -777,7 +742,7 @@ jobs:
|
||||
bash ./tools/verify-driver-agent-revisions.sh \
|
||||
--assets-dir drivers \
|
||||
--platform "$TARGET_PLATFORM" \
|
||||
--drivers "$NORMALIZED_CHANGED_DRIVER_AGENTS"
|
||||
--drivers "$CHANGED_DRIVER_AGENTS"
|
||||
|
||||
# macOS Packaging
|
||||
- name: Package macOS DMG
|
||||
|
||||
47
.github/workflows/release.yml
vendored
47
.github/workflows/release.yml
vendored
@@ -535,45 +535,7 @@ jobs:
|
||||
export GOCACHE="${RUNNER_TEMP}/go-build-${{ matrix.os_name }}-${{ matrix.arch_name }}-${REVISION_HASH}"
|
||||
mkdir -p "$GOCACHE"
|
||||
echo "🧭 可选驱动使用隔离 GOCACHE:$GOCACHE"
|
||||
normalize_driver_token() {
|
||||
local value
|
||||
value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
||||
case "$value" in
|
||||
"") return 1 ;;
|
||||
doris|diros) echo "doris" ;;
|
||||
open_gauss|open-gauss) echo "opengauss" ;;
|
||||
gaussdb|gauss_db|gauss-db) echo "gaussdb" ;;
|
||||
elastic|elasticsearch) echo "elasticsearch" ;;
|
||||
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse)
|
||||
echo "$value"
|
||||
;;
|
||||
*)
|
||||
echo "❌ 不支持的 driver-agent:${1:-}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_driver_name() {
|
||||
case "$1" in
|
||||
doris) echo "diros" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -a RAW_DRIVERS=()
|
||||
declare -a DRIVERS=()
|
||||
IFS=',' read -r -a RAW_DRIVERS <<< "$CHANGED_DRIVER_AGENTS"
|
||||
for RAW_DRIVER in "${RAW_DRIVERS[@]}"; do
|
||||
DRIVER="$(normalize_driver_token "$RAW_DRIVER")" || continue
|
||||
DRIVERS+=("$DRIVER")
|
||||
done
|
||||
if [ ${#DRIVERS[@]} -eq 0 ]; then
|
||||
echo "🧭 没有需要构建的 driver-agent"
|
||||
exit 0
|
||||
fi
|
||||
NORMALIZED_CHANGED_DRIVER_AGENTS="$(IFS=,; echo "${DRIVERS[*]}")"
|
||||
echo "🧭 归一后的 driver-agent 构建列表:${NORMALIZED_CHANGED_DRIVER_AGENTS}"
|
||||
IFS=',' read -r -a DRIVERS <<< "$CHANGED_DRIVER_AGENTS"
|
||||
OUTDIR="drivers/${{ matrix.os_name }}"
|
||||
mkdir -p "$OUTDIR"
|
||||
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
|
||||
@@ -629,7 +591,10 @@ jobs:
|
||||
}
|
||||
|
||||
for DRIVER in "${DRIVERS[@]}"; do
|
||||
BUILD_DRIVER="$(build_driver_name "$DRIVER")"
|
||||
BUILD_DRIVER="$DRIVER"
|
||||
if [ "$DRIVER" = "doris" ]; then
|
||||
BUILD_DRIVER="diros"
|
||||
fi
|
||||
if [ "$DRIVER" = "duckdb" ] && [ "$GOOS" = "windows" ] && [ "$GOARCH" != "amd64" ]; then
|
||||
echo "⚠️ 跳过 DuckDB driver(当前平台 ${GOOS}/${GOARCH} 不受支持,仅支持 windows/amd64)"
|
||||
continue
|
||||
@@ -724,7 +689,7 @@ jobs:
|
||||
bash ./tools/verify-driver-agent-revisions.sh \
|
||||
--assets-dir drivers \
|
||||
--platform "$TARGET_PLATFORM" \
|
||||
--drivers "$NORMALIZED_CHANGED_DRIVER_AGENTS"
|
||||
--drivers "$CHANGED_DRIVER_AGENTS"
|
||||
|
||||
# macOS Packaging
|
||||
- name: Package macOS DMG
|
||||
|
||||
@@ -2463,33 +2463,6 @@ describe('DataGrid layout', () => {
|
||||
expect(css).toContain('user-select: text !important;');
|
||||
});
|
||||
|
||||
it('wires data-view column header filters through the existing filter state', () => {
|
||||
const source = readDataGridSource();
|
||||
const dataViewerSource = readDataViewerSource();
|
||||
const filterHookSource = readFileSync(new URL('./useDataGridFilters.tsx', import.meta.url), 'utf8');
|
||||
const columnTitleSource = readFileSync(new URL('./DataGridColumnTitle.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(filterHookSource).toContain('export type GridColumnFilterDraft');
|
||||
expect(filterHookSource).toContain('const applyColumnFilter = React.useCallback');
|
||||
expect(filterHookSource).toContain('onApplyFilter(nextConditions)');
|
||||
expect(source).toContain("const columnHeaderFilterEnabled = exportScope === 'table' && !!onApplyFilter;");
|
||||
expect(source).toContain("filterOpOptions.filter((option) => option.value !== 'CUSTOM')");
|
||||
expect(source).toContain("eventTarget?.closest?.('[data-grid-column-filter-trigger=\"true\"]')");
|
||||
expect(source).toContain("eventTarget?.closest?.('[data-grid-column-filter-popover=\"true\"]')");
|
||||
expect(source).toContain("eventTarget?.closest?.('.ant-select-dropdown')");
|
||||
expect(source).toContain('onApply: (draft) => applyColumnHeaderFilter(normalizedName, draft)');
|
||||
expect(source).toContain('onClear: () => clearColumnFilter(normalizedName)');
|
||||
expect(dataViewerSource).toContain('skipNextAutoFetchRef.current = false;');
|
||||
expect(dataViewerSource).toContain('setFilterConditions(normalizeViewerFilterConditions(conditions));');
|
||||
expect(columnTitleSource).toContain('data-grid-column-filter-trigger="true"');
|
||||
expect(columnTitleSource).toContain('const submitColumnFilter = (event?: React.SyntheticEvent<HTMLElement>) => {');
|
||||
expect(columnTitleSource).toContain('onClick={submitColumnFilter}');
|
||||
expect(columnTitleSource).toContain('onPressEnter={submitColumnFilter}');
|
||||
expect(columnTitleSource).toContain('getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}');
|
||||
expect(columnTitleSource).toContain('data-grid-column-filter-active={columnFilter.active ?');
|
||||
expect(columnTitleSource).toContain('data-grid-column-filter-popover="true"');
|
||||
});
|
||||
|
||||
it('keeps DataGrid scroll synchronization throttled to animation frames', () => {
|
||||
const source = readDataGridSource();
|
||||
const secondaryActionsSource = readFileSync(new URL('./DataGridSecondaryActions.tsx', import.meta.url), 'utf8');
|
||||
|
||||
@@ -124,7 +124,7 @@ import {
|
||||
type V2CellContextMenuActionKey,
|
||||
type V2ColumnHeaderContextMenuActionKey,
|
||||
} from './V2TableContextMenu';
|
||||
import DataGridColumnTitle, { type DataGridColumnFilterDraft } from './DataGridColumnTitle';
|
||||
import DataGridColumnTitle from './DataGridColumnTitle';
|
||||
import DataGridColumnInfoPopoverContent from './DataGridColumnInfoPopoverContent';
|
||||
import DataGridColumnQuickFind from './DataGridColumnQuickFind';
|
||||
import DataGridPageFind from './DataGridPageFind';
|
||||
@@ -1041,6 +1041,29 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
openTableByName(String(target?.refTableName || '').trim());
|
||||
}, [openTableByName]);
|
||||
|
||||
const renderColumnTitle = useCallback((name: string): React.ReactNode => {
|
||||
const normalizedName = String(name || '');
|
||||
const meta = columnMetaMap[normalizedName] || columnMetaMapByLowerName[normalizedName.toLowerCase()];
|
||||
const foreignKeyTarget = foreignKeyMap[normalizedName] || foreignKeyMapByLowerName[normalizedName.toLowerCase()];
|
||||
|
||||
return (
|
||||
<DataGridColumnTitle
|
||||
columnName={normalizedName}
|
||||
columnMeta={meta}
|
||||
foreignKeyTarget={foreignKeyTarget}
|
||||
showColumnType={showColumnType}
|
||||
showColumnComment={showColumnComment}
|
||||
metaFontSize={densityParams.metaFontSize}
|
||||
columnMetaHintColor={columnMetaHintColor}
|
||||
columnMetaTooltipColor={columnMetaTooltipColor}
|
||||
darkMode={darkMode}
|
||||
highlighted={highlightedColumnName === normalizedName}
|
||||
translate={translateDataGrid}
|
||||
onOpenForeignKey={foreignKeyTarget ? () => openForeignKeyTarget(foreignKeyTarget) : undefined}
|
||||
/>
|
||||
);
|
||||
}, [columnMetaHintColor, columnMetaTooltipColor, columnMetaMap, columnMetaMapByLowerName, darkMode, densityParams.metaFontSize, foreignKeyMap, foreignKeyMapByLowerName, highlightedColumnName, openForeignKeyTarget, showColumnComment, showColumnType, translateDataGrid]);
|
||||
|
||||
const lockVirtualInlineTableScroll = useCallback((lock: boolean) => {
|
||||
if (lock) {
|
||||
if (virtualInlineScrollLockRef.current) {
|
||||
@@ -1238,8 +1261,6 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
addFilter,
|
||||
updateFilter,
|
||||
removeFilter,
|
||||
applyColumnFilter,
|
||||
clearColumnFilter,
|
||||
applyQuickWhereCondition,
|
||||
clearQuickWhereCondition,
|
||||
clearAllFiltersAndSorts,
|
||||
@@ -1269,102 +1290,6 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
resolveNextGridFilterOperatorForColumnChange,
|
||||
});
|
||||
|
||||
const columnHeaderFilterEnabled = exportScope === 'table' && !!onApplyFilter;
|
||||
const columnHeaderFilterOpOptions = useMemo(
|
||||
() => filterOpOptions.filter((option) => option.value !== 'CUSTOM'),
|
||||
[filterOpOptions],
|
||||
);
|
||||
const getColumnHeaderFilterState = useCallback((columnName: string) => {
|
||||
const normalizedName = String(columnName || '').trim();
|
||||
const columnFilterConditions = filterConditions.filter((cond) => (
|
||||
String(cond?.column || '') === normalizedName && String(cond?.op || '') !== 'CUSTOM'
|
||||
));
|
||||
const firstCondition = columnFilterConditions[0];
|
||||
const defaultOperator = resolveDefaultGridFilterOperator(getColumnFilterType(normalizedName));
|
||||
return {
|
||||
active: columnFilterConditions.some((cond) => cond.enabled !== false),
|
||||
defaultOperator,
|
||||
initialOperator: String(firstCondition?.op || defaultOperator),
|
||||
initialValue: String(firstCondition?.value ?? ''),
|
||||
initialValue2: String(firstCondition?.value2 ?? ''),
|
||||
};
|
||||
}, [filterConditions, getColumnFilterType]);
|
||||
|
||||
const applyColumnHeaderFilter = useCallback((columnName: string, draft: DataGridColumnFilterDraft) => {
|
||||
return applyColumnFilter({
|
||||
column: columnName,
|
||||
op: draft.op,
|
||||
value: draft.value,
|
||||
value2: draft.value2,
|
||||
});
|
||||
}, [applyColumnFilter]);
|
||||
|
||||
const renderColumnTitle = useCallback((name: string): React.ReactNode => {
|
||||
const normalizedName = String(name || '');
|
||||
const meta = columnMetaMap[normalizedName] || columnMetaMapByLowerName[normalizedName.toLowerCase()];
|
||||
const foreignKeyTarget = foreignKeyMap[normalizedName] || foreignKeyMapByLowerName[normalizedName.toLowerCase()];
|
||||
const columnFilterState = columnHeaderFilterEnabled ? getColumnHeaderFilterState(normalizedName) : null;
|
||||
|
||||
return (
|
||||
<DataGridColumnTitle
|
||||
columnName={normalizedName}
|
||||
columnMeta={meta}
|
||||
foreignKeyTarget={foreignKeyTarget}
|
||||
showColumnType={showColumnType}
|
||||
showColumnComment={showColumnComment}
|
||||
metaFontSize={densityParams.metaFontSize}
|
||||
columnMetaHintColor={columnMetaHintColor}
|
||||
columnMetaTooltipColor={columnMetaTooltipColor}
|
||||
darkMode={darkMode}
|
||||
highlighted={highlightedColumnName === normalizedName}
|
||||
translate={translateDataGrid}
|
||||
onOpenForeignKey={foreignKeyTarget ? () => openForeignKeyTarget(foreignKeyTarget) : undefined}
|
||||
columnFilter={columnFilterState ? {
|
||||
active: columnFilterState.active,
|
||||
operatorOptions: columnHeaderFilterOpOptions,
|
||||
defaultOperator: columnFilterState.defaultOperator,
|
||||
initialOperator: columnFilterState.initialOperator,
|
||||
initialValue: columnFilterState.initialValue,
|
||||
initialValue2: columnFilterState.initialValue2,
|
||||
filterLabel: translateDataGrid('data_grid.toolbar.filter'),
|
||||
applyLabel: translateDataGrid('data_grid.filter.apply'),
|
||||
clearLabel: translateDataGrid('data_grid.filter.clear'),
|
||||
valuePlaceholder: translateDataGrid('data_grid.filter.start_value_placeholder'),
|
||||
secondValuePlaceholder: translateDataGrid('data_grid.filter.end_value_placeholder'),
|
||||
listValuePlaceholder: translateDataGrid('data_grid.filter.list_values_placeholder'),
|
||||
noValuePlaceholder: translateDataGrid('data_grid.filter.no_value_placeholder'),
|
||||
isNoValueOp,
|
||||
isBetweenOp,
|
||||
isListOp,
|
||||
onApply: (draft) => applyColumnHeaderFilter(normalizedName, draft),
|
||||
onClear: () => clearColumnFilter(normalizedName),
|
||||
} : null}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
applyColumnHeaderFilter,
|
||||
clearColumnFilter,
|
||||
columnHeaderFilterEnabled,
|
||||
columnHeaderFilterOpOptions,
|
||||
columnMetaHintColor,
|
||||
columnMetaTooltipColor,
|
||||
columnMetaMap,
|
||||
columnMetaMapByLowerName,
|
||||
darkMode,
|
||||
densityParams.metaFontSize,
|
||||
foreignKeyMap,
|
||||
foreignKeyMapByLowerName,
|
||||
getColumnHeaderFilterState,
|
||||
highlightedColumnName,
|
||||
isBetweenOp,
|
||||
isListOp,
|
||||
isNoValueOp,
|
||||
openForeignKeyTarget,
|
||||
showColumnComment,
|
||||
showColumnType,
|
||||
translateDataGrid,
|
||||
]);
|
||||
|
||||
const selectedRowKeysRef = useRef(selectedRowKeys);
|
||||
const displayDataRef = useRef<any[]>([]);
|
||||
|
||||
@@ -2478,9 +2403,6 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
if (!onSort) return;
|
||||
const eventTarget = event.target as HTMLElement | null;
|
||||
if (eventTarget?.closest?.('[data-grid-fk-jump="true"]')) return;
|
||||
if (eventTarget?.closest?.('[data-grid-column-filter-trigger="true"]')) return;
|
||||
if (eventTarget?.closest?.('[data-grid-column-filter-popover="true"]')) return;
|
||||
if (eventTarget?.closest?.('.ant-select-dropdown')) return;
|
||||
const headerCell = event.currentTarget as HTMLElement;
|
||||
const upArrow = headerCell.querySelector('.ant-table-column-sorter-up') as HTMLElement | null;
|
||||
const downArrow = headerCell.querySelector('.ant-table-column-sorter-down') as HTMLElement | null;
|
||||
|
||||
@@ -1,40 +1,10 @@
|
||||
import React from 'react';
|
||||
import { act, create } from 'react-test-renderer';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import DataGridColumnTitle from './DataGridColumnTitle';
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
Button: ({ children, type: buttonType, htmlType, ...props }: { children?: React.ReactNode; type?: string; htmlType?: 'button' | 'submit' | 'reset' }) => (
|
||||
<button type={htmlType || 'button'} data-button-type={buttonType} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Form: ({ children, component: Component = 'form', onFinish: _onFinish, ...props }: { children?: React.ReactNode; component?: React.ElementType; onFinish?: () => void }) => (
|
||||
<Component {...props}>{children}</Component>
|
||||
),
|
||||
Input: Object.assign(
|
||||
({ onPressEnter: _onPressEnter, ...props }: { onPressEnter?: () => void }) => <input {...props} />,
|
||||
{
|
||||
TextArea: ({ autoSize: _autoSize, ...props }: { autoSize?: unknown }) => <textarea {...props} />,
|
||||
},
|
||||
),
|
||||
Popover: ({ children, content, open }: { children: React.ReactNode; content?: React.ReactNode; open?: boolean }) => (
|
||||
<span data-popover-open={open ? 'true' : 'false'}>
|
||||
{content}
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
Select: ({ options = [], value, onChange }: { options?: Array<{ value: string; label: string }>; value?: string; onChange?: (value: string) => void }) => (
|
||||
<select value={value} onChange={(event) => onChange?.(event.target.value)}>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
Tooltip: ({ children, title, rootClassName }: { children: React.ReactNode; title?: React.ReactNode; rootClassName?: string }) => (
|
||||
<>
|
||||
<div data-testid="tooltip-title">{title}</div>
|
||||
@@ -44,11 +14,6 @@ vi.mock('antd', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@ant-design/icons', () => ({
|
||||
FilterOutlined: () => <span data-icon="filter" />,
|
||||
LinkOutlined: () => <span data-icon="link" />,
|
||||
}));
|
||||
|
||||
describe('DataGridColumnTitle', () => {
|
||||
it('marks v2 table headers as single-line when column type and comment rows are hidden', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
@@ -146,170 +111,6 @@ describe('DataGridColumnTitle', () => {
|
||||
expect(markup).toContain('data-ref-table-name="customers"');
|
||||
});
|
||||
|
||||
it('renders a compact column filter trigger with active state', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<DataGridColumnTitle
|
||||
columnName="status"
|
||||
showColumnType={false}
|
||||
showColumnComment={false}
|
||||
metaFontSize={11}
|
||||
columnMetaHintColor="#999"
|
||||
columnMetaTooltipColor="#fff"
|
||||
darkMode={false}
|
||||
columnFilter={{
|
||||
active: true,
|
||||
operatorOptions: [
|
||||
{ value: '=', label: '=' },
|
||||
{ value: 'CONTAINS', label: 'Contains' },
|
||||
],
|
||||
defaultOperator: 'CONTAINS',
|
||||
initialOperator: '=',
|
||||
initialValue: 'active',
|
||||
filterLabel: 'Filter',
|
||||
applyLabel: 'Apply',
|
||||
clearLabel: 'Clear',
|
||||
valuePlaceholder: 'Value',
|
||||
secondValuePlaceholder: 'End value',
|
||||
listValuePlaceholder: 'List values',
|
||||
noValuePlaceholder: 'No value needed',
|
||||
isNoValueOp: () => false,
|
||||
isBetweenOp: () => false,
|
||||
isListOp: () => false,
|
||||
onApply: () => true,
|
||||
onClear: () => true,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('class="gn-v2-column-title-shell"');
|
||||
expect(markup).toContain('data-grid-column-filter-trigger="true"');
|
||||
expect(markup).toContain('data-grid-column-filter-active="true"');
|
||||
expect(markup).toContain('data-grid-column-filter-popover="true"');
|
||||
expect(markup).toContain('Filter status');
|
||||
expect(markup).toContain('value="active"');
|
||||
});
|
||||
|
||||
it('applies the column filter from the popover action button', () => {
|
||||
const onApply = vi.fn(() => true);
|
||||
const renderer = create(
|
||||
<DataGridColumnTitle
|
||||
columnName="code"
|
||||
showColumnType={false}
|
||||
showColumnComment={false}
|
||||
metaFontSize={11}
|
||||
columnMetaHintColor="#999"
|
||||
columnMetaTooltipColor="#fff"
|
||||
darkMode={false}
|
||||
columnFilter={{
|
||||
active: false,
|
||||
operatorOptions: [
|
||||
{ value: 'CONTAINS', label: 'Contains' },
|
||||
],
|
||||
defaultOperator: 'CONTAINS',
|
||||
initialOperator: 'CONTAINS',
|
||||
initialValue: '3551',
|
||||
filterLabel: 'Filter',
|
||||
applyLabel: 'Apply',
|
||||
clearLabel: 'Clear',
|
||||
valuePlaceholder: 'Value',
|
||||
secondValuePlaceholder: 'End value',
|
||||
listValuePlaceholder: 'List values',
|
||||
noValuePlaceholder: 'No value needed',
|
||||
isNoValueOp: () => false,
|
||||
isBetweenOp: () => false,
|
||||
isListOp: () => false,
|
||||
onApply,
|
||||
onClear: () => true,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const applyButton = renderer.root
|
||||
.findAllByType('button')
|
||||
.find((button) => button.children.includes('Apply'));
|
||||
|
||||
expect(applyButton).toBeTruthy();
|
||||
act(() => {
|
||||
applyButton!.props.onClick({
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
expect(onApply).toHaveBeenCalledWith({
|
||||
op: 'CONTAINS',
|
||||
value: '3551',
|
||||
value2: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps column filter operator switching and clearing interactive', () => {
|
||||
const onApply = vi.fn(() => true);
|
||||
const onClear = vi.fn(() => true);
|
||||
const renderer = create(
|
||||
<DataGridColumnTitle
|
||||
columnName="title"
|
||||
showColumnType={false}
|
||||
showColumnComment={false}
|
||||
metaFontSize={11}
|
||||
columnMetaHintColor="#999"
|
||||
columnMetaTooltipColor="#fff"
|
||||
darkMode={false}
|
||||
columnFilter={{
|
||||
active: true,
|
||||
operatorOptions: [
|
||||
{ value: '=', label: '=' },
|
||||
{ value: 'CONTAINS', label: 'Contains' },
|
||||
],
|
||||
defaultOperator: 'CONTAINS',
|
||||
initialOperator: 'CONTAINS',
|
||||
initialValue: '3551',
|
||||
filterLabel: 'Filter',
|
||||
applyLabel: 'Apply',
|
||||
clearLabel: 'Clear',
|
||||
valuePlaceholder: 'Value',
|
||||
secondValuePlaceholder: 'End value',
|
||||
listValuePlaceholder: 'List values',
|
||||
noValuePlaceholder: 'No value needed',
|
||||
isNoValueOp: () => false,
|
||||
isBetweenOp: () => false,
|
||||
isListOp: () => false,
|
||||
onApply,
|
||||
onClear,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const operatorSelect = renderer.root.findByType('select');
|
||||
act(() => {
|
||||
operatorSelect.props.onChange({ target: { value: '=' } });
|
||||
});
|
||||
|
||||
const buttons = renderer.root.findAllByType('button');
|
||||
const clearButton = buttons.find((button) => button.children.includes('Clear'));
|
||||
const applyButton = buttons.find((button) => button.children.includes('Apply'));
|
||||
|
||||
expect(clearButton).toBeTruthy();
|
||||
expect(applyButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
applyButton!.props.onClick({
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
});
|
||||
expect(onApply).toHaveBeenLastCalledWith({
|
||||
op: '=',
|
||||
value: '3551',
|
||||
value2: '',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
clearButton!.props.onClick();
|
||||
});
|
||||
expect(onClear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('uses translated tooltip wrappers while preserving raw metadata values', () => {
|
||||
const translate = vi.fn((key: string, params?: Record<string, unknown>) => {
|
||||
if (key === 'data_grid.column.type_tooltip') return `TYPE ${String(params?.type)}`;
|
||||
|
||||
@@ -1,37 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Button, Input, Popover, Select, Tooltip } from 'antd';
|
||||
import { FilterOutlined, LinkOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { t as defaultTranslate, type I18nParams } from '../i18n';
|
||||
|
||||
export type DataGridColumnTitleTranslate = (key: string, params?: I18nParams) => string;
|
||||
|
||||
export type DataGridColumnFilterDraft = {
|
||||
op: string;
|
||||
value: string;
|
||||
value2?: string;
|
||||
};
|
||||
|
||||
export interface DataGridColumnFilterConfig {
|
||||
active: boolean;
|
||||
operatorOptions: Array<{ value: string; label: string }>;
|
||||
defaultOperator: string;
|
||||
initialOperator?: string;
|
||||
initialValue?: string;
|
||||
initialValue2?: string;
|
||||
filterLabel: string;
|
||||
applyLabel: string;
|
||||
clearLabel: string;
|
||||
valuePlaceholder: string;
|
||||
secondValuePlaceholder: string;
|
||||
listValuePlaceholder: string;
|
||||
noValuePlaceholder: string;
|
||||
isNoValueOp: (op: string) => boolean;
|
||||
isBetweenOp: (op: string) => boolean;
|
||||
isListOp: (op: string) => boolean;
|
||||
onApply: (draft: DataGridColumnFilterDraft) => boolean | void;
|
||||
onClear: () => boolean | void;
|
||||
}
|
||||
|
||||
export interface DataGridColumnTitleProps {
|
||||
columnName: string;
|
||||
columnMeta?: {
|
||||
@@ -51,13 +24,8 @@ export interface DataGridColumnTitleProps {
|
||||
highlighted?: boolean;
|
||||
translate?: DataGridColumnTitleTranslate;
|
||||
onOpenForeignKey?: () => void;
|
||||
columnFilter?: DataGridColumnFilterConfig | null;
|
||||
}
|
||||
|
||||
const stopColumnHeaderInteraction = (event: React.SyntheticEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
|
||||
columnName,
|
||||
columnMeta,
|
||||
@@ -71,7 +39,6 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
|
||||
highlighted = false,
|
||||
translate = defaultTranslate,
|
||||
onOpenForeignKey,
|
||||
columnFilter,
|
||||
}) => {
|
||||
const normalizedName = String(columnName || '');
|
||||
const columnType = String(columnMeta?.type || '').trim();
|
||||
@@ -81,24 +48,6 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
|
||||
const shouldShowColumnType = showColumnType && columnType.length > 0;
|
||||
const shouldShowColumnComment = showColumnComment && columnComment.length > 0;
|
||||
const isSingleLineColumnTitle = !shouldShowColumnType && !shouldShowColumnComment;
|
||||
const [filterPopoverOpen, setFilterPopoverOpen] = React.useState(false);
|
||||
const initialFilterOperator = columnFilter?.initialOperator || columnFilter?.defaultOperator || '=';
|
||||
const [draftFilterOperator, setDraftFilterOperator] = React.useState(initialFilterOperator);
|
||||
const [draftFilterValue, setDraftFilterValue] = React.useState(columnFilter?.initialValue || '');
|
||||
const [draftFilterValue2, setDraftFilterValue2] = React.useState(columnFilter?.initialValue2 || '');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!filterPopoverOpen || !columnFilter) return;
|
||||
setDraftFilterOperator(columnFilter.initialOperator || columnFilter.defaultOperator || '=');
|
||||
setDraftFilterValue(columnFilter.initialValue || '');
|
||||
setDraftFilterValue2(columnFilter.initialValue2 || '');
|
||||
}, [
|
||||
columnFilter?.defaultOperator,
|
||||
columnFilter?.initialOperator,
|
||||
columnFilter?.initialValue,
|
||||
columnFilter?.initialValue2,
|
||||
filterPopoverOpen,
|
||||
]);
|
||||
|
||||
const hoverLines: string[] = [];
|
||||
if (columnType) hoverLines.push(translate('data_grid.column.type_tooltip', { type: columnType }));
|
||||
@@ -202,227 +151,35 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
const titleWithOptionalTooltip = (() => {
|
||||
if (hoverLines.length === 0) {
|
||||
return titleNode;
|
||||
}
|
||||
|
||||
const tooltipTextColor = darkMode ? columnMetaTooltipColor : 'var(--gn-fg-1, #fff)';
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={(
|
||||
<pre
|
||||
className="gn-data-grid-column-meta-tooltip-content"
|
||||
style={{
|
||||
maxHeight: 260,
|
||||
overflow: 'auto',
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: tooltipTextColor,
|
||||
}}
|
||||
>
|
||||
{hoverLines.join('\n')}
|
||||
</pre>
|
||||
)}
|
||||
rootClassName="gn-data-grid-column-meta-tooltip"
|
||||
styles={{ root: { maxWidth: 640 } }}
|
||||
{...(!darkMode ? { color: 'rgba(0, 0, 0, 0.82)' } : {})}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', maxWidth: '100%' }}>{titleNode}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
})();
|
||||
|
||||
if (!columnFilter) {
|
||||
return titleWithOptionalTooltip;
|
||||
if (hoverLines.length === 0) {
|
||||
return titleNode;
|
||||
}
|
||||
|
||||
const noValueOperator = columnFilter.isNoValueOp(draftFilterOperator);
|
||||
const betweenOperator = columnFilter.isBetweenOp(draftFilterOperator);
|
||||
const listOperator = columnFilter.isListOp(draftFilterOperator);
|
||||
const activeColor = darkMode ? '#74d99f' : '#16a34a';
|
||||
const mutedColor = columnFilter.active
|
||||
? activeColor
|
||||
: (darkMode ? 'rgba(255,255,255,0.52)' : 'rgba(15, 23, 42, 0.46)');
|
||||
const filterButtonTitle = `${columnFilter.filterLabel} ${normalizedName}`;
|
||||
const submitColumnFilter = (event?: React.SyntheticEvent<HTMLElement>) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
const applied = columnFilter.onApply({
|
||||
op: draftFilterOperator,
|
||||
value: draftFilterValue,
|
||||
value2: draftFilterValue2,
|
||||
});
|
||||
if (applied !== false) setFilterPopoverOpen(false);
|
||||
};
|
||||
const filterPopoverContent = (
|
||||
<div
|
||||
data-grid-column-filter-popover="true"
|
||||
onClick={stopColumnHeaderInteraction}
|
||||
style={{
|
||||
width: 260,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: 600,
|
||||
color: darkMode ? 'rgba(255,255,255,0.88)' : 'rgba(15,23,42,0.88)',
|
||||
}}
|
||||
title={normalizedName}
|
||||
>
|
||||
{filterButtonTitle}
|
||||
</div>
|
||||
<Select
|
||||
size="small"
|
||||
value={draftFilterOperator}
|
||||
options={columnFilter.operatorOptions}
|
||||
popupMatchSelectWidth={false}
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}
|
||||
onChange={(value) => {
|
||||
const nextOperator = String(value || columnFilter.defaultOperator || '=');
|
||||
setDraftFilterOperator(nextOperator);
|
||||
if (columnFilter.isNoValueOp(nextOperator)) {
|
||||
setDraftFilterValue('');
|
||||
setDraftFilterValue2('');
|
||||
} else if (!columnFilter.isBetweenOp(nextOperator)) {
|
||||
setDraftFilterValue2('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{noValueOperator ? (
|
||||
<Input
|
||||
size="small"
|
||||
disabled
|
||||
value={columnFilter.noValuePlaceholder}
|
||||
/>
|
||||
) : listOperator ? (
|
||||
<Input.TextArea
|
||||
value={draftFilterValue}
|
||||
placeholder={columnFilter.listValuePlaceholder}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
onChange={(event) => setDraftFilterValue(event.target.value)}
|
||||
/>
|
||||
) : betweenOperator ? (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Input
|
||||
size="small"
|
||||
value={draftFilterValue}
|
||||
placeholder={columnFilter.valuePlaceholder}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
onPressEnter={submitColumnFilter}
|
||||
onChange={(event) => setDraftFilterValue(event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
size="small"
|
||||
value={draftFilterValue2}
|
||||
placeholder={columnFilter.secondValuePlaceholder}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
onPressEnter={submitColumnFilter}
|
||||
onChange={(event) => setDraftFilterValue2(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
size="small"
|
||||
value={draftFilterValue}
|
||||
placeholder={columnFilter.valuePlaceholder}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
onPressEnter={submitColumnFilter}
|
||||
onChange={(event) => setDraftFilterValue(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const cleared = columnFilter.onClear();
|
||||
if (cleared !== false) setFilterPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
{columnFilter.clearLabel}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={submitColumnFilter}
|
||||
>
|
||||
{columnFilter.applyLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const tooltipTextColor = darkMode ? columnMetaTooltipColor : 'var(--gn-fg-1, #fff)';
|
||||
|
||||
return (
|
||||
<span
|
||||
className="gn-v2-column-title-shell"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', minWidth: 0, maxWidth: 'calc(100% - 24px)' }}>
|
||||
{titleWithOptionalTooltip}
|
||||
</span>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="bottomLeft"
|
||||
open={filterPopoverOpen}
|
||||
onOpenChange={setFilterPopoverOpen}
|
||||
content={filterPopoverContent}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-grid-column-filter-trigger="true"
|
||||
data-grid-column-filter-active={columnFilter.active ? 'true' : undefined}
|
||||
aria-label={filterButtonTitle}
|
||||
title={filterButtonTitle}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onMouseDown={stopColumnHeaderInteraction}
|
||||
onPointerDown={stopColumnHeaderInteraction}
|
||||
<Tooltip
|
||||
title={(
|
||||
<pre
|
||||
className="gn-data-grid-column-meta-tooltip-content"
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
flex: '0 0 22px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 0,
|
||||
border: columnFilter.active ? `1px solid ${activeColor}` : '1px solid transparent',
|
||||
borderRadius: 6,
|
||||
background: columnFilter.active
|
||||
? (darkMode ? 'rgba(34, 197, 94, 0.14)' : 'rgba(34, 197, 94, 0.12)')
|
||||
: 'transparent',
|
||||
color: mutedColor,
|
||||
cursor: 'pointer',
|
||||
maxHeight: 260,
|
||||
overflow: 'auto',
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: tooltipTextColor,
|
||||
}}
|
||||
>
|
||||
<FilterOutlined style={{ fontSize: 12 }} />
|
||||
</button>
|
||||
</Popover>
|
||||
</span>
|
||||
{hoverLines.join('\n')}
|
||||
</pre>
|
||||
)}
|
||||
rootClassName="gn-data-grid-column-meta-tooltip"
|
||||
styles={{ root: { maxWidth: 640 } }}
|
||||
{...(!darkMode ? { color: 'rgba(0, 0, 0, 0.82)' } : {})}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', maxWidth: '100%' }}>{titleNode}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -438,48 +438,6 @@ describe('DataViewer safe editing locator', () => {
|
||||
renderer.unmount();
|
||||
});
|
||||
|
||||
it('requeries table preview when a column header filter is applied', async () => {
|
||||
storeState.connections[0].config.type = 'mysql';
|
||||
storeState.connections[0].config.database = 'missav_bot';
|
||||
backendApp.DBGetColumns.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{ name: 'id', key: 'PRI' }, { name: 'code', key: '' }],
|
||||
});
|
||||
backendApp.DBQuery.mockResolvedValue({
|
||||
success: true,
|
||||
fields: ['id', 'code'],
|
||||
data: [{ id: 2, code: 'EROFV-3551' }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<DataViewer tab={createTab({ dbName: 'missav_bot', tableName: 'videos', title: 'videos' })} />);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
backendApp.DBQuery.mockClear();
|
||||
await act(async () => {
|
||||
dataGridState.latestProps?.onApplyFilter([{
|
||||
id: 1,
|
||||
enabled: true,
|
||||
logic: 'AND',
|
||||
column: 'code',
|
||||
op: 'CONTAINS',
|
||||
value: '3551',
|
||||
value2: '',
|
||||
}]);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const filteredSelectSql = backendApp.DBQuery.mock.calls
|
||||
.map((call: any[]) => String(call[2] || ''))
|
||||
.find((sql: string) => /select\s+\*\s+from\s+`videos`/i.test(sql) && /where/i.test(sql));
|
||||
expect(filteredSelectSql).toContain("`code` LIKE '%3551%'");
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('keeps DuckDB table preview writable when primary key metadata arrives for a qualified table name', async () => {
|
||||
storeState.connections[0].config.type = 'duckdb';
|
||||
storeState.connections[0].config.database = 'main';
|
||||
|
||||
@@ -1151,17 +1151,7 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
}, []);
|
||||
const handlePageChange = useCallback((page: number, size: number) => fetchData(page, size), [fetchData]);
|
||||
const handleToggleFilter = useCallback(() => setShowFilter(prev => !prev), []);
|
||||
const handleApplyFilter = useCallback((conditions: FilterCondition[]) => {
|
||||
skipNextAutoFetchRef.current = false;
|
||||
initialLoadRef.current = true;
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: 1,
|
||||
totalCountLoading: false,
|
||||
totalCountCancelled: false,
|
||||
}));
|
||||
setFilterConditions(normalizeViewerFilterConditions(conditions));
|
||||
}, []);
|
||||
const handleApplyFilter = useCallback((conditions: FilterCondition[]) => setFilterConditions(conditions), []);
|
||||
const handleApplyQuickWhereCondition = useCallback((condition: string) => {
|
||||
const normalized = normalizeQuickWhereCondition(condition);
|
||||
const validation = validateQuickWhereCondition(normalized);
|
||||
@@ -1169,14 +1159,6 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
message.error(validation.message);
|
||||
return;
|
||||
}
|
||||
skipNextAutoFetchRef.current = false;
|
||||
initialLoadRef.current = true;
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: 1,
|
||||
totalCountLoading: false,
|
||||
totalCountCancelled: false,
|
||||
}));
|
||||
setQuickWhereCondition(normalized);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useDataGridFilters, type UseDataGridFiltersResult } from './useDataGridFilters';
|
||||
|
||||
const createFilterHookProps = (
|
||||
overrides: Partial<Parameters<typeof useDataGridFilters>[0]> = {},
|
||||
): Parameters<typeof useDataGridFilters>[0] => ({
|
||||
appliedFilterConditions: [],
|
||||
quickWhereCondition: '',
|
||||
showFilter: false,
|
||||
displayColumnNames: ['id', 'code', 'title'],
|
||||
allTableColumnNames: ['id', 'code', 'title'],
|
||||
columnMetaMap: {
|
||||
id: { type: 'bigint' },
|
||||
code: { type: 'varchar(50)' },
|
||||
title: { type: 'varchar(500)' },
|
||||
},
|
||||
dbType: 'mysql',
|
||||
darkMode: false,
|
||||
getColumnFilterType: (columnName) => {
|
||||
if (columnName === 'id') return 'bigint';
|
||||
return 'varchar(255)';
|
||||
},
|
||||
resolveDefaultGridFilterOperator: (columnType) => (
|
||||
String(columnType || '').toLowerCase().includes('char') ? 'CONTAINS' : '='
|
||||
),
|
||||
resolveNextGridFilterOperatorForColumnChange: () => 'CONTAINS',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('useDataGridFilters', () => {
|
||||
it('syncs column-header filters into the shared toolbar filter state without requiring the filter panel to be open', () => {
|
||||
const onApplyFilter = vi.fn();
|
||||
const hookProps = createFilterHookProps({
|
||||
showFilter: false,
|
||||
onApplyFilter,
|
||||
});
|
||||
let latest: UseDataGridFiltersResult | undefined;
|
||||
let renderer: ReactTestRenderer | undefined;
|
||||
|
||||
const Harness = () => {
|
||||
latest = useDataGridFilters(hookProps);
|
||||
return null;
|
||||
};
|
||||
|
||||
act(() => {
|
||||
renderer = create(<Harness />);
|
||||
});
|
||||
|
||||
expect(latest?.filterConditions).toEqual([]);
|
||||
|
||||
act(() => {
|
||||
expect(latest?.applyColumnFilter({
|
||||
column: 'code',
|
||||
op: 'CONTAINS',
|
||||
value: '3551',
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
expect(latest?.filterConditions).toMatchObject([{
|
||||
enabled: true,
|
||||
logic: 'AND',
|
||||
column: 'code',
|
||||
op: 'CONTAINS',
|
||||
value: '3551',
|
||||
value2: '',
|
||||
}]);
|
||||
expect(onApplyFilter).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
logic: 'AND',
|
||||
column: 'code',
|
||||
op: 'CONTAINS',
|
||||
value: '3551',
|
||||
value2: '',
|
||||
}),
|
||||
]);
|
||||
|
||||
renderer?.unmount();
|
||||
});
|
||||
});
|
||||
@@ -15,13 +15,6 @@ export type GridFilterConditionState = FilterCondition & {
|
||||
value2?: string;
|
||||
};
|
||||
|
||||
export type GridColumnFilterDraft = {
|
||||
column: string;
|
||||
op: string;
|
||||
value?: string;
|
||||
value2?: string;
|
||||
};
|
||||
|
||||
type GridSortInfo = {
|
||||
columnKey: string;
|
||||
order: string;
|
||||
@@ -72,8 +65,6 @@ export interface UseDataGridFiltersResult {
|
||||
addFilter: () => void;
|
||||
updateFilter: (id: number, field: keyof GridFilterConditionState, val: string | boolean) => void;
|
||||
removeFilter: (id: number) => void;
|
||||
applyColumnFilter: (draft: GridColumnFilterDraft) => boolean;
|
||||
clearColumnFilter: (columnName: string) => boolean;
|
||||
applyQuickWhereCondition: (condition?: string) => boolean;
|
||||
clearQuickWhereCondition: () => void;
|
||||
clearAllFiltersAndSorts: () => void;
|
||||
@@ -354,76 +345,6 @@ export const useDataGridFilters = ({
|
||||
if (onApplyFilter) onApplyFilter(filterConditions);
|
||||
}, [applyQuickWhereCondition, filterConditions, onApplyFilter]);
|
||||
|
||||
const applyColumnFilter = React.useCallback((draft: GridColumnFilterDraft): boolean => {
|
||||
const column = String(draft?.column || '').trim();
|
||||
if (!column) return false;
|
||||
if (!applyQuickWhereCondition()) return false;
|
||||
|
||||
const existingColumnConditions = filterConditions.filter((cond) => (
|
||||
String(cond?.column || '') === column && String(cond?.op || '') !== 'CUSTOM'
|
||||
));
|
||||
const existingPrimary = existingColumnConditions[0];
|
||||
const op = String(draft?.op || resolveDefaultGridFilterOperator(getColumnFilterType(column))).trim();
|
||||
const id = Number.isFinite(Number(existingPrimary?.id)) ? Number(existingPrimary?.id) : nextFilterId;
|
||||
existingColumnConditions.forEach((cond) => {
|
||||
autoDefaultFilterIdsRef.current.delete(cond.id);
|
||||
});
|
||||
autoDefaultFilterIdsRef.current.delete(id);
|
||||
|
||||
const nextCondition: GridFilterConditionState = {
|
||||
id,
|
||||
enabled: true,
|
||||
logic: normalizeFilterLogic(existingPrimary?.logic),
|
||||
column,
|
||||
op,
|
||||
value: isNoValueOp(op) ? '' : String(draft?.value ?? ''),
|
||||
value2: isNoValueOp(op) || !isBetweenOp(op) ? '' : String(draft?.value2 ?? ''),
|
||||
};
|
||||
const nextConditions = [
|
||||
...filterConditions.filter((cond) => !(
|
||||
String(cond?.column || '') === column && String(cond?.op || '') !== 'CUSTOM'
|
||||
)),
|
||||
nextCondition,
|
||||
];
|
||||
|
||||
setFilterConditions(nextConditions);
|
||||
if (!existingPrimary) {
|
||||
setNextFilterId((prev) => Math.max(prev, id + 1));
|
||||
}
|
||||
if (onApplyFilter) onApplyFilter(nextConditions);
|
||||
return true;
|
||||
}, [
|
||||
applyQuickWhereCondition,
|
||||
filterConditions,
|
||||
getColumnFilterType,
|
||||
isBetweenOp,
|
||||
isNoValueOp,
|
||||
nextFilterId,
|
||||
normalizeFilterLogic,
|
||||
onApplyFilter,
|
||||
resolveDefaultGridFilterOperator,
|
||||
]);
|
||||
|
||||
const clearColumnFilter = React.useCallback((columnName: string): boolean => {
|
||||
const column = String(columnName || '').trim();
|
||||
if (!column) return false;
|
||||
|
||||
const nextConditions = filterConditions.filter((cond) => !(
|
||||
String(cond?.column || '') === column && String(cond?.op || '') !== 'CUSTOM'
|
||||
));
|
||||
if (nextConditions.length === filterConditions.length) return true;
|
||||
if (!applyQuickWhereCondition()) return false;
|
||||
|
||||
filterConditions.forEach((cond) => {
|
||||
if (String(cond?.column || '') === column && String(cond?.op || '') !== 'CUSTOM') {
|
||||
autoDefaultFilterIdsRef.current.delete(cond.id);
|
||||
}
|
||||
});
|
||||
setFilterConditions(nextConditions);
|
||||
if (onApplyFilter) onApplyFilter(nextConditions);
|
||||
return true;
|
||||
}, [applyQuickWhereCondition, filterConditions, onApplyFilter]);
|
||||
|
||||
const applyAllFiltersEnabled = React.useCallback(() => {
|
||||
setFilterConditions((prev) => prev.map((cond) => ({ ...cond, enabled: true })));
|
||||
}, []);
|
||||
@@ -451,8 +372,6 @@ export const useDataGridFilters = ({
|
||||
addFilter,
|
||||
updateFilter,
|
||||
removeFilter,
|
||||
applyColumnFilter,
|
||||
clearColumnFilter,
|
||||
applyQuickWhereCondition,
|
||||
clearQuickWhereCondition,
|
||||
clearAllFiltersAndSorts,
|
||||
|
||||
@@ -140,43 +140,17 @@ agent_variants_for() {
|
||||
|
||||
probe_agent_revision() {
|
||||
local agent_path="$1"
|
||||
local stdout_file stderr_file probe_exit
|
||||
local request
|
||||
request='{"id":1,"method":"metadata"}'
|
||||
stdout_file="$(mktemp "${TMPDIR:-/tmp}/gonavi-agent-revision-stdout.XXXXXX")"
|
||||
stderr_file="$(mktemp "${TMPDIR:-/tmp}/gonavi-agent-revision-stderr.XXXXXX")"
|
||||
|
||||
if ! printf '%s\n' "$request" | "$agent_path" >"$stdout_file" 2>"$stderr_file"; then
|
||||
[[ -s "$stderr_file" ]] && sed "s/^/ stderr: /" "$stderr_file" >&2
|
||||
rm -f "$stdout_file" "$stderr_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
python3 - "$stdout_file" <<'PY'
|
||||
printf '%s\n' "$request" | "$agent_path" | python3 -c '
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
lines = [
|
||||
line.strip()
|
||||
for line in Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
if not lines:
|
||||
raise SystemExit(1)
|
||||
try:
|
||||
payload = json.loads(lines[0])
|
||||
except json.JSONDecodeError:
|
||||
raise SystemExit(1)
|
||||
line = sys.stdin.readline()
|
||||
payload = json.loads(line)
|
||||
data = payload.get("data") or {}
|
||||
print(data.get("agentRevision", ""))
|
||||
PY
|
||||
probe_exit=$?
|
||||
if [[ "$probe_exit" -ne 0 && -s "$stderr_file" ]]; then
|
||||
sed "s/^/ stderr: /" "$stderr_file" >&2
|
||||
fi
|
||||
rm -f "$stdout_file" "$stderr_file"
|
||||
return "$probe_exit"
|
||||
'
|
||||
}
|
||||
|
||||
probe_host_agent_revision() {
|
||||
|
||||
Reference in New Issue
Block a user