mirror of
https://github.com/apache/superset.git
synced 2026-07-02 21:05:36 +00:00
Compare commits
1 Commits
showtime-m
...
chore/ci-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e06720b067 |
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -297,7 +297,7 @@ pre-commit run eslint # Frontend linting
|
||||
## Platform-Specific Instructions
|
||||
|
||||
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
|
||||
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
|
||||
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
|
||||
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
|
||||
- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
|
||||
- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor
|
||||
|
||||
@@ -141,7 +141,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
|
||||
COPY superset/translations/ /app/translations_mo/
|
||||
RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
|
||||
pybabel compile --use-fuzzy -d /app/translations_mo || true; \
|
||||
pybabel compile -d /app/translations_mo | true; \
|
||||
fi; \
|
||||
rm -f /app/translations_mo/*/*/*.[po,json]
|
||||
|
||||
|
||||
@@ -332,28 +332,15 @@ cd superset-frontend
|
||||
npm run build-translation
|
||||
|
||||
# Backend
|
||||
pybabel compile --use-fuzzy -d superset/translations
|
||||
pybabel compile -d superset/translations
|
||||
```
|
||||
|
||||
`--use-fuzzy` includes `#, fuzzy` entries in the compiled `.mo` files. Superset
|
||||
serves fuzzy translations on purpose: the frontend build (`po2json --fuzzy`)
|
||||
already includes them, `flask fab babel-compile` (used by the release images)
|
||||
compiles with `-f`, and the production `Dockerfile` compiles with `--use-fuzzy`
|
||||
as well. This keeps machine-generated (and other draft) translations visible in
|
||||
the UI rather than falling back to English while they await review.
|
||||
|
||||
### Backfilling missing translations with AI
|
||||
|
||||
For languages with many untranslated strings, the repo includes a script that
|
||||
uses Claude AI to generate draft translations for any missing entries. All
|
||||
AI-generated strings are marked `#, fuzzy` and tagged with an attribution
|
||||
comment so that human reviewers know they need to be checked.
|
||||
|
||||
Note that `#, fuzzy` marks a translation as *needing review*, not as *withheld*:
|
||||
both the frontend and backend builds serve fuzzy entries (see [Applying
|
||||
translations](#applying-translations) above), so an AI-generated string is shown
|
||||
in the UI as soon as it is built and deployed. Reviewers should verify each
|
||||
entry and remove the `#, fuzzy` flag to promote it to a confirmed translation.
|
||||
comment so that human reviewers know they need to be checked before merging.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"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
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Change Log
|
||||
|
||||
30
superset-frontend/package-lock.json
generated
30
superset-frontend/package-lock.json
generated
@@ -182,7 +182,7 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.6",
|
||||
"@storybook/addon-docs": "10.4.5",
|
||||
"@storybook/addon-links": "10.4.4",
|
||||
"@storybook/react-webpack5": "10.4.4",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
@@ -9718,16 +9718,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.6.tgz",
|
||||
"integrity": "sha512-aWAfP5JMiT5a3zBJizwroCRzOCqZwDTJmvsYvwMD3ilIEa/kT1vhf6Xrbk4XIPhDwbh8Hpb/Gfnka1xBYEISWg==",
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.5.tgz",
|
||||
"integrity": "sha512-9mIV0maIxixfuvdpNhr3QMeU/gbJKeaBcWhPYuf176cqDZAG9EUhZ50TIinxeFRbyEGRJqaLPoiYwIu4GJu3jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.4.6",
|
||||
"@storybook/csf-plugin": "10.4.5",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@storybook/react-dom-shim": "10.4.6",
|
||||
"@storybook/react-dom-shim": "10.4.5",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -9738,7 +9738,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6"
|
||||
"storybook": "^10.4.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9747,9 +9747,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.6.tgz",
|
||||
"integrity": "sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==",
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.5.tgz",
|
||||
"integrity": "sha512-fKdikHC7cDgSuaBirPwvgFBmfO//3cln0y3GmDEQchUV2VFDrZ7ZL1/iH7dA21XuiFFhQcDRRkArJmvMAGG5Cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -9761,7 +9761,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6"
|
||||
"storybook": "^10.4.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9800,9 +9800,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.6.tgz",
|
||||
"integrity": "sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA==",
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.5.tgz",
|
||||
"integrity": "sha512-OsSsSLulBmdKTz7MIKLgoWADZB8bjYaAjZZy/THdI50G/TTd6FVSXQMCM7GO7xQZ/EguRY1PmjOVCLbgcnXsDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9815,7 +9815,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.4.6",
|
||||
"storybook": "^10.4.5",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.6",
|
||||
"@storybook/addon-docs": "10.4.5",
|
||||
"@storybook/addon-links": "10.4.4",
|
||||
"@storybook/react-webpack5": "10.4.4",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
|
||||
@@ -713,5 +713,4 @@ export interface DataColumnMeta {
|
||||
isChildColumn?: boolean;
|
||||
description?: string;
|
||||
currencyCodeColumn?: string;
|
||||
isFilterable?: boolean;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"xss": "^1.0.15",
|
||||
"lodash-es": "^4.18.1"
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/styled": "^11.14.1",
|
||||
|
||||
@@ -33,9 +33,8 @@ import { getLayerConfig } from '../util/controlPanelUtil';
|
||||
export default class CartodiagramPlugin extends ChartPlugin {
|
||||
constructor(opts: CartodiagramPluginConstructorOpts) {
|
||||
const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
description:
|
||||
'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.',
|
||||
),
|
||||
name: t('Cartodiagram'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
|
||||
@@ -28,9 +28,8 @@ export default class PopKPIPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('KPI'),
|
||||
description: t(
|
||||
description:
|
||||
'Showcases a metric along with a comparison of value, change, and percent change for a selected time period.',
|
||||
),
|
||||
name: t('Big Number with Time Period Comparison'),
|
||||
tags: [
|
||||
t('Comparison'),
|
||||
|
||||
@@ -1204,11 +1204,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
onClick:
|
||||
emitCrossFilters && !valueRange && !isMetric
|
||||
? () => {
|
||||
const isFilterable = columnsMeta.find(
|
||||
(cm: DataColumnMeta) => cm.key === key,
|
||||
)?.isFilterable;
|
||||
// allow selecting text in a cell
|
||||
if (!getSelectedText() && isFilterable !== false) {
|
||||
if (!getSelectedText()) {
|
||||
toggleFilter(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +232,6 @@ const processColumns = memoizeOne(function processColumns(
|
||||
const metricsSet = new Set(metrics);
|
||||
const percentMetricsSet = new Set(percentMetrics);
|
||||
const rawPercentMetricsSet = new Set(rawPercentMetrics);
|
||||
const columnsByName = new Map(
|
||||
(props.datasource.columns ?? []).map(col => [col.column_name, col]),
|
||||
);
|
||||
|
||||
const columns: DataColumnMeta[] = (colnames || [])
|
||||
.filter(
|
||||
@@ -247,7 +244,6 @@ const processColumns = memoizeOne(function processColumns(
|
||||
const config = columnConfig[key] || {};
|
||||
// for the purpose of presentation, only numeric values are treated as metrics
|
||||
// because users can also add things like `MAX(str_col)` as a metric.
|
||||
const isFilterable = columnsByName.get(key)?.filterable;
|
||||
const isMetric = metricsSet.has(key) && isNumeric(key, records);
|
||||
const isPercentMetric = percentMetricsSet.has(key);
|
||||
const label =
|
||||
@@ -330,7 +326,6 @@ const processColumns = memoizeOne(function processColumns(
|
||||
isPercentMetric,
|
||||
formatter,
|
||||
config,
|
||||
isFilterable,
|
||||
description,
|
||||
currencyCodeColumn,
|
||||
};
|
||||
|
||||
@@ -2534,33 +2534,3 @@ test('sorts genuinely string columns alphanumerically', () => {
|
||||
const values = Array.from(cells).map(td => td.textContent);
|
||||
expect(values).toEqual(['apple', 'banana', 'cherry']);
|
||||
});
|
||||
|
||||
test('TableChart should NOT emit cross-filter when clicking a cell in a not-filterable column', () => {
|
||||
const setDataMask = jest.fn();
|
||||
const props = transformProps({
|
||||
...testData.basic,
|
||||
datasource: {
|
||||
...testData.basic.datasource,
|
||||
columns: [{ column_name: 'name', filterable: false } as any],
|
||||
},
|
||||
hooks: { setDataMask },
|
||||
emitCrossFilters: true,
|
||||
});
|
||||
render(
|
||||
<ProviderWrapper>
|
||||
<TableChart
|
||||
{...props}
|
||||
emitCrossFilters
|
||||
setDataMask={setDataMask}
|
||||
sticky={false}
|
||||
/>
|
||||
</ProviderWrapper>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Michael'));
|
||||
|
||||
const crossFilterCall = setDataMask.mock.calls.find(
|
||||
(call: any[]) => call[0]?.filterState?.filters,
|
||||
);
|
||||
expect(crossFilterCall).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
createStore,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import reducerIndex from 'spec/helpers/reducerIndex';
|
||||
@@ -31,7 +30,7 @@ import {
|
||||
useDashboardCharts,
|
||||
useDashboardDatasets,
|
||||
} from 'src/hooks/apiResources';
|
||||
import { SupersetApiError, SupersetClient } from '@superset-ui/core';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import CrudThemeProvider from 'src/components/CrudThemeProvider';
|
||||
import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
|
||||
import {
|
||||
@@ -560,48 +559,6 @@ test('does not overwrite filterState when modern native_filters URL format is us
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('renders a not-found state instead of throwing when the dashboard 404s', async () => {
|
||||
mockUseDashboard.mockReturnValue({
|
||||
result: null,
|
||||
error: new SupersetApiError({ status: 404, message: 'Not found' }),
|
||||
});
|
||||
mockUseDashboardCharts.mockReturnValue({
|
||||
result: null,
|
||||
error: new SupersetApiError({ status: 404, message: 'Not found' }),
|
||||
});
|
||||
mockUseDashboardDatasets.mockReturnValue({
|
||||
result: null,
|
||||
error: new SupersetApiError({ status: 404, message: 'Not found' }),
|
||||
status: 'error',
|
||||
});
|
||||
|
||||
render(
|
||||
<Suspense fallback="loading">
|
||||
<DashboardPage idOrSlug="404" />
|
||||
</Suspense>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: {
|
||||
dashboardInfo: {},
|
||||
dashboardState: { sliceIds: [] },
|
||||
nativeFilters: { filters: {} },
|
||||
dataMask: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText('This dashboard does not exist'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('dashboard-builder')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', { name: 'See all dashboards' }),
|
||||
);
|
||||
expect(window.location.pathname).toBe('/dashboard/list/');
|
||||
});
|
||||
|
||||
test('clears undo history after hydrating the dashboard', async () => {
|
||||
render(
|
||||
<Suspense fallback="loading">
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useTheme } from '@apache-superset/core/theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
import { Loading } from '@superset-ui/core/components';
|
||||
import {
|
||||
useDashboard,
|
||||
useDashboardCharts,
|
||||
@@ -67,8 +67,7 @@ import SyncDashboardState, {
|
||||
getDashboardContextLocalStorage,
|
||||
} from '../components/SyncDashboardState';
|
||||
import { AutoRefreshProvider } from '../contexts/AutoRefreshContext';
|
||||
import { Filter, PartialFilters, SupersetApiError } from '@superset-ui/core';
|
||||
import { RoutePaths } from 'src/views/routePaths';
|
||||
import { Filter, PartialFilters } from '@superset-ui/core';
|
||||
import {
|
||||
parseRisonFilters,
|
||||
risonFiltersToExtraFormDataFilters,
|
||||
@@ -152,9 +151,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
const isDashboardHydrated = useRef(false);
|
||||
|
||||
const error = dashboardApiError || chartsApiError;
|
||||
// Only 404 gets a graceful not-found state; a 403 (access denied) still
|
||||
// surfaces through the error boundary.
|
||||
const isNotFoundError = (error as SupersetApiError | null)?.status === 404;
|
||||
const readyToRender = Boolean(dashboard && charts);
|
||||
const { dashboard_title, id = 0 } = dashboard || {};
|
||||
|
||||
@@ -369,21 +365,18 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetsApiError) {
|
||||
// A missing dashboard also 404s its datasets; the not-found state covers it.
|
||||
if (!isNotFoundError) {
|
||||
addDangerToast(
|
||||
t('Error loading chart datasources. Filters may not work correctly.'),
|
||||
);
|
||||
}
|
||||
addDangerToast(
|
||||
t('Error loading chart datasources. Filters may not work correctly.'),
|
||||
);
|
||||
} else {
|
||||
dispatch(setDatasources(datasets));
|
||||
}
|
||||
}, [addDangerToast, datasets, datasetsApiError, dispatch, isNotFoundError]);
|
||||
}, [addDangerToast, datasets, datasetsApiError, dispatch]);
|
||||
|
||||
const relevantDataMask = useSelector(selectRelevantDatamask);
|
||||
const activeFilters = useSelector(selectActiveFilters);
|
||||
|
||||
if (error && !isNotFoundError) throw error; // caught in error boundary
|
||||
if (error) throw error; // caught in error boundary
|
||||
|
||||
const globalStyles = useMemo(
|
||||
() => [
|
||||
@@ -396,25 +389,9 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
[theme],
|
||||
);
|
||||
|
||||
if (error && !isNotFoundError) throw error; // caught in error boundary
|
||||
if (error) throw error; // caught in error boundary
|
||||
|
||||
const DashboardBuilderComponent = useMemo(() => <DashboardBuilder />, []);
|
||||
|
||||
if (isNotFoundError) {
|
||||
return (
|
||||
<EmptyState
|
||||
size="large"
|
||||
image="empty-dashboard.svg"
|
||||
title={t('This dashboard does not exist')}
|
||||
description={t(
|
||||
'The dashboard you are looking for may have been deleted or moved.',
|
||||
)}
|
||||
buttonText={t('See all dashboards')}
|
||||
buttonAction={() => history.push(RoutePaths.DASHBOARD_LIST)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Global styles={globalStyles} />
|
||||
|
||||
37
superset-frontend/src/explore/components/DataTableControl/FilterInput.test.tsx
Executable file → Normal file
37
superset-frontend/src/explore/components/DataTableControl/FilterInput.test.tsx
Executable file → Normal file
@@ -34,40 +34,3 @@ test('Render a FilterInput', async () => {
|
||||
|
||||
expect(onChangeHandler).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
test('FilterInput auto-focuses when a non-editable element (e.g. a tab) has focus', () => {
|
||||
const onChangeHandler = jest.fn();
|
||||
const button = document.createElement('button');
|
||||
document.body.appendChild(button);
|
||||
try {
|
||||
button.focus();
|
||||
expect(document.activeElement).toBe(button);
|
||||
|
||||
render(<FilterInput onChangeHandler={onChangeHandler} shouldFocus />);
|
||||
const filterInput = screen.getByPlaceholderText('Search');
|
||||
|
||||
// Auto-focus should fire — a button is not an editable element
|
||||
expect(document.activeElement).toBe(filterInput);
|
||||
} finally {
|
||||
document.body.removeChild(button);
|
||||
}
|
||||
});
|
||||
|
||||
test('FilterInput does not steal focus when another input already has focus', () => {
|
||||
const onChangeHandler = jest.fn();
|
||||
const otherInput = document.createElement('input');
|
||||
document.body.appendChild(otherInput);
|
||||
try {
|
||||
otherInput.focus();
|
||||
expect(document.activeElement).toBe(otherInput);
|
||||
|
||||
render(<FilterInput onChangeHandler={onChangeHandler} shouldFocus />);
|
||||
const filterInput = screen.getByPlaceholderText('Search');
|
||||
|
||||
// FilterInput should not have stolen focus from the already-focused input
|
||||
expect(document.activeElement).not.toBe(filterInput);
|
||||
expect(document.activeElement).toBe(otherInput);
|
||||
} finally {
|
||||
document.body.removeChild(otherInput);
|
||||
}
|
||||
});
|
||||
|
||||
15
superset-frontend/src/explore/components/DataTableControl/index.tsx
Executable file → Normal file
15
superset-frontend/src/explore/components/DataTableControl/index.tsx
Executable file → Normal file
@@ -98,20 +98,9 @@ export const FilterInput = ({
|
||||
const inputRef: RefObject<any> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Focus the input element when the component mounts
|
||||
if (inputRef.current && shouldFocus) {
|
||||
// Skip auto-focus only when an editable element already has focus (e.g.
|
||||
// user is typing in a form control when this pane remounts after a data
|
||||
// refresh). Non-editable focused elements like tabs/buttons still allow
|
||||
// auto-focus so the search box focuses on first open.
|
||||
const activeEl = document.activeElement;
|
||||
const editableFocused =
|
||||
activeEl instanceof HTMLElement &&
|
||||
(activeEl.tagName === 'INPUT' ||
|
||||
activeEl.tagName === 'TEXTAREA' ||
|
||||
activeEl.isContentEditable);
|
||||
if (!editableFocused) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -541,9 +541,8 @@ function SavedQueryList({
|
||||
key: 'search',
|
||||
input: 'search',
|
||||
operator: FilterOperator.AllText,
|
||||
toolTipDescription: t(
|
||||
toolTipDescription:
|
||||
'Searches all text fields: Name, Description, Database & Schema',
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: t('Database'),
|
||||
|
||||
@@ -133,9 +133,8 @@ function TagList(props: TagListProps) {
|
||||
const emptyState = {
|
||||
title: t('No Tags created'),
|
||||
image: 'dashboard.svg',
|
||||
description: t(
|
||||
description:
|
||||
'Create a new tag and assign it to existing entities like charts or dashboards',
|
||||
),
|
||||
buttonAction: () => setShowTagModal(true),
|
||||
buttonIcon: <Icons.PlusOutlined iconSize="m" data-test="add-rule-empty" />,
|
||||
buttonText: t('Create a new Tag'),
|
||||
|
||||
@@ -8250,16 +8250,16 @@ msgid "List"
|
||||
msgstr "Auflisten"
|
||||
|
||||
msgid "List Groups"
|
||||
msgstr "Gruppen auflisten"
|
||||
msgstr "Gruppen"
|
||||
|
||||
msgid "List Roles"
|
||||
msgstr "Rollen auflisten"
|
||||
msgstr "Rollen"
|
||||
|
||||
msgid "List Unique Values"
|
||||
msgstr "Eindeutige Werte auflisten"
|
||||
|
||||
msgid "List Users"
|
||||
msgstr "Benutzer*innen auflisten"
|
||||
msgstr "Benutzer*innen"
|
||||
|
||||
msgid "List of extra columns made available in JavaScript functions"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user