diff --git a/deploy/helm/thecombine/charts/frontend/values.yaml b/deploy/helm/thecombine/charts/frontend/values.yaml
index 42240c00fb..5ae4da14cf 100644
--- a/deploy/helm/thecombine/charts/frontend/values.yaml
+++ b/deploy/helm/thecombine/charts/frontend/values.yaml
@@ -23,6 +23,6 @@ imageName: combine_frontend
# The additional domain list is a space-separated string list of domains
combineAddlDomainList: ""
configAnalyticsWriteKey: ""
-configCaptchaSiteKey: "None - defined in profiles"
+configCaptchaSiteKey: ""
configOffline: false
configShowCertExpiration: false
diff --git a/deploy/scripts/app_release.py b/deploy/scripts/app_release.py
index e7bbb1d3f1..604cd369fb 100755
--- a/deploy/scripts/app_release.py
+++ b/deploy/scripts/app_release.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
from __future__ import annotations
diff --git a/deploy/scripts/aws_env.py b/deploy/scripts/aws_env.py
index ed7d983324..251bc8fabe 100755
--- a/deploy/scripts/aws_env.py
+++ b/deploy/scripts/aws_env.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
"""Set AWS Environment variables from aws cli profiles."""
from __future__ import annotations
diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py
index 09237f4fb0..969c1533be 100755
--- a/deploy/scripts/build.py
+++ b/deploy/scripts/build.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Build the containerd images for The Combine.
diff --git a/deploy/scripts/check_certs.py b/deploy/scripts/check_certs.py
index b7224dd900..cbd47e6a43 100755
--- a/deploy/scripts/check_certs.py
+++ b/deploy/scripts/check_certs.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
import argparse
import subprocess
diff --git a/deploy/scripts/combine_charts.py b/deploy/scripts/combine_charts.py
index 6e5750af6a..3dff99e3b9 100755
--- a/deploy/scripts/combine_charts.py
+++ b/deploy/scripts/combine_charts.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
"""
Update the Helm chart version with the specified version.
"""
diff --git a/deploy/scripts/kube_env.py b/deploy/scripts/kube_env.py
index 2e70824f56..413ded5b32 100755
--- a/deploy/scripts/kube_env.py
+++ b/deploy/scripts/kube_env.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""Manage the Kubernetes environment for kubectl & helm."""
from __future__ import annotations
diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py
index 858f518122..66cebe5b6d 100755
--- a/deploy/scripts/package_images.py
+++ b/deploy/scripts/package_images.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
"""
Package the container images used for The Combine to support air-gapped installation.
diff --git a/deploy/scripts/sem_dom_import.py b/deploy/scripts/sem_dom_import.py
index d434655288..d91823f013 100755
--- a/deploy/scripts/sem_dom_import.py
+++ b/deploy/scripts/sem_dom_import.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
"""
Create data files for importing the Semantic Domain information into the Mongo database.
diff --git a/deploy/scripts/setup_cluster.py b/deploy/scripts/setup_cluster.py
index aee6ef1af4..e65abd52fe 100755
--- a/deploy/scripts/setup_cluster.py
+++ b/deploy/scripts/setup_cluster.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""Install the pre-requisite helm charts for the Combine on a k8s cluster."""
from __future__ import annotations
diff --git a/deploy/scripts/setup_combine.py b/deploy/scripts/setup_combine.py
index efb8c2c1f2..4320a0cf6f 100755
--- a/deploy/scripts/setup_combine.py
+++ b/deploy/scripts/setup_combine.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
"""
Install The Combine Helm charts on a specified Kubernetes cluster.
diff --git a/deploy/scripts/setup_target.py b/deploy/scripts/setup_target.py
index 3bce8d35c7..821905e0f3 100755
--- a/deploy/scripts/setup_target.py
+++ b/deploy/scripts/setup_target.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
import argparse
import os
diff --git a/maintenance/scripts/add_user_to_proj.py b/maintenance/scripts/add_user_to_proj.py
index b9e5a62f44..cebdb9e526 100755
--- a/maintenance/scripts/add_user_to_proj.py
+++ b/maintenance/scripts/add_user_to_proj.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Add user to a project.
diff --git a/maintenance/scripts/combine_backup.py b/maintenance/scripts/combine_backup.py
index 9841e2715b..a73f176321 100755
--- a/maintenance/scripts/combine_backup.py
+++ b/maintenance/scripts/combine_backup.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""Create a backup of TheCombine and push the file to AWS S3 service."""
import argparse
diff --git a/maintenance/scripts/combine_restore.py b/maintenance/scripts/combine_restore.py
index d98b9e55df..868738f994 100755
--- a/maintenance/scripts/combine_restore.py
+++ b/maintenance/scripts/combine_restore.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Restore The Combine from a backup stored in the AWS S3 service.
diff --git a/maintenance/scripts/db_update_audio_type.py b/maintenance/scripts/db_update_audio_type.py
index 5b2ea027e9..3c3cb0244e 100755
--- a/maintenance/scripts/db_update_audio_type.py
+++ b/maintenance/scripts/db_update_audio_type.py
@@ -1,4 +1,4 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python
import argparse
import logging
diff --git a/maintenance/scripts/get_fonts.py b/maintenance/scripts/get_fonts.py
index 2186be0cd1..d3904b0ecf 100755
--- a/maintenance/scripts/get_fonts.py
+++ b/maintenance/scripts/get_fonts.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Generates font support for all SIL fonts used in Mui-Language-Picker.
diff --git a/maintenance/scripts/monitor.py b/maintenance/scripts/monitor.py
index 8e82c8396d..191d7e9666 100755
--- a/maintenance/scripts/monitor.py
+++ b/maintenance/scripts/monitor.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Monitor TLS secrets for changes and push changes to AWS S3.
diff --git a/maintenance/scripts/rm_project.py b/maintenance/scripts/rm_project.py
index ffeddf4345..d35fd4dabf 100755
--- a/maintenance/scripts/rm_project.py
+++ b/maintenance/scripts/rm_project.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Remove a project and its associated data from TheCombine.
diff --git a/maintenance/scripts/update_cert.py b/maintenance/scripts/update_cert.py
index ada2cdcf9b..0068be981b 100755
--- a/maintenance/scripts/update_cert.py
+++ b/maintenance/scripts/update_cert.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Check the expiration time of the TLS secret and update if needed.
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 3bb803e9c2..5f2b8a25f9 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -51,6 +51,7 @@
"forgotPassword": "Forgot password?",
"login": "Log in",
"failed": "Failed to log in. Please check your username and password.",
+ "failedUnknownReason": "Failed to log in. Something went wrong with The Combine.",
"backToLogin": "Back to login",
"signUp": "Sign Up",
"signUpNew": "Sign Up New User",
diff --git a/scripts/clean_aws_repo.py b/scripts/clean_aws_repo.py
index a4ea7f8383..6a03a66cf9 100755
--- a/scripts/clean_aws_repo.py
+++ b/scripts/clean_aws_repo.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
This script cleans out old docker images from the AWS ECR repository.
diff --git a/scripts/cleanup_local_repo.py b/scripts/cleanup_local_repo.py
index 9cbb2d06a8..d9e0fe9a11 100755
--- a/scripts/cleanup_local_repo.py
+++ b/scripts/cleanup_local_repo.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""Remove all temporary files and folders within the local Git repository.
diff --git a/scripts/generate_openapi.py b/scripts/generate_openapi.py
index bc1bb21225..7d4383b892 100644
--- a/scripts/generate_openapi.py
+++ b/scripts/generate_openapi.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""Regenerate the frontend OpenAPI bindings to the backend.
diff --git a/scripts/get_fonts_dev.py b/scripts/get_fonts_dev.py
index 1c035b6460..9f4b3ec820 100755
--- a/scripts/get_fonts_dev.py
+++ b/scripts/get_fonts_dev.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Runs maintenance/scripts/get_fonts.py with dev arguments for -f and -o.
diff --git a/scripts/split_dictionary.py b/scripts/split_dictionary.py
index 00e008ecd1..d922df9a9b 100644
--- a/scripts/split_dictionary.py
+++ b/scripts/split_dictionary.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Splits a dictionary file into smaller files.
"""
diff --git a/scripts/subtitle_tutorial_video.py b/scripts/subtitle_tutorial_video.py
index 32d792daec..5d7533b7f9 100644
--- a/scripts/subtitle_tutorial_video.py
+++ b/scripts/subtitle_tutorial_video.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
"""
Add subtitles to a tutorial video.
If video path is not provided, still generates .srt files.
diff --git a/src/backend/index.ts b/src/backend/index.ts
index 539a97d096..718d125f51 100644
--- a/src/backend/index.ts
+++ b/src/backend/index.ts
@@ -44,10 +44,10 @@ const config = new Api.Configuration(config_parameters);
/** A list of URL patterns for which the frontend explicitly handles errors
* and the blanket error pop ups should be suppressed.*/
const whiteListedErrorUrls = [
- "users/authenticate",
- "users/captcha",
"/speakers/create/",
"/speakers/update/",
+ "/users/authenticate",
+ "/users/captcha/",
];
// Create an axios instance to allow for attaching interceptors to it.
@@ -66,16 +66,13 @@ axiosInstance.interceptors.response.use(undefined, (err: AxiosError) => {
router.navigate(Path.Login);
}
- // Check for fatal errors (4xx-5xx).
- if (
- status >= StatusCodes.BAD_REQUEST &&
- status <= StatusCodes.NETWORK_AUTHENTICATION_REQUIRED
- ) {
- // Suppress error pop-ups for URLs the frontend already explicitly handles.
- if (url && whiteListedErrorUrls.some((u) => url.includes(u))) {
- return Promise.reject(err);
- }
+ // Suppress error pop-ups for URLs the frontend already explicitly handles.
+ if (url && whiteListedErrorUrls.some((u) => url.includes(u))) {
+ return Promise.reject(err);
+ }
+ // Check for fatal errors (400+).
+ if (status >= StatusCodes.BAD_REQUEST) {
console.error(err);
enqueueSnackbar(`${status} ${response.statusText}\n${err.config.url}`);
}
diff --git a/src/components/Login/Captcha.tsx b/src/components/Login/Captcha.tsx
index 8f3de5f5ed..8e740176dd 100644
--- a/src/components/Login/Captcha.tsx
+++ b/src/components/Login/Captcha.tsx
@@ -22,14 +22,6 @@ export default function Captcha(props: CaptchaProps): ReactElement {
setSuccess(!isRequired.current);
}, [isRequired, setSuccess]);
- const siteKey =
- process.env.NODE_ENV === "production"
- ? RuntimeConfig.getInstance().captchaSiteKey()
- : // https://developers.cloudflare.com/turnstile/troubleshooting/testing/
- // has dummy site keys for development and testing; options are
- // invisible pass, invisible fail, visible pass, visible fail, forced interaction
- "1x00000000000000000000AA"; // visible pass
-
const fail = (): void => {
setSuccess(false);
toast.error(t("captcha.error"));
@@ -49,7 +41,7 @@ export default function Captcha(props: CaptchaProps): ReactElement {
onExpire={fail}
onSuccess={verify}
options={{ language: i18n.resolvedLanguage, theme: "light" }}
- siteKey={siteKey}
+ siteKey={RuntimeConfig.getInstance().captchaSiteKey()}
/>
) : (
diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx
index f093471b52..3846ee7bfb 100644
--- a/src/components/Login/Login.tsx
+++ b/src/components/Login/Login.tsx
@@ -46,6 +46,9 @@ export enum LoginId {
export default function Login(): ReactElement {
const dispatch = useAppDispatch();
+ const loginError = useAppSelector(
+ (state: StoreState) => state.loginState.error
+ );
const status = useAppSelector(
(state: StoreState) => state.loginState.loginStatus
);
@@ -147,7 +150,11 @@ export default function Login(): ReactElement {
style={{ color: "red", marginBottom: 24, marginTop: 24 }}
variant="body2"
>
- {t("login.failed")}
+ {t(
+ loginError.includes("401")
+ ? "login.failed"
+ : "login.failedUnknownReason"
+ )}
)}
diff --git a/src/components/Login/Redux/LoginActions.ts b/src/components/Login/Redux/LoginActions.ts
index 519d003ca2..49da53dc04 100644
--- a/src/components/Login/Redux/LoginActions.ts
+++ b/src/components/Login/Redux/LoginActions.ts
@@ -59,7 +59,7 @@ export function asyncLogIn(username: string, password: string) {
router.navigate(Path.ProjScreen);
})
.catch((err) =>
- dispatch(loginFailure(err.response?.data ?? err.message))
+ dispatch(loginFailure(`${err.response?.status ?? err.message}`))
);
};
}
diff --git a/src/components/Project/ProjectActions.ts b/src/components/Project/ProjectActions.ts
index 672b9bf341..1984d7acf3 100644
--- a/src/components/Project/ProjectActions.ts
+++ b/src/components/Project/ProjectActions.ts
@@ -1,4 +1,9 @@
import { type Action, type PayloadAction } from "@reduxjs/toolkit";
+import {
+ type MRT_ColumnOrderState,
+ type MRT_Updater,
+ type MRT_VisibilityState,
+} from "material-react-table";
import { type Project, type Speaker, type User } from "api/models";
import {
@@ -9,6 +14,8 @@ import {
import { setProjectId } from "backend/localStorage";
import {
resetAction,
+ setColumnOrderAction,
+ setColumnVisibilityAction,
setProjectAction,
setSemanticDomainsAction,
setSpeakerAction,
@@ -25,6 +32,18 @@ export function resetCurrentProject(): Action {
return resetAction();
}
+export function setReviewEntriesColumnOrder(
+ updater: MRT_Updater
+): PayloadAction {
+ return setColumnOrderAction(updater);
+}
+
+export function setReviewEntriesColumnVisibility(
+ updater: MRT_Updater
+): PayloadAction {
+ return setColumnVisibilityAction(updater);
+}
+
export function setCurrentProject(project?: Project): PayloadAction {
return setProjectAction(project ?? newProject());
}
diff --git a/src/components/Project/ProjectReducer.ts b/src/components/Project/ProjectReducer.ts
index e17e067f6c..2abfce0f3f 100644
--- a/src/components/Project/ProjectReducer.ts
+++ b/src/components/Project/ProjectReducer.ts
@@ -8,8 +8,29 @@ const projectSlice = createSlice({
initialState: defaultState,
reducers: {
resetAction: () => defaultState,
+ setColumnOrderAction: (state, action) => {
+ const columns = state.reviewEntriesColumns;
+ // Payload is a state updater, which can either be a new state
+ // or a function that takes the previous state and returns a new state.
+ if (typeof action.payload === "function") {
+ columns.columnOrder = action.payload(columns.columnOrder);
+ } else {
+ columns.columnOrder = action.payload;
+ }
+ },
+ setColumnVisibilityAction: (state, action) => {
+ const columns = state.reviewEntriesColumns;
+ // Payload is a state updater, which can either be a new state
+ // or a function that takes the previous state and returns a new state.
+ if (typeof action.payload === "function") {
+ columns.columnVisibility = action.payload(columns.columnVisibility);
+ } else {
+ columns.columnVisibility = action.payload;
+ }
+ },
setProjectAction: (state, action) => {
if (state.project.id !== action.payload.id) {
+ state.reviewEntriesColumns = defaultState.reviewEntriesColumns;
state.speaker = undefined;
state.users = [];
}
@@ -31,6 +52,8 @@ const projectSlice = createSlice({
export const {
resetAction,
+ setColumnOrderAction,
+ setColumnVisibilityAction,
setProjectAction,
setSemanticDomainsAction,
setSpeakerAction,
diff --git a/src/components/Project/ProjectReduxTypes.ts b/src/components/Project/ProjectReduxTypes.ts
index f1335688e4..ee575f5a1a 100644
--- a/src/components/Project/ProjectReduxTypes.ts
+++ b/src/components/Project/ProjectReduxTypes.ts
@@ -1,9 +1,20 @@
+import {
+ type MRT_ColumnOrderState,
+ type MRT_VisibilityState,
+} from "material-react-table";
+
import { type Project, type Speaker, type User } from "api/models";
import { type Hash } from "types/hash";
import { newProject } from "types/project";
export interface CurrentProjectState {
project: Project;
+ /** For project-level persistance of ReviewEntriesTable's managed states
+ * per https://www.material-react-table.com/docs/guides/state-management */
+ reviewEntriesColumns: {
+ columnOrder: MRT_ColumnOrderState;
+ columnVisibility: MRT_VisibilityState;
+ };
semanticDomains?: Hash;
speaker?: Speaker;
users: User[];
@@ -11,5 +22,6 @@ export interface CurrentProjectState {
export const defaultState: CurrentProjectState = {
project: newProject(),
+ reviewEntriesColumns: { columnOrder: [], columnVisibility: {} },
users: [],
};
diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx
index eda333755c..5bfd9b55a5 100644
--- a/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx
+++ b/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx
@@ -4,12 +4,14 @@ import {
PlayArrow,
} from "@mui/icons-material";
import { Typography } from "@mui/material";
+import { createSelector } from "@reduxjs/toolkit";
import {
MaterialReactTable,
type MRT_Localization,
type MRT_PaginationState,
type MRT_Row,
type MRT_RowVirtualizer,
+ type MRT_VisibilityState,
createMRTColumnHelper,
useMaterialReactTable,
} from "material-react-table";
@@ -19,10 +21,14 @@ import { useTranslation } from "react-i18next";
import { GramCatGroup, type GrammaticalInfo, type Word } from "api/models";
import { getAllSpeakers, getFrontierWords, getWord } from "backend";
import { topBarHeight } from "components/LandingPage/TopBar";
+import {
+ setReviewEntriesColumnOrder,
+ setReviewEntriesColumnVisibility,
+} from "components/Project/ProjectActions";
import * as Cell from "goals/ReviewEntries/ReviewEntriesTable/Cells";
import * as ff from "goals/ReviewEntries/ReviewEntriesTable/filterFn";
import * as sf from "goals/ReviewEntries/ReviewEntriesTable/sortingFn";
-import { useAppSelector } from "rootRedux/hooks";
+import { useAppDispatch, useAppSelector } from "rootRedux/hooks";
import { type StoreState } from "rootRedux/types";
import { type Hash } from "types/hash";
@@ -61,6 +67,20 @@ const IconHeaderPaddingTop = "2px"; // Vertical offset for a small icon as Heade
const IconHeaderWidth = 20; // Width for a small icon as Header
const SensesHeaderWidth = 15; // Width for # as Header
+export enum ColumnId {
+ Definitions = "definitions",
+ Delete = "delete",
+ Domains = "domains",
+ Edit = "edit",
+ Flag = "flag",
+ Glosses = "glosses",
+ Note = "note",
+ PartOfSpeech = "partOfSpeech",
+ Pronunciations = "pronunciations",
+ Senses = "senses",
+ Vernacular = "vernacular",
+}
+
// Constants for pagination state.
const rowsPerPage = [10, 100];
const initPaginationState: MRT_PaginationState = {
@@ -76,12 +96,32 @@ interface RowsPerPageOption {
export default function ReviewEntriesTable(props: {
disableVirtualization?: boolean;
}): ReactElement {
- const showDefinitions = useAppSelector(
- (state: StoreState) => state.currentProjectState.project.definitionsEnabled
- );
- const showGrammaticalInfo = useAppSelector(
+ const dispatch = useAppDispatch();
+
+ const columnOrder = useAppSelector(
(state: StoreState) =>
- state.currentProjectState.project.grammaticalInfoEnabled
+ state.currentProjectState.reviewEntriesColumns.columnOrder
+ );
+ const columnVisibility: MRT_VisibilityState = useAppSelector(
+ // Memoized selector that ensures correct column visibility.
+ createSelector(
+ [
+ (state: StoreState) =>
+ state.currentProjectState.reviewEntriesColumns.columnVisibility,
+ (state: StoreState) =>
+ state.currentProjectState.project.definitionsEnabled,
+ (state: StoreState) =>
+ state.currentProjectState.project.grammaticalInfoEnabled,
+ ],
+ (colVis, def, pos) => ({
+ ...colVis,
+ [ColumnId.Definitions]: (colVis[ColumnId.Definitions] ?? def) && def,
+ [ColumnId.PartOfSpeech]: (colVis[ColumnId.PartOfSpeech] ?? pos) && pos,
+ })
+ )
+ );
+ const { definitionsEnabled, grammaticalInfoEnabled } = useAppSelector(
+ (state: StoreState) => state.currentProjectState.project
);
const autoResetPageIndexRef = useRef(true);
@@ -171,6 +211,7 @@ export default function ReviewEntriesTable(props: {
enableHiding: false,
Header: "",
header: t("reviewEntries.columns.edit"),
+ id: ColumnId.Edit,
size: IconColumnSize,
visibleInShowHideMenu: false,
}),
@@ -181,6 +222,7 @@ export default function ReviewEntriesTable(props: {
enableColumnOrdering: false,
enableHiding: false,
header: t("reviewEntries.columns.vernacular"),
+ id: ColumnId.Vernacular,
size: BaselineColumnSize - 40,
}),
@@ -190,7 +232,7 @@ export default function ReviewEntriesTable(props: {
filterFn: "equals",
Header: #,
header: t("reviewEntries.columns.sensesCount"),
- id: "senses",
+ id: ColumnId.Senses,
muiTableHeadCellProps: {
sx: {
"& .Mui-TableHeadCell-Content-Wrapper": {
@@ -207,10 +249,10 @@ export default function ReviewEntriesTable(props: {
Cell: ({ row }: CellProps) => ,
filterFn: ff.filterFnDefinitions,
header: t("reviewEntries.columns.definitions"),
- id: "definitions",
+ id: ColumnId.Definitions,
size: BaselineColumnSize + 20,
sortingFn: sf.sortingFnDefinitions,
- visibleInShowHideMenu: showDefinitions,
+ visibleInShowHideMenu: definitionsEnabled,
}),
// Glosses column
@@ -218,7 +260,7 @@ export default function ReviewEntriesTable(props: {
Cell: ({ row }: CellProps) => ,
filterFn: ff.filterFnGlosses,
header: t("reviewEntries.columns.glosses"),
- id: "glosses",
+ id: ColumnId.Glosses,
sortingFn: sf.sortingFnGlosses,
}),
@@ -235,9 +277,9 @@ export default function ReviewEntriesTable(props: {
})),
filterVariant: "select",
header: t("reviewEntries.columns.partOfSpeech"),
- id: "partOfSpeech",
+ id: ColumnId.PartOfSpeech,
sortingFn: sf.sortingFnPartOfSpeech,
- visibleInShowHideMenu: showGrammaticalInfo,
+ visibleInShowHideMenu: grammaticalInfoEnabled,
}),
// Domains column
@@ -245,7 +287,7 @@ export default function ReviewEntriesTable(props: {
Cell: ({ row }: CellProps) => ,
filterFn: ff.filterFnDomains,
header: t("reviewEntries.columns.domains"),
- id: "domains",
+ id: ColumnId.Domains,
sortingFn: sf.sortingFnDomains,
}),
@@ -268,7 +310,7 @@ export default function ReviewEntriesTable(props: {
>
),
header: t("reviewEntries.columns.pronunciations"),
- id: "pronunciations",
+ id: ColumnId.Pronunciations,
muiTableHeadCellProps: {
sx: {
"& .Mui-TableHeadCell-Content-Wrapper": {
@@ -286,7 +328,7 @@ export default function ReviewEntriesTable(props: {
columnHelper.accessor((w) => w.note.text || undefined, {
Cell: ({ row }: CellProps) => ,
header: t("reviewEntries.columns.note"),
- id: "note",
+ id: ColumnId.Note,
size: BaselineColumnSize - 40,
}),
@@ -301,6 +343,7 @@ export default function ReviewEntriesTable(props: {
/>
),
header: t("reviewEntries.columns.flag"),
+ id: ColumnId.Flag,
muiTableHeadCellProps: {
sx: {
"& .Mui-TableHeadCell-Content-Wrapper": {
@@ -323,6 +366,7 @@ export default function ReviewEntriesTable(props: {
enableHiding: false,
Header: "",
header: t("reviewEntries.columns.delete"),
+ id: ColumnId.Delete,
size: IconColumnSize,
visibleInShowHideMenu: false,
}),
@@ -341,13 +385,7 @@ export default function ReviewEntriesTable(props: {
enableGlobalFilter: false,
enablePagination,
enableRowVirtualization: !props.disableVirtualization,
- initialState: {
- columnVisibility: {
- definitions: showDefinitions,
- partOfSpeech: showGrammaticalInfo,
- },
- density: "compact",
- },
+ initialState: { density: "compact" },
localization,
muiPaginationProps: { rowsPerPageOptions },
// Override whiteSpace: "nowrap" from having density: "compact"
@@ -357,13 +395,17 @@ export default function ReviewEntriesTable(props: {
sx: { maxHeight: `calc(100vh - ${enablePagination ? 180 : 130}px)` },
},
muiTablePaperProps: { sx: { height: `calc(100vh - ${topBarHeight}px)` } },
+ onColumnOrderChange: (updater) =>
+ dispatch(setReviewEntriesColumnOrder(updater)),
+ onColumnVisibilityChange: (updater) =>
+ dispatch(setReviewEntriesColumnVisibility(updater)),
onPaginationChange: (updater) => {
setPagination(updater);
scrollToTop();
},
rowVirtualizerInstanceRef,
sortDescFirst: false,
- state: { isLoading, pagination },
+ state: { columnOrder, columnVisibility, isLoading, pagination },
});
return ;
diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx
index 179279f474..7c1c6239d3 100644
--- a/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx
+++ b/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx
@@ -5,7 +5,9 @@ import { type ReactTestRenderer, act, create } from "react-test-renderer";
import configureMockStore from "redux-mock-store";
import { defaultState } from "components/Project/ProjectReduxTypes";
-import ReviewEntriesTable from "goals/ReviewEntries/ReviewEntriesTable";
+import ReviewEntriesTable, {
+ ColumnId,
+} from "goals/ReviewEntries/ReviewEntriesTable";
import VernacularCell from "goals/ReviewEntries/ReviewEntriesTable/Cells/VernacularCell";
import {
mockWords,
@@ -38,10 +40,6 @@ jest.mock("backend", () => ({
}));
jest.mock("components/Pronunciations/PronunciationsBackend");
jest.mock("i18n", () => ({}));
-jest.mock("rootRedux/hooks", () => ({
- ...jest.requireActual("rootRedux/hooks"),
- useAppDispatch: () => jest.fn(),
-}));
const mockClickEvent = { stopPropagation: jest.fn() };
const mockGetAllSpeakers = jest.fn();
@@ -153,16 +151,13 @@ describe("ReviewEntriesTable", () => {
});
describe("definitionsEnabled & grammaticalInfoEnabled", () => {
- const definitionsId = "definitions";
- const partOfSpeechId = "partOfSpeech";
-
test("show definitions when definitionsEnabled is true", async () => {
await renderReviewEntriesTable(true, false);
const colIds = renderer.root
.findAllByType(MRT_TableHeadCell)
.map((col) => col.props.header.id);
- expect(colIds).toContain(definitionsId);
- expect(colIds).not.toContain(partOfSpeechId);
+ expect(colIds).toContain(ColumnId.Definitions);
+ expect(colIds).not.toContain(ColumnId.PartOfSpeech);
});
test("show part of speech when grammaticalInfoEnabled is true", async () => {
@@ -170,8 +165,8 @@ describe("ReviewEntriesTable", () => {
const colIds = renderer.root
.findAllByType(MRT_TableHeadCell)
.map((col) => col.props.header.id);
- expect(colIds).not.toContain(definitionsId);
- expect(colIds).toContain(partOfSpeechId);
+ expect(colIds).not.toContain(ColumnId.Definitions);
+ expect(colIds).toContain(ColumnId.PartOfSpeech);
});
});
diff --git a/src/types/runtimeConfig.ts b/src/types/runtimeConfig.ts
index ec17c2b26d..b784b35a00 100644
--- a/src/types/runtimeConfig.ts
+++ b/src/types/runtimeConfig.ts
@@ -17,7 +17,10 @@ declare global {
const defaultConfig: RuntimeConfigItems = {
baseUrl: "http://localhost:5000",
captchaRequired: true,
- captchaSiteKey: "0x4AAAAAAAiMciPlBW1aA1iL",
+ /* https://developers.cloudflare.com/turnstile/troubleshooting/testing/
+ * has dummy site keys for development and testing; options are:
+ * invisible pass, invisible fail, visible pass, visible fail, forced interaction */
+ captchaSiteKey: "1x00000000000000000000AA", // visible pass
offline: false,
emailServicesEnabled: true,
showCertExpiration: true,