diff --git a/app/api/api.js b/app/api/api.js
index 3552e468a7..9857a26925 100644
--- a/app/api/api.js
+++ b/app/api/api.js
@@ -33,6 +33,7 @@ export default (app, server) => {
require('./files/ocrRoutes').ocrRoutes(app);
require('./settings/routes').default(app);
require('./i18n/routes').default(app);
+ require('./i18n.v2/routes').translationsRoutes(app);
require('./sync/routes').default(app);
require('./tasks/routes').default(app);
require('./usergroups/routes').default(app);
diff --git a/app/api/i18n.v2/routes/index.ts b/app/api/i18n.v2/routes/index.ts
new file mode 100644
index 0000000000..1e4a94a547
--- /dev/null
+++ b/app/api/i18n.v2/routes/index.ts
@@ -0,0 +1,54 @@
+import { Application, Request } from 'express';
+import { needsAuthorization } from 'api/auth';
+import { validation } from 'api/utils';
+import translations from 'api/i18n';
+import { getTranslationsEntriesV2 } from 'api/i18n/v2_support';
+
+const translationsRoutes = (app: Application) => {
+ app.get('/api/v2/translations', async (_req: Request, res) => {
+ const translationsV2 = await getTranslationsEntriesV2();
+ const translationList = await translationsV2.all();
+ res.json(translationList);
+ });
+
+ app.post(
+ '/api/v2/translations',
+ needsAuthorization(),
+ validation.validateRequest({
+ type: 'object',
+ properties: {
+ body: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ _id: { type: 'string' },
+ language: { type: 'string' },
+ key: { type: 'string' },
+ value: { type: 'string' },
+ context: {
+ type: 'object',
+ properties: {
+ id: { type: 'string' },
+ label: { type: 'string' },
+ type: { type: 'string' },
+ },
+ required: ['id', 'label', 'type'],
+ },
+ },
+ required: ['language', 'key', 'value', 'context'],
+ },
+ },
+ },
+ required: ['body'],
+ }),
+ async (req, res) => {
+ await translations.v2StructureSave(req.body);
+ req.sockets.emitToCurrentTenant('translationKeysChange', req.body);
+ res.status(200);
+ res.json({ success: true });
+ }
+ );
+};
+
+export { translationsRoutes };
diff --git a/app/api/i18n.v2/routes/specs/routes.spec.ts b/app/api/i18n.v2/routes/specs/routes.spec.ts
new file mode 100644
index 0000000000..dfdc40de32
--- /dev/null
+++ b/app/api/i18n.v2/routes/specs/routes.spec.ts
@@ -0,0 +1,102 @@
+import 'isomorphic-fetch';
+import request from 'supertest';
+
+import { TranslationDBO } from 'api/i18n.v2/schemas/TranslationDBO';
+import { getFixturesFactory } from 'api/utils/fixturesFactory';
+import { testingEnvironment } from 'api/utils/testingEnvironment';
+import { TestEmitSources, iosocket, setUpApp } from 'api/utils/testingRoutes';
+import { UserRole } from 'shared/types/userSchema';
+import { translationsRoutes } from '..';
+
+describe('i18n translations V2 routes', () => {
+ const createTranslationDBO = getFixturesFactory().v2.database.translationDBO;
+ const app = setUpApp(translationsRoutes, (req, _res, next) => {
+ req.user = {
+ username: 'admin',
+ role: UserRole.ADMIN,
+ email: 'admin@test.com',
+ };
+ // @ts-ignore
+ req.file = { path: 'filder/filename.ext' };
+ next();
+ });
+
+ beforeEach(async () => {
+ const translationsV2: TranslationDBO[] = [
+ createTranslationDBO('Search', 'Buscar', 'es', {
+ id: 'System',
+ type: 'Entity',
+ label: 'User Interface',
+ }),
+ createTranslationDBO('Search', 'Search', 'en', {
+ id: 'System',
+ type: 'Uwazi UI',
+ label: 'User Interface',
+ }),
+ ];
+ await testingEnvironment.setUp(
+ {
+ settings: [
+ {
+ languages: [
+ { key: 'en', label: 'English', default: true },
+ { key: 'es', label: 'Spanish', default: false },
+ ],
+ },
+ ],
+ translationsV2,
+ },
+ 'index_i18n_v2_routes'
+ );
+ });
+
+ afterEach(() => {
+ iosocket.emit.mockReset();
+ });
+
+ afterAll(async () => {
+ await testingEnvironment.tearDown();
+ });
+
+ describe('/api/v2/translations', () => {
+ it('should update the translations and emit translationKeysChange event', async () => {
+ const response = await request(app)
+ .post('/api/v2/translations')
+ .send([
+ {
+ language: 'es',
+ key: 'Search',
+ value: 'Búsqueda',
+ context: {
+ id: 'System',
+ label: 'User Interface',
+ type: 'Uwazi UI',
+ },
+ },
+ ]);
+ expect(response.status).toEqual(200);
+ expect(iosocket.emit).toHaveBeenCalledWith(
+ 'translationKeysChange',
+ TestEmitSources.currentTenant,
+ [
+ {
+ context: { id: 'System', label: 'User Interface', type: 'Uwazi UI' },
+ key: 'Search',
+ language: 'es',
+ value: 'Búsqueda',
+ },
+ ]
+ );
+ });
+
+ it('should handle invalid POST request payload', async () => {
+ const response = await request(app)
+ .post('/api/v2/translations')
+ .send({ invalidKey: 'value' }); // Invalid payload
+ expect(response.status).toBe(400);
+ expect(response.body).toEqual(
+ expect.objectContaining({ prettyMessage: 'validation failed' })
+ );
+ });
+ });
+});
diff --git a/app/api/i18n/routes.ts b/app/api/i18n/routes.ts
index 7382258053..b4764f2357 100644
--- a/app/api/i18n/routes.ts
+++ b/app/api/i18n/routes.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
import { createError, validation } from 'api/utils';
import settings from 'api/settings';
import entities from 'api/entities';
@@ -59,6 +60,7 @@ async function deleteLanguage(key: LanguageISO6391, req: Request) {
type TranslationsRequest = Request & { query: { context: string } };
+// eslint-disable-next-line max-statements
export default (app: Application) => {
app.get(
'/api/translations',
diff --git a/app/api/i18n/specs/fixtures.ts b/app/api/i18n/specs/fixtures.ts
index bb0093846c..8b68dee4d4 100644
--- a/app/api/i18n/specs/fixtures.ts
+++ b/app/api/i18n/specs/fixtures.ts
@@ -199,6 +199,13 @@ const fixtures: DBFixture = {
{
_id: entityTemplateId,
type: 'template',
+ properties: [
+ {
+ type: 'select',
+ name: 'Dictionary',
+ content: dictionaryId.toString(),
+ },
+ ],
},
{
_id: documentTemplateId,
@@ -214,6 +221,21 @@ const fixtures: DBFixture = {
published: false,
metadata: {},
},
+ {
+ language: 'es',
+ sharedId: 'entity1',
+ title: '1',
+ template: entityTemplateId,
+ published: false,
+ metadata: {
+ Dictionary: [
+ {
+ value: '1',
+ label: 'Password',
+ },
+ ],
+ },
+ },
],
pages: [
{
diff --git a/app/api/i18n/specs/translations.spec.ts b/app/api/i18n/specs/translations.spec.ts
index 473c3a8beb..44ca0c6864 100644
--- a/app/api/i18n/specs/translations.spec.ts
+++ b/app/api/i18n/specs/translations.spec.ts
@@ -7,11 +7,13 @@ import thesauri from 'api/thesauri/thesauri.js';
import { ContextType } from 'shared/translationSchema';
// eslint-disable-next-line node/no-restricted-import
import * as fs from 'fs';
+import { TranslationSyO } from 'api/i18n.v2/schemas/TranslationSyO';
import { UITranslationNotAvailable } from '../defaultTranslations';
import translations from '../translations';
import fixtures, { dictionaryId } from './fixtures';
import { sortByLocale } from './sortByLocale';
import { addLanguage } from '../routes';
+import { getTranslationsV2ByContext } from '../v2_support';
describe('translations', () => {
beforeEach(async () => {
@@ -29,7 +31,7 @@ describe('translations', () => {
expect(result).toMatchObject({
contexts: [
{
- type: 'Thesaurus',
+ type: 'Thesaurus' as 'Thesaurus',
values: {
Account: 'Account',
Age: 'Age',
@@ -56,6 +58,36 @@ describe('translations', () => {
});
});
+ describe('v2StructureSave', () => {
+ it('should save changed translations and propagate the changes', async () => {
+ const initialTranslations = await getTranslationsV2ByContext(dictionaryId.toString());
+ const initialEntity = (await entities.get({ language: 'es', sharedId: 'entity1' }))[0];
+ const translationsToSave = [
+ {
+ _id: '1',
+ language: initialTranslations[0].locale!,
+ key: 'Password',
+ value: 'Changed Password ES',
+ context: {
+ id: dictionaryId.toString(),
+ type: 'Thesaurus' as TranslationSyO['context']['type'],
+ label: '',
+ },
+ },
+ ];
+
+ await translations.v2StructureSave(translationsToSave);
+ const updatedTranslations = await getTranslationsV2ByContext(dictionaryId.toString());
+ initialTranslations![0]!.contexts![0]!.values!.find(v => v.key === 'Password')!.value =
+ 'Changed Password ES';
+ expect(updatedTranslations).toEqual(initialTranslations);
+
+ const updatedEntity = (await entities.get({ language: 'es', sharedId: 'entity1' }))[0];
+ initialEntity.metadata.Dictionary[0].label = 'Changed Password ES';
+ expect(updatedEntity).toEqual(initialEntity);
+ });
+ });
+
describe('save()', () => {
it('should save the translation and return it', async () => {
const result = await translations.save({ locale: 'fr' });
diff --git a/app/api/i18n/translations.ts b/app/api/i18n/translations.ts
index d77c894553..2d83db209a 100644
--- a/app/api/i18n/translations.ts
+++ b/app/api/i18n/translations.ts
@@ -17,6 +17,7 @@ import { availableLanguages } from 'shared/language';
import { ContextType } from 'shared/translationSchema';
import { LanguageISO6391 } from 'shared/types/commonTypes';
import { pipeline } from 'stream/promises';
+import { TranslationSyO } from 'api/i18n.v2/schemas/TranslationSyO';
import {
addLanguageV2,
deleteTranslationsByContextIdV2,
@@ -25,6 +26,7 @@ import {
getTranslationsV2ByContext,
getTranslationsV2ByLanguage,
updateContextV2,
+ upsertTranslationEntries,
upsertTranslationsV2,
} from './v2_support';
@@ -105,6 +107,47 @@ function processContextValues(context: TranslationContext | IndexedContext): Tra
return { ...context, values };
}
+const propagateTranslationInMetadata = async (
+ translation: TranslationType,
+ context: TranslationContext
+) => {
+ const isPresentInTheComingData = (translation.contexts || []).find(
+ _context => _context.id?.toString() === context.id?.toString()
+ );
+
+ if (isPresentInTheComingData && isPresentInTheComingData.type === 'Thesaurus') {
+ const thesaurus = await thesauri.getById(context.id);
+
+ const valuesChanged: IndexedContextValues = (isPresentInTheComingData.values || []).reduce(
+ (changes, value) => {
+ const currentValue = (context.values || []).find(v => v.key === value.key);
+ if (currentValue?.key && currentValue.value !== value.value) {
+ return { ...changes, [currentValue.key]: value.value } as IndexedContextValues;
+ }
+ return changes;
+ },
+ {} as IndexedContextValues
+ );
+
+ const changesMatchingDictionaryId = Object.keys(valuesChanged)
+ .map(valueChanged => {
+ const valueFound = (thesaurus?.values || []).find(v => v.label === valueChanged);
+ if (valueFound?.id) {
+ return { id: valueFound.id, value: valuesChanged[valueChanged] };
+ }
+ return null;
+ })
+ .filter(a => a) as { id: string; value: string }[];
+
+ return Promise.all(
+ changesMatchingDictionaryId.map(async change =>
+ thesauri.renameThesaurusInMetadata(change.id, change.value, context.id, translation.locale)
+ )
+ );
+ }
+ return Promise.resolve([]);
+};
+
const propagateTranslation = async (
translation: TranslationType,
currentTranslationData: WithId
@@ -112,47 +155,7 @@ const propagateTranslation = async (
await (currentTranslationData.contexts || ([] as TranslationContext[])).reduce(
async (promise: Promise, context) => {
await promise;
-
- const isPresentInTheComingData = (translation.contexts || []).find(
- _context => _context.id?.toString() === context.id?.toString()
- );
-
- if (isPresentInTheComingData && isPresentInTheComingData.type === 'Thesaurus') {
- const thesaurus = await thesauri.getById(context.id);
-
- const valuesChanged: IndexedContextValues = (isPresentInTheComingData.values || []).reduce(
- (changes, value) => {
- const currentValue = (context.values || []).find(v => v.key === value.key);
- if (currentValue?.key && currentValue.value !== value.value) {
- return { ...changes, [currentValue.key]: value.value } as IndexedContextValues;
- }
- return changes;
- },
- {} as IndexedContextValues
- );
-
- const changesMatchingDictionaryId = Object.keys(valuesChanged)
- .map(valueChanged => {
- const valueFound = (thesaurus?.values || []).find(v => v.label === valueChanged);
- if (valueFound?.id) {
- return { id: valueFound.id, value: valuesChanged[valueChanged] };
- }
- return null;
- })
- .filter(a => a) as { id: string; value: string }[];
-
- return Promise.all(
- changesMatchingDictionaryId.map(async change =>
- thesauri.renameThesaurusInMetadata(
- change.id,
- change.value,
- context.id,
- translation.locale
- )
- )
- );
- }
- return Promise.resolve([]);
+ return propagateTranslationInMetadata(translation, context);
},
Promise.resolve([])
);
@@ -209,6 +212,24 @@ export default {
return translationToSave;
},
+ async v2StructureSave(translationsToSave: TranslationSyO[]) {
+ const { context } = translationsToSave[0];
+ const currentTranslations = await getTranslationsV2ByContext(context.id);
+ await upsertTranslationEntries(translationsToSave);
+ const thesaurusTranslations = currentTranslations[0].contexts?.[0].type === 'Thesaurus';
+ if (thesaurusTranslations) {
+ const updatedTranslations = await getTranslationsV2ByContext(context.id);
+ await Promise.all(
+ updatedTranslations.map(async translation => {
+ const originalContexts = currentTranslations.find(
+ t => t.locale === translation.locale
+ )?.contexts;
+ return propagateTranslationInMetadata(translation, (originalContexts || [context])[0]);
+ })
+ );
+ }
+ },
+
async updateEntries(
contextId: string,
keyValuePairsPerLanguage: {
diff --git a/app/api/i18n/v2_support.ts b/app/api/i18n/v2_support.ts
index a2c4161d2b..5c51c2fbf5 100644
--- a/app/api/i18n/v2_support.ts
+++ b/app/api/i18n/v2_support.ts
@@ -110,7 +110,7 @@ export const createTranslationsV2 = async (translation: TranslationType) => {
).create(flattenTranslations(translation));
};
-export const upsertTranslationsV2 = async (translations: TranslationType[]) => {
+export const upsertTranslationEntries = async (translations: CreateTranslationsData[]) => {
const transactionManager = DefaultTransactionManager();
await new UpsertTranslationsService(
DefaultTranslationsDataSource(transactionManager),
@@ -120,12 +120,15 @@ export const upsertTranslationsV2 = async (translations: TranslationType[]) => {
DefaultSettingsDataSource(transactionManager)
),
transactionManager
- ).upsert(
- translations.reduce(
- (flattened, t) => flattened.concat(flattenTranslations(t)),
- []
- )
+ ).upsert(translations);
+};
+
+export const upsertTranslationsV2 = async (translations: TranslationType[]) => {
+ const translationsToUpsert = translations.reduce(
+ (flattened, t) => flattened.concat(flattenTranslations(t)),
+ []
);
+ return upsertTranslationEntries(translationsToUpsert);
};
export const deleteTranslationsByContextIdV2 = async (contextId: string) => {
@@ -159,10 +162,11 @@ export const getTranslationsV2ByLanguage = async (language: LanguageISO6391) =>
language
);
+export const getTranslationsEntriesV2 = async () =>
+ new GetTranslationsService(DefaultTranslationsDataSource(DefaultTransactionManager())).getAll();
+
export const getTranslationsV2 = async () =>
- resultsToV1TranslationType(
- new GetTranslationsService(DefaultTranslationsDataSource(DefaultTransactionManager())).getAll()
- );
+ resultsToV1TranslationType(await getTranslationsEntriesV2());
export const updateContextV2 = async (
context: CreateTranslationsData['context'],
diff --git a/app/react/App/App.js b/app/react/App/App.js
index 05d22199c6..12979af0db 100644
--- a/app/react/App/App.js
+++ b/app/react/App/App.js
@@ -5,12 +5,13 @@ import { Outlet, useLocation, useParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import Notifications from 'app/Notifications';
import Cookiepopup from 'app/App/Cookiepopup';
-import { TranslateForm, t } from 'app/I18N';
import { Icon } from 'UI';
import { socket } from 'app/socket';
import { NotificationsContainer } from 'V2/Components/UI';
import { Matomo, CleanInsights } from 'app/V2/Components/Analitycs';
import { settingsAtom } from 'V2/atoms/settingsAtom';
+import { TranslateModal, t } from 'app/I18N';
+import { inlineEditAtom } from 'V2/atoms';
import Confirm from './Confirm';
import { Menu } from './Menu';
import { AppMainContext } from './AppMainContext';
@@ -26,6 +27,7 @@ import 'flowbite';
const App = ({ customParams }) => {
const [showMenu, setShowMenu] = useState(false);
+ const [inlineEditState] = useAtom(inlineEditAtom);
const [confirmOptions, setConfirmOptions] = useState({});
const [settings, setSettings] = useAtom(settingsAtom);
@@ -92,7 +94,6 @@ const App = ({ customParams }) => {
-
@@ -101,6 +102,7 @@ const App = ({ customParams }) => {
+ {inlineEditState.inlineEdit && inlineEditState.context && }
);
};
diff --git a/app/react/App/DropdownMenu.tsx b/app/react/App/DropdownMenu.tsx
index 1a0fb4f7bb..7b71a7703d 100644
--- a/app/react/App/DropdownMenu.tsx
+++ b/app/react/App/DropdownMenu.tsx
@@ -1,7 +1,7 @@
+import { useOnClickOutsideElement } from 'app/utils/useOnClickOutsideElementHook';
import { I18NLink, Translate } from 'app/I18N';
import { Icon } from 'UI';
import React, { useRef, useState, useCallback } from 'react';
-import { useOnClickOutsideElement } from 'app/utils/useOnClickOutsideElementHook';
import { ILink, ISublink } from 'app/V2/shared/types';
import { IImmutable } from 'shared/types/Immutable';
diff --git a/app/react/App/SearchTipsContent.tsx b/app/react/App/SearchTipsContent.tsx
index 3e9ec3e3a8..5b6ee330bd 100644
--- a/app/react/App/SearchTipsContent.tsx
+++ b/app/react/App/SearchTipsContent.tsx
@@ -1,55 +1,53 @@
import React from 'react';
import { t } from 'app/I18N';
-const SearchTipsContent = () => {
- return (
-
-
- {t(
- 'System',
- 'Search Tips: wildcard',
- 'Use an * for wildcard search. Ie: "juris*" will match words ' +
- 'such as jurisdiction, jurisdictional, jurists, jurisprudence, etc.',
- false
- )}
-
-
- {t(
- 'System',
- 'Search Tips: one char wildcard',
- '? for one character wildcard. Ie: "198?" will match 1980 to 1989 and also 198a, 198b, etc.',
- false
- )}
-
-
- {t(
- 'System',
- 'Search Tips: exact term',
- 'Exact term match by enclosing your search string with quotes. Ie. "Costa Rica"' +
- ' will toss different results compared to Costa Rica without quotes.',
- false
- )}
-
-
- {t(
- 'System',
- 'Search Tips: proximity',
- '~ for proximity searches. Ie: "the status"~5 will find anything having "the" and' +
- '"status" within a distance of 5 words, such as "the procedural status", "the specific legal status".',
- false
- )}
-
-
- {t(
- 'System',
- 'Search Tips: boolean',
- 'AND, OR and NOT for boolean searches. Ie. "status AND women NOT Nicaragua" will match anything ' +
- 'containing both the words status and women, and necessarily not containing the word Nicaragua.',
- false
- )}
-
-
- );
-};
+const SearchTipsContent = () => (
+
+
+ {t(
+ 'System',
+ 'Search Tips: wildcard',
+ 'Use an * for wildcard search. Ie: "juris*" will match words ' +
+ 'such as jurisdiction, jurisdictional, jurists, jurisprudence, etc.',
+ false
+ )}
+
+
+ {t(
+ 'System',
+ 'Search Tips: one char wildcard',
+ '? for one character wildcard. Ie: "198?" will match 1980 to 1989 and also 198a, 198b, etc.',
+ false
+ )}
+
+
+ {t(
+ 'System',
+ 'Search Tips: exact term',
+ 'Exact term match by enclosing your search string with quotes. Ie. "Costa Rica"' +
+ ' will toss different results compared to Costa Rica without quotes.',
+ false
+ )}
+
+
+ {t(
+ 'System',
+ 'Search Tips: proximity',
+ '~ for proximity searches. Ie: "the status"~5 will find anything having "the" and' +
+ '"status" within a distance of 5 words, such as "the procedural status", "the specific legal status".',
+ false
+ )}
+
+
+ {t(
+ 'System',
+ 'Search Tips: boolean',
+ 'AND, OR and NOT for boolean searches. Ie. "status AND women NOT Nicaragua" will match anything ' +
+ 'containing both the words status and women, and necessarily not containing the word Nicaragua.',
+ false
+ )}
+
+
+);
export { SearchTipsContent };
diff --git a/app/react/App/scss/layout/_header.scss b/app/react/App/scss/layout/_header.scss
index ca0943a5e3..892e0c559c 100644
--- a/app/react/App/scss/layout/_header.scss
+++ b/app/react/App/scss/layout/_header.scss
@@ -1,4 +1,4 @@
-@use "sass:color";
+@use 'sass:color';
@import '../config/colors';
@import '../elements/tooltip';
@@ -254,14 +254,6 @@ header {
}
}
- .live-on {
- color: #88eacd;
- }
-
- .live-off {
- color: #ffe66b;
- }
-
button.singleItem {
padding-left: 1em;
}
diff --git a/app/react/App/sockets.js b/app/react/App/sockets.js
index c1785eb0c2..af401ebbe7 100644
--- a/app/react/App/sockets.js
+++ b/app/react/App/sockets.js
@@ -1,7 +1,8 @@
import { actions } from 'app/BasicReducer';
-import { t, Translate } from 'app/I18N';
+import { t } from 'app/I18N';
import { notificationActions } from 'app/Notifications';
import { documentProcessed } from 'app/Uploads/actions/uploadsActions';
+import { atomStore, translationsAtom } from 'V2/atoms';
import { store } from '../store';
import { socket, reconnectSocket } from '../socket';
@@ -58,10 +59,28 @@ socket.on('thesauriDelete', thesauri => {
store.dispatch(actions.remove('thesauris', { _id: thesauri.id }));
});
-socket.on('translationsChange', translations => {
- store.dispatch(actions.update('translations', translations, 'locale'));
- t.resetCachedTranslation();
- Translate.resetCachedTranslation();
+socket.on('translationsChange', languageTranslations => {
+ const translations = atomStore.get(translationsAtom);
+ const modifiedLanguage = translations.find(
+ translation => translation.locale === languageTranslations.locale
+ );
+ if (modifiedLanguage) {
+ modifiedLanguage.contexts = languageTranslations.contexts;
+ } else {
+ translations.push(languageTranslations);
+ }
+ atomStore.set(translationsAtom, translations);
+});
+
+socket.on('translationKeysChange', translationsEntries => {
+ const translations = atomStore.get(translationsAtom);
+ translationsEntries.forEach(item => {
+ const modifiedContext = translations
+ .find(translation => translation.locale === item.language)
+ .contexts.find(c => c.id && c.id === item.context.id);
+ modifiedContext.values[item.key] = item.value;
+ });
+ atomStore.set(translationsAtom, translations);
});
socket.on('translationsInstallDone', () => {
@@ -88,9 +107,9 @@ socket.on('translationsInstallError', errorMessage => {
});
socket.on('translationsDelete', locale => {
- store.dispatch(actions.remove('translations', { locale }, 'locale'));
- t.resetCachedTranslation();
- Translate.resetCachedTranslation();
+ const translations = atomStore.get(translationsAtom);
+ const updatedTranslations = translations.filter(language => language.locale !== locale);
+ atomStore.set(translationsAtom, updatedTranslations);
});
socket.on('translationsDeleteDone', () => {
diff --git a/app/react/App/specs/__snapshots__/Confirm.spec.js.snap b/app/react/App/specs/__snapshots__/Confirm.spec.js.snap
index c850ab82e4..8985807101 100644
--- a/app/react/App/specs/__snapshots__/Confirm.spec.js.snap
+++ b/app/react/App/specs/__snapshots__/Confirm.spec.js.snap
@@ -9,21 +9,21 @@ exports[`CantDeleteTemplateAlert extraConfirm option should render a confirm inp
>
-
+
Confirm action
-
+
-
Are you sure you want to continue?
-
+
-
+
If you want to continue, please type
-
+
'
CONFIRM
'
@@ -41,9 +41,9 @@ exports[`CantDeleteTemplateAlert extraConfirm option should render a confirm inp
onClick={[Function]}
type="button"
>
-
+
Cancel
-
+
-
+
Accept
-
+
@@ -68,16 +68,16 @@ exports[`CantDeleteTemplateAlert noCancel option should hide the cancel button 1
>
-
+
Confirm action
-
+
-
Are you sure you want to continue?
-
+
@@ -87,9 +87,9 @@ exports[`CantDeleteTemplateAlert noCancel option should hide the cancel button 1
onClick={[Function]}
type="button"
>
-
+
Accept
-
+
diff --git a/app/react/App/specs/__snapshots__/Cookiepopup.spec.js.snap b/app/react/App/specs/__snapshots__/Cookiepopup.spec.js.snap
index 867950e1ec..29a3c72095 100644
--- a/app/react/App/specs/__snapshots__/Cookiepopup.spec.js.snap
+++ b/app/react/App/specs/__snapshots__/Cookiepopup.spec.js.snap
@@ -13,9 +13,9 @@ exports[`Cookiepopup when the cookiepolicy is active and the cookie not exists s
+
To bring you a better experience, this site uses cookies.
-
+
}
removeNotification={[Function]}
type="success"
diff --git a/app/react/App/specs/fixtures/fixtures.ts b/app/react/App/specs/fixtures/fixtures.ts
new file mode 100644
index 0000000000..3205bc1a23
--- /dev/null
+++ b/app/react/App/specs/fixtures/fixtures.ts
@@ -0,0 +1,155 @@
+import { ClientTranslationSchema } from 'app/istore';
+
+const currentTranslations: ClientTranslationSchema[] = [
+ {
+ locale: 'en',
+ contexts: [
+ {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ values: {
+ Documents: 'Document',
+ Select: 'Select',
+ Title: 'Title',
+ },
+ },
+ ],
+ },
+ {
+ locale: 'es',
+ contexts: [
+ {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ values: {
+ Documents: 'Documento',
+ Select: 'Selector',
+ Title: 'Título',
+ },
+ },
+ ],
+ },
+];
+
+const updatedTranslation: ClientTranslationSchema = {
+ locale: 'en',
+ contexts: [
+ {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ values: {
+ Documents: 'Document',
+ Select: 'Select',
+ Title: 'Update title',
+ },
+ },
+ ],
+};
+
+const newLanguage: ClientTranslationSchema = {
+ locale: 'fr',
+ contexts: [
+ {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ values: {
+ Documents: 'Document',
+ Select: 'Select',
+ Title: 'Title',
+ },
+ },
+ ],
+};
+
+const translationKeysChangeArguments = [
+ {
+ language: 'en',
+ value: 'Select',
+ key: 'Select',
+ context: {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ },
+ },
+ {
+ language: 'es',
+ value: 'Select ES',
+ key: 'Select',
+ context: {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ },
+ },
+ {
+ language: 'fr',
+ value: 'Select FR',
+ key: 'Select',
+ context: {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ },
+ },
+];
+
+const translationKeysChangeResult = [
+ {
+ locale: 'en',
+ contexts: [
+ {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ values: {
+ Documents: 'Document',
+ Select: 'Select',
+ Title: 'Title',
+ },
+ },
+ ],
+ },
+ {
+ locale: 'es',
+ contexts: [
+ {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ values: {
+ Documents: 'Documento',
+ Select: 'Select ES',
+ Title: 'Título',
+ },
+ },
+ ],
+ },
+ {
+ locale: 'fr',
+ contexts: [
+ {
+ id: 'id1',
+ label: 'Documents',
+ type: 'Entity',
+ values: {
+ Documents: 'Document',
+ Select: 'Select FR',
+ Title: 'Title',
+ },
+ },
+ ],
+ },
+];
+
+export {
+ updatedTranslation,
+ currentTranslations,
+ newLanguage,
+ translationKeysChangeResult,
+ translationKeysChangeArguments,
+};
diff --git a/app/react/App/specs/sockets.spec.js b/app/react/App/specs/sockets.spec.js
index a68978f88d..54e36e36a0 100644
--- a/app/react/App/specs/sockets.spec.js
+++ b/app/react/App/specs/sockets.spec.js
@@ -1,10 +1,19 @@
/**
* @jest-environment jsdom
*/
+/* eslint-disable max-statements */
import * as uploadActions from 'app/Uploads/actions/uploadsActions';
+import { atomStore, translationsAtom } from 'V2/atoms';
import { socket } from '../../socket';
import '../sockets';
import { store } from '../../store';
+import {
+ currentTranslations,
+ newLanguage,
+ updatedTranslation,
+ translationKeysChangeArguments,
+ translationKeysChangeResult,
+} from './fixtures/fixtures';
describe('sockets', () => {
beforeEach(() => {
@@ -113,13 +122,42 @@ describe('sockets', () => {
});
describe('translationsChange', () => {
+ beforeEach(() => {
+ atomStore.set(
+ translationsAtom,
+ currentTranslations.map(t => ({ ...t }))
+ );
+ spyOn(atomStore, 'set');
+ });
+
it('should emit a translationsChange event', () => {
- socket._callbacks.$translationsChange[0]({ id: '123' });
- expect(store.dispatch).toHaveBeenCalledWith({
- customIndex: 'locale',
- type: 'translations/UPDATE',
- value: { id: '123' },
- });
+ socket._callbacks.$translationsChange[0](updatedTranslation);
+ expect(atomStore.set).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.arrayContaining([updatedTranslation, currentTranslations[1]])
+ );
+ });
+
+ it('should add a new language to the translations', () => {
+ socket._callbacks.$translationsChange[0](newLanguage);
+ expect(atomStore.set).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.arrayContaining([...currentTranslations, newLanguage])
+ );
+ });
+ });
+
+ describe('translationKeysChange', () => {
+ const initialTranslations = [...currentTranslations.map(t => ({ ...t })), newLanguage];
+
+ beforeEach(() => {
+ atomStore.set(translationsAtom, initialTranslations);
+ spyOn(atomStore, 'set');
+ });
+
+ it('should emit a translationKeysChange event', () => {
+ socket._callbacks.$translationKeysChange[0](translationKeysChangeArguments);
+ expect(atomStore.set).toHaveBeenCalledWith(expect.any(Object), translationKeysChangeResult);
});
});
@@ -152,13 +190,17 @@ describe('sockets', () => {
});
describe('translationsDelete', () => {
+ beforeEach(() => {
+ atomStore.set(
+ translationsAtom,
+ currentTranslations.map(t => ({ ...t }))
+ );
+ spyOn(atomStore, 'set');
+ });
+
it('should emit a translationsDelete event', () => {
- socket._callbacks.$translationsDelete[0]('localeString');
- expect(store.dispatch).toHaveBeenCalledWith({
- customIndex: 'locale',
- type: 'translations/REMOVE',
- value: { locale: 'localeString' },
- });
+ socket._callbacks.$translationsDelete[0]('es');
+ expect(atomStore.set).toHaveBeenCalledWith(expect.any(Object), [currentTranslations[0]]);
});
});
diff --git a/app/react/Attachments/components/AttachmentForm.js b/app/react/Attachments/components/AttachmentForm.js
index 9b38b393cd..caeff3d6eb 100644
--- a/app/react/Attachments/components/AttachmentForm.js
+++ b/app/react/Attachments/components/AttachmentForm.js
@@ -5,7 +5,7 @@ import { Form, Field } from 'react-redux-form';
import { FormGroup, Select } from 'app/ReactReduxForms';
import { elasticLanguages } from 'shared/language';
-import t from 'app/I18N/t';
+import { t } from 'app/I18N';
import ShowIf from 'app/App/ShowIf';
export class AttachmentForm extends Component {
diff --git a/app/react/Attachments/components/UploadAttachment.js b/app/react/Attachments/components/UploadAttachment.js
index 7b033a6bab..c6a0648dd0 100644
--- a/app/react/Attachments/components/UploadAttachment.js
+++ b/app/react/Attachments/components/UploadAttachment.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
-import t from 'app/I18N/t';
+import { t } from 'app/I18N';
import { Icon } from 'UI';
import { uploadAttachment } from '../actions/actions';
diff --git a/app/react/Attachments/components/specs/__snapshots__/AttachmentsList.spec.js.snap b/app/react/Attachments/components/specs/__snapshots__/AttachmentsList.spec.js.snap
index 53f6e31efe..feaa909232 100644
--- a/app/react/Attachments/components/specs/__snapshots__/AttachmentsList.spec.js.snap
+++ b/app/react/Attachments/components/specs/__snapshots__/AttachmentsList.spec.js.snap
@@ -23,9 +23,9 @@ exports[`AttachmentsList When parent is Target Document should treat all Attachm
}
>
-
+
Supporting files
-
+
@@ -58,9 +58,9 @@ exports[`AttachmentsList should render a sorted list of attachments (files) 1`]
}
>
-
+
Supporting files
-
+
-
+
Supporting files
-
+
-
+
Supporting files
-
+
-
+
Supporting files
-
+
diff --git a/app/react/Attachments/components/specs/__snapshots__/AttachmentsModal.spec.tsx.snap b/app/react/Attachments/components/specs/__snapshots__/AttachmentsModal.spec.tsx.snap
index c009fe2f1a..1f6d43f296 100644
--- a/app/react/Attachments/components/specs/__snapshots__/AttachmentsModal.spec.tsx.snap
+++ b/app/react/Attachments/components/specs/__snapshots__/AttachmentsModal.spec.tsx.snap
@@ -12,7 +12,7 @@ exports[`Attachments Modal Attachment from web Should match render of web form 1
>
Supporting files
@@ -38,7 +38,7 @@ exports[`Attachments Modal Attachment from web Should match render of web form 1
Cancel
@@ -62,7 +62,7 @@ exports[`Attachments Modal Attachment from web Should match render of web form 1
style=""
>
Upload from computer
@@ -77,7 +77,7 @@ exports[`Attachments Modal Attachment from web Should match render of web form 1
style="font-weight: bold;"
>
Add from web
@@ -155,7 +155,7 @@ exports[`Attachments Modal Attachment from web Should match render of web form 1
Add from URL
@@ -181,7 +181,7 @@ exports[`Attachments Modal Should match render of upload form 1`] = `
>
Supporting files
@@ -207,7 +207,7 @@ exports[`Attachments Modal Should match render of upload form 1`] = `
Cancel
@@ -231,7 +231,7 @@ exports[`Attachments Modal Should match render of upload form 1`] = `
style="font-weight: bold;"
>
Upload from computer
@@ -245,7 +245,7 @@ exports[`Attachments Modal Should match render of upload form 1`] = `
role="tab"
>
Add from web
@@ -286,7 +286,7 @@ exports[`Attachments Modal Should match render of upload form 1`] = `
Upload and select file
@@ -300,7 +300,7 @@ exports[`Attachments Modal Should match render of upload form 1`] = `
class="attachments-modal__dropzone-title"
>
Drag and drop file in this window to upload
diff --git a/app/react/Attachments/components/specs/__snapshots__/WebMediaResourceForm.spec.tsx.snap b/app/react/Attachments/components/specs/__snapshots__/WebMediaResourceForm.spec.tsx.snap
index 7a728abf7e..e181110ade 100644
--- a/app/react/Attachments/components/specs/__snapshots__/WebMediaResourceForm.spec.tsx.snap
+++ b/app/react/Attachments/components/specs/__snapshots__/WebMediaResourceForm.spec.tsx.snap
@@ -40,9 +40,9 @@ exports[`Should match render of wem media form 1`] = `
icon="link"
/>
-
+
Add from URL
-
+
`;
@@ -105,9 +105,9 @@ exports[`should also display a name field when hasName is true 1`] = `
icon="link"
/>
-
+
Add from URL
-
+
`;
diff --git a/app/react/ConnectionsList/components/specs/ConnectionsGroup.spec.js b/app/react/ConnectionsList/components/specs/ConnectionsGroup.spec.js
index 63527f9028..5cb30aefeb 100644
--- a/app/react/ConnectionsList/components/specs/ConnectionsGroup.spec.js
+++ b/app/react/ConnectionsList/components/specs/ConnectionsGroup.spec.js
@@ -1,9 +1,7 @@
-import { fromJS as Immutable } from 'immutable';
import React from 'react';
-
+import { fromJS as Immutable } from 'immutable';
import { shallow } from 'enzyme';
import ShowIf from 'app/App/ShowIf';
-
import { ConnectionsGroup } from '../ConnectionsGroup';
describe('ConnectionsGroup', () => {
@@ -58,12 +56,16 @@ describe('ConnectionsGroup', () => {
expect(subItem1.props().title).toBe('template 1');
expect(subItem1.find('input').props().checked).toBe(false);
- expect(subItem1.find('.multiselectItem-name').text()).toBe('template 1');
+ expect(subItem1.find('.multiselectItem-name').children().children().text()).toBe(
+ 'template 1'
+ );
expect(subItem1.find('.multiselectItem-results').text()).toBe('1');
expect(subItem2.props().title).toBe('template 2');
expect(subItem2.find('input').props().checked).toBe(false);
- expect(subItem2.find('.multiselectItem-name').text()).toBe('template 2');
+ expect(subItem2.find('.multiselectItem-name').children().children().text()).toBe(
+ 'template 2'
+ );
expect(subItem2.find('.multiselectItem-results').text()).toBe('2');
});
diff --git a/app/react/Documents/components/specs/__snapshots__/DocumentContentSnippets.spec.js.snap b/app/react/Documents/components/specs/__snapshots__/DocumentContentSnippets.spec.js.snap
index d78695d607..b956b0df0b 100644
--- a/app/react/Documents/components/specs/__snapshots__/DocumentContentSnippets.spec.js.snap
+++ b/app/react/Documents/components/specs/__snapshots__/DocumentContentSnippets.spec.js.snap
@@ -5,7 +5,11 @@ exports[`SnippetList should render all document snippets 1`] = `
- Document contents
+
+ Document contents
+
-
+
Edit
-
+
`;
diff --git a/app/react/Documents/components/specs/__snapshots__/MetadataFieldSnippets.spec.js.snap b/app/react/Documents/components/specs/__snapshots__/MetadataFieldSnippets.spec.js.snap
index 618af0de66..43f296cb90 100644
--- a/app/react/Documents/components/specs/__snapshots__/MetadataFieldSnippets.spec.js.snap
+++ b/app/react/Documents/components/specs/__snapshots__/MetadataFieldSnippets.spec.js.snap
@@ -8,7 +8,11 @@ exports[`SnippetList should properly render title snippets with Title label as h
- Summary
+
+ Summary
+
- Summary
+
+ Summary
+
- Search text
+
+ Search text
+
- Search text description
+
+ Search text description
+
`;
@@ -24,10 +32,18 @@ exports[`SearchText blankState when there is search term should render a no matc
icon="search"
/>
- No text match
+
+ No text match
+
- No text match description
+
+ No text match description
+
`;
diff --git a/app/react/Forms/components/specs/Switcher.spec.tsx b/app/react/Forms/components/specs/Switcher.spec.tsx
index 8d5906823a..1a09c57a4d 100644
--- a/app/react/Forms/components/specs/Switcher.spec.tsx
+++ b/app/react/Forms/components/specs/Switcher.spec.tsx
@@ -40,7 +40,7 @@ describe('Switcher', () => {
it('should receive alternative elements for values', () => {
render({ leftLabel: ALL , rightLabel: NONE });
- const labels = component.find('Connect(Translate)');
+ const labels = component.find('Translate');
expect(labels.at(0).props().children).toEqual('ALL');
expect(labels.at(1).props().children).toEqual('NONE');
});
@@ -54,7 +54,7 @@ describe('Switcher', () => {
it('should render default labels AND/OR', () => {
render();
- const labels = component.find('Connect(Translate)');
+ const labels = component.find('Translate');
expect(labels.at(0).props().children).toEqual('AND');
expect(labels.at(1).props().children).toEqual('OR');
});
diff --git a/app/react/Forms/components/specs/__snapshots__/DateRange.spec.js.snap b/app/react/Forms/components/specs/__snapshots__/DateRange.spec.js.snap
index 224a91aca5..cb1d26c690 100644
--- a/app/react/Forms/components/specs/__snapshots__/DateRange.spec.js.snap
+++ b/app/react/Forms/components/specs/__snapshots__/DateRange.spec.js.snap
@@ -8,11 +8,11 @@ exports[`DateRange should allow using the local timezone 1`] = `
-
From:
-
+
-
To:
-
+
-
From:
-
+
-
To:
-
+
- No options found
+
+ No options found
+
-5
-
+
x more
-
+
@@ -526,9 +530,9 @@ exports[`MultiSelect should not render an empty group 1`] = `
-2
-
+
x more
-
+
diff --git a/app/react/I18N/I18NLinkV2.tsx b/app/react/I18N/I18NLinkV2.tsx
new file mode 100644
index 0000000000..a418caa87f
--- /dev/null
+++ b/app/react/I18N/I18NLinkV2.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { useAtomValue } from 'jotai';
+import { NavLinkProps, NavLink } from 'react-router-dom';
+import { localeAtom } from 'V2/atoms';
+
+type I18NLinkProps = NavLinkProps & { to: string; activeClassname?: string };
+
+const I18NLink = (props: I18NLinkProps) => {
+ const { to: link, className, activeClassname, ...rest } = props;
+ const locale = useAtomValue(localeAtom);
+ const parsedLink = link.startsWith('/') ? link.slice(1) : link;
+ const to = locale ? `/${locale}/${parsedLink}` : `/${parsedLink}`;
+
+ return (
+ `${className || ''} ${isActive ? activeClassname : ''}`}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...rest}
+ />
+ );
+};
+
+export { I18NLink };
diff --git a/app/react/I18N/Translate.tsx b/app/react/I18N/Translate.tsx
new file mode 100644
index 0000000000..dca43aba2f
--- /dev/null
+++ b/app/react/I18N/Translate.tsx
@@ -0,0 +1,82 @@
+/* eslint-disable max-statements */
+import React, { Fragment, ReactNode } from 'react';
+import { useAtom, useAtomValue } from 'jotai';
+import { translationsAtom, inlineEditAtom, localeAtom } from 'V2/atoms';
+
+const parseMarkdownMarker = (
+ line: string,
+ regexp: RegExp,
+ wrapper: (text: string) => ReactNode
+) => {
+ const matches = line.match(regexp);
+ if (matches == null) {
+ return matches;
+ }
+ const parts = matches.input?.split(matches[0]);
+ return (
+ <>
+ {parts?.length && parts[0]}
+ {wrapper(matches[1])}
+ {parts?.length && parts[1]}
+ >
+ );
+};
+
+const parseMarkdownBoldMarker = (line: string) =>
+ parseMarkdownMarker(line, /\*{2}(.*)\*{2}/, text => {text} );
+
+const parseMarkdownItalicMarker = (line: string) =>
+ parseMarkdownMarker(line, /\*(.*)\*/, text => {text} );
+
+type TranslateProps = {
+ className?: string;
+ children?: string;
+ context?: string;
+ translationKey?: string;
+};
+
+const Translate = ({ className, children, context = 'System', translationKey }: TranslateProps) => {
+ const translations = useAtomValue(translationsAtom);
+ const locale = useAtomValue(localeAtom);
+ const [inlineEditState, setInlineEditState] = useAtom(inlineEditAtom);
+
+ const language = translations.find(translation => translation.locale === locale);
+ const activeClassName = inlineEditState.inlineEdit ? 'translation active' : 'translation';
+
+ const translationContext = language?.contexts.find(ctx => ctx.id === context) || { values: {} };
+ const text = translationContext.values[(translationKey || children)!] || children;
+ const lines = text ? text.split('\n') : [];
+
+ return (
+ {
+ if (inlineEditState.inlineEdit) {
+ event.stopPropagation();
+ event.preventDefault();
+ setInlineEditState({
+ inlineEdit: inlineEditState.inlineEdit,
+ context,
+ translationKey: (translationKey || children)!,
+ });
+ }
+ }}
+ className={`${activeClassName} ${className || ''}`}
+ >
+ {lines.map((line, index) => {
+ const boldMatches = parseMarkdownBoldMarker(line);
+ const italicMatches = parseMarkdownItalicMarker(line);
+ return (
+
+ {boldMatches ||
+ italicMatches || ( // eslint-disable-next-line react/jsx-no-useless-fragment
+ <>{line}>
+ )}
+ {index < lines.length - 1 && }
+
+ );
+ })}
+
+ );
+};
+
+export { Translate };
diff --git a/app/react/I18N/TranslateModal.tsx b/app/react/I18N/TranslateModal.tsx
new file mode 100644
index 0000000000..336bfa78f3
--- /dev/null
+++ b/app/react/I18N/TranslateModal.tsx
@@ -0,0 +1,131 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import React from 'react';
+import { useAtom, useAtomValue, useSetAtom } from 'jotai';
+import { useFieldArray, useForm } from 'react-hook-form';
+import { FetchResponseError } from 'shared/JSONRequest';
+import { Modal } from 'V2/Components/UI';
+import { settingsAtom, translationsAtom, inlineEditAtom, notificationAtom } from 'V2/atoms';
+import { InputField } from 'app/V2/Components/Forms';
+import { Button } from 'V2/Components/UI/Button';
+import { TranslationValue } from 'V2/shared/types';
+import { postV2 } from 'V2/api/translations';
+import { t } from './translateFunction';
+
+const TranslateModal = () => {
+ const [inlineEditState, setInlineEditState] = useAtom(inlineEditAtom);
+ const [translations] = useAtom(translationsAtom);
+ const setNotifications = useSetAtom(notificationAtom);
+ const context = translations[0].contexts.find(ctx => ctx.id === inlineEditState.context)!;
+ const { languages = [] } = useAtomValue(settingsAtom);
+
+ const {
+ register,
+ handleSubmit,
+ control,
+ reset,
+ formState: { errors, isDirty, isSubmitting },
+ } = useForm<{ data: TranslationValue[] }>({
+ mode: 'onSubmit',
+ });
+
+ const { fields } = useFieldArray({ control, name: 'data' });
+
+ React.useEffect(() => {
+ const initialValues = translations.map(translation => {
+ const language = languages.find(lang => lang.key === translation.locale)!;
+ const languageContext = translation.contexts.find(c => c.id === context?.id);
+ const value =
+ languageContext?.values[inlineEditState.translationKey] || inlineEditState.translationKey;
+ return {
+ language: language.key,
+ value,
+ key: inlineEditState.translationKey,
+ };
+ });
+ reset({ data: initialValues });
+ }, [context, inlineEditState.translationKey, languages, reset, translations]);
+
+ const closeModal = () => {
+ setInlineEditState({ inlineEdit: true, translationKey: '', context: '' });
+ };
+
+ const submit = async ({ data }: { data: TranslationValue[] }) => {
+ if (isDirty) {
+ const response = await postV2(data, context);
+ if (response === 200) {
+ setNotifications({
+ type: 'success',
+ text: t('System', 'Translations saved', null, false),
+ });
+ }
+ if (response instanceof FetchResponseError) {
+ const message = response.json?.prettyMessage
+ ? response.json.prettyMessage
+ : response.message;
+ setNotifications({
+ type: 'error',
+ text: t('System', 'An error occurred', null, false),
+ details: message,
+ });
+ }
+ }
+ closeModal();
+ };
+
+ return (
+ inlineEditState.context && (
+
+ )
+ );
+};
+
+export { TranslateModal };
diff --git a/app/react/I18N/actions/I18NActions.js b/app/react/I18N/actions/I18NActions.js
index 38f5e911aa..49b4f6a53a 100644
--- a/app/react/I18N/actions/I18NActions.js
+++ b/app/react/I18N/actions/I18NActions.js
@@ -2,8 +2,8 @@ import { actions as formActions } from 'react-redux-form';
import * as notifications from 'app/Notifications/actions/notificationsActions';
import { store } from 'app/store';
import { RequestParams } from 'app/utils/RequestParams';
+import { t } from 'app/I18N';
import I18NApi from '../I18NApi';
-import t from '../t';
export function inlineEditTranslation(contextId, key) {
return dispatch => {
diff --git a/app/react/I18N/components/I18N.js b/app/react/I18N/components/I18N.js
index 3339a99b4a..72295f0c8f 100644
--- a/app/react/I18N/components/I18N.js
+++ b/app/react/I18N/components/I18N.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
-export class I18N extends Component {
+class I18NComponent extends Component {
render() {
const dictionary = this.props.dictionaries.toJS().find(d => d.locale === this.props.locale) || {
values: {},
@@ -13,7 +13,7 @@ export class I18N extends Component {
}
}
-I18N.propTypes = {
+I18NComponent.propTypes = {
children: PropTypes.string,
locale: PropTypes.string,
dictionaries: PropTypes.object,
@@ -21,4 +21,4 @@ I18N.propTypes = {
const mapStateToProps = ({ locale, dictionaries }) => ({ locale, dictionaries });
-export default connect(mapStateToProps)(I18N);
+export const I18N = connect(mapStateToProps)(I18NComponent);
diff --git a/app/react/I18N/components/I18NMenu.tsx b/app/react/I18N/components/I18NMenu.tsx
index 0e74a6e592..8d49d472c2 100644
--- a/app/react/I18N/components/I18NMenu.tsx
+++ b/app/react/I18N/components/I18NMenu.tsx
@@ -1,28 +1,21 @@
+/* eslint-disable react/no-multi-comp */
/* eslint-disable react-hooks/rules-of-hooks */
import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { bindActionCreators, Dispatch } from 'redux';
-import { connect, ConnectedProps } from 'react-redux';
-import { IImmutable } from 'shared/types/Immutable';
+import { Location, useLocation } from 'react-router-dom';
+import { useAtom, useAtomValue } from 'jotai';
+import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/20/solid';
import { LanguagesListSchema } from 'shared/types/commonTypes';
-import { Icon } from 'UI';
-import { actions, Translate, t } from 'app/I18N';
-import { IStore } from 'app/istore';
-import { NeedAuthorization } from 'app/Auth';
+import { NeedAuthorization } from 'V2/Components/UI';
import { useOnClickOutsideElement } from 'app/utils/useOnClickOutsideElementHook';
-import { Location, useLocation } from 'react-router-dom';
+import { inlineEditAtom, localeAtom, settingsAtom, userAtom } from 'V2/atoms';
+import { Translate, t } from 'app/I18N';
const locationSearch = (location: Location) => {
const cleanSearch = location.search.split(/page=\d+|&page=\d+/).join('');
return cleanSearch === '?' ? '' : cleanSearch;
};
-const prepareValues = (
- languageMap: IImmutable,
- locale: string,
- location: Location
-) => {
- const languages: LanguagesListSchema = languageMap.toJS();
-
+const prepareValues = (languages: LanguagesListSchema, locale: string, location: Location) => {
const selectedLanguage =
languages.find(lang => lang.key === locale) || languages.find(lang => lang.default);
@@ -37,28 +30,20 @@ const prepareValues = (
return { languages, selectedLanguage, urlLocation, path };
};
-const mapStateToProps = (state: IStore) => ({
- languages: state.settings.collection.get('languages'),
- i18nmode: state.inlineEdit.get('inlineEdit'),
- locale: state.locale,
- user: state.user,
-});
+const SVGCircle = ({ fill }: { fill: string }) => (
+
+
+
+);
-const mapDispatchToProps = (dispatch: Dispatch<{}>) =>
- bindActionCreators({ toggleInlineEdit: actions.toggleInlineEdit }, dispatch);
+// eslint-disable-next-line max-statements
+const I18NMenu = () => {
+ const [inlineEditState, setInlineEditState] = useAtom(inlineEditAtom);
+ const locale = useAtomValue(localeAtom);
+ const user = useAtomValue(userAtom);
+ const { languages: languageList } = useAtomValue(settingsAtom);
-const connector = connect(mapStateToProps, mapDispatchToProps);
-
-type mappedProps = ConnectedProps;
-
-const i18NMenuComponent = ({
- languages: languageMap,
- i18nmode,
- user,
- locale,
- toggleInlineEdit,
-}: mappedProps) => {
- if (!languageMap || languageMap.size < 1 || (languageMap!.size <= 1 && !user.get('_id'))) {
+ if (!languageList || languageList.length < 1 || (languageList.length <= 1 && !user?._id)) {
return
;
}
@@ -68,7 +53,7 @@ const i18NMenuComponent = ({
const [dropdownOpen, setDropdownOpen] = useState(false);
const { languages, selectedLanguage, path, urlLocation } = prepareValues(
- languageMap!,
+ languageList,
locale,
location
);
@@ -88,21 +73,27 @@ const i18NMenuComponent = ({
return (
- {i18nmode && (
+ {inlineEditState.inlineEdit && (
+ setInlineEditState({ inlineEdit: false, translationKey: '', context: '' })
+ }
aria-label={t('System', 'Turn off inline translation', null, false)}
>
-
+ {inlineEditState.inlineEdit ? (
+
+ ) : (
+
+ )}
@@ -112,7 +103,7 @@ const i18NMenuComponent = ({
)}
- {!i18nmode && (
+ {!inlineEditState.inlineEdit && (
{selectedLanguage?.localized_label}
-
+ {dropdownOpen ? : }
@@ -146,11 +137,19 @@ const i18NMenuComponent = ({
className="live-translate"
type="button"
onClick={() => {
- toggleInlineEdit();
+ setInlineEditState({
+ inlineEdit: !inlineEditState.inlineEdit,
+ translationKey: '',
+ context: '',
+ });
setDropdownOpen(false);
}}
>
-
+ {inlineEditState.inlineEdit ? (
+
+ ) : (
+
+ )}
Live translate
@@ -162,6 +161,4 @@ const i18NMenuComponent = ({
);
};
-const container = connector(i18NMenuComponent);
-
-export { container as i18NMenuComponent };
+export { I18NMenu };
diff --git a/app/react/I18N/components/Translate.js b/app/react/I18N/components/Translate.js
deleted file mode 100644
index e5e73dd7c3..0000000000
--- a/app/react/I18N/components/Translate.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component, Fragment } from 'react';
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import { actions } from 'app/I18N';
-
-const parseMarkdownMarker = (line, regexp, wrapper) => {
- const matches = line.match(regexp);
- if (matches == null) {
- return matches;
- }
- const parts = matches.input.split(matches[0]);
- return (
- <>
- {parts[0]}
- {wrapper(matches[1])}
- {parts[1]}
- >
- );
-};
-
-const parseMarkdownBoldMarker = line =>
- parseMarkdownMarker(line, /\*{2}(.*)\*{2}/, text => {text} );
-
-const parseMarkdownItalicMarker = line =>
- parseMarkdownMarker(line, /\*(.*)\*/, text => {text} );
-
-class Translate extends Component {
- static resetCachedTranslation() {
- Translate.translation = null;
- }
-
- constructor(props) {
- super(props);
- this.onClick = this.onClick.bind(this);
- }
-
- onClick(e) {
- if (this.props.i18nmode) {
- e.stopPropagation();
- e.preventDefault();
- this.props.edit(this.props.context, this.props.translationKey);
- }
- }
-
- render() {
- const lines = this.props.children ? this.props.children.split('\n') : [];
- const className = this.props.i18nmode ? 'translation active' : 'translation';
- return (
-
- {lines.map((line, index) => {
- const boldMatches = parseMarkdownBoldMarker(line);
- const italicMatches = parseMarkdownItalicMarker(line);
- return (
-
- {boldMatches ||
- italicMatches || ( // eslint-disable-next-line react/jsx-no-useless-fragment
- <>{line}>
- )}
- {index < lines.length - 1 && }
-
- );
- })}
-
- );
- }
-}
-
-Translate.defaultProps = {
- i18nmode: false,
- context: 'System',
- edit: false,
- translationKey: '',
- className: '',
-};
-
-Translate.propTypes = {
- translationKey: PropTypes.string,
- context: PropTypes.string,
- edit: PropTypes.func,
- i18nmode: PropTypes.bool,
- children: PropTypes.string.isRequired,
- className: PropTypes.string,
-};
-
-const mapStateToProps = (state, props) => {
- if (
- !Translate.translation ||
- Translate.translation.locale !== state.locale ||
- state.inlineEdit.get('inlineEdit')
- ) {
- const translations = state.translations.toJS();
- Translate.translation = translations.find(t => t.locale === state.locale) || { contexts: [] };
- }
- const _ctx = props.context || 'System';
- const key = props.translationKey || props.children;
- const context = Translate.translation.contexts.find(ctx => ctx.id === _ctx) || { values: {} };
- const canEditThisValue = _ctx === 'System' || !!context.values[props.children];
- return {
- translationKey: key,
- children: context.values[key] || props.children,
- i18nmode: state.inlineEdit.get('inlineEdit') && canEditThisValue,
- };
-};
-
-function mapDispatchToProps(dispatch) {
- return bindActionCreators({ edit: actions.inlineEditTranslation }, dispatch);
-}
-
-export { mapStateToProps, Translate };
-export default connect(mapStateToProps, mapDispatchToProps)(Translate);
diff --git a/app/react/I18N/components/TranslateForm.js b/app/react/I18N/components/TranslateForm.js
deleted file mode 100644
index 6c29a61bb8..0000000000
--- a/app/react/I18N/components/TranslateForm.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { bindActionCreators } from 'redux';
-import { Form, Field } from 'react-redux-form';
-import { connect } from 'react-redux';
-import { actions, Translate, t } from 'app/I18N';
-import Modal from 'app/Layout/Modal';
-import { FormGroup } from 'app/Forms';
-
-export class TranslateForm extends Component {
- constructor(props) {
- super(props);
- this.submit = this.submit.bind(this);
- this.cancel = this.cancel.bind(this);
- }
-
- submit(values) {
- let translations = this.props.translations.toJS();
- translations = translations.map(translation => {
- const { locale } = translation;
- const context = translation.contexts.find(c => c.id === this.props.context);
- context.values[this.props.value] = values[locale];
- translation.contexts = [context];
- return translation;
- });
- this.props.saveTranslations(translations);
- this.props.close();
- }
-
- cancel() {
- this.props.close();
- }
-
- render() {
- const translations = this.props.translations.toJS();
- const languages = translations.map(translation => translation.locale);
-
- return (
-
-
-
- Translate
-
-
-
-
-
-
- {t('System', 'Cancel', null, false)}
-
-
- {t('System', 'Submit', null, false)}
-
-
-
- );
- }
-}
-
-TranslateForm.defaultProps = {
- isOpen: false,
-};
-
-TranslateForm.propTypes = {
- saveTranslations: PropTypes.func.isRequired,
- close: PropTypes.func.isRequired,
- isOpen: PropTypes.bool,
- context: PropTypes.string.isRequired,
- value: PropTypes.string.isRequired,
- translations: PropTypes.instanceOf(Object).isRequired,
-};
-
-export function mapStateToProps(state) {
- return {
- translations: state.translations,
- isOpen: state.inlineEdit.get('showInlineEditForm'),
- context: state.inlineEdit.get('context'),
- value: state.inlineEdit.get('key'),
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return bindActionCreators(
- { saveTranslations: actions.saveTranslations, close: actions.closeInlineEditTranslation },
- dispatch
- );
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(TranslateForm);
diff --git a/app/react/I18N/components/specs/I18NMenu.spec.tsx b/app/react/I18N/components/specs/I18NMenu.spec.tsx
index f8cbb92073..e6743238da 100644
--- a/app/react/I18N/components/specs/I18NMenu.spec.tsx
+++ b/app/react/I18N/components/specs/I18NMenu.spec.tsx
@@ -2,63 +2,134 @@
* @jest-environment jsdom
*/
import React from 'react';
-import { act, fireEvent, RenderResult, screen } from '@testing-library/react';
-import { Provider } from 'react-redux';
-import { MockStoreEnhanced } from 'redux-mock-store';
+import { act, fireEvent, RenderResult, screen, render } from '@testing-library/react';
import { Location, MemoryRouter } from 'react-router-dom';
-import Immutable from 'immutable';
-import { defaultState, renderConnectedContainer } from 'app/utils/test/renderConnected';
-import { i18NMenuComponent as I18NMenu } from '../I18NMenu';
+import { createStore, Provider } from 'jotai';
+import { ClientUserSchema } from 'app/apiResponseTypes';
+import { inlineEditAtom, localeAtom, settingsAtom, userAtom } from 'V2/atoms';
+import { TestAtomStoreProvider } from 'V2/testing';
+import { UserRole } from 'shared/types/userSchema';
+import { LanguageISO6391 } from 'shared/types/commonTypes';
+import { I18NMenu } from '../I18NMenu';
+
+const defaultLanguages = [
+ {
+ _id: '1',
+ label: 'English',
+ key: 'en' as LanguageISO6391,
+ localized_label: 'English',
+ default: true,
+ },
+ {
+ _id: '2',
+ label: 'Spanish',
+ key: 'es' as LanguageISO6391,
+ localized_label: 'Español',
+ default: false,
+ },
+];
+
+const users = [
+ { _id: 'admin', username: 'admin', role: UserRole.ADMIN, email: '' },
+ { _id: 'collab', username: 'collab', role: UserRole.COLLABORATOR, email: '' },
+];
describe('I18NMenu', () => {
- let props: any;
+ const initialEntry: Partial = { pathname: '/library' };
+ const inlineEditAtomValue = { inlineEdit: false };
let renderResult: RenderResult;
- let store: MockStoreEnhanced;
- let location: Partial;
- const toggleInlineEditMock = jest.fn();
+ let settingsAtomValue = { languages: defaultLanguages };
+ let localeAtomValue = 'en';
Reflect.deleteProperty(global.window, 'location');
window.location = { ...window.location, assign: jest.fn() };
+ const renderComponent = (user?: ClientUserSchema) => {
+ renderResult = render(
+
+
+
+
+
+ );
+ };
+
beforeEach(() => {
+ initialEntry.pathname = '/library';
+ localeAtomValue = 'en';
+ settingsAtomValue = { languages: defaultLanguages };
+ inlineEditAtomValue.inlineEdit = false;
jest.clearAllMocks();
- const languages = [
- { _id: '1', key: 'en', label: 'English', localized_label: 'English' },
- { _id: '2', key: 'es', label: 'Spanish', localized_label: 'Español', default: true },
- ];
-
- props = {
- languages: Immutable.fromJS(languages),
- toggleInlineEdit: toggleInlineEditMock,
- i18nmode: false,
- locale: 'es',
- };
+ });
+
+ it('should render the links to the different languages', () => {
+ renderComponent();
+ const links = screen.getAllByRole('link');
+ expect(links.map(link => link.getAttribute('href'))).toEqual(
+ expect.arrayContaining(['/en/library', '/es/library'])
+ );
+ });
- location = {
- pathname: '/templates/2452345',
- search: '?query=weneedmoreclerics',
+ it('should not render anything if there is only one language', () => {
+ settingsAtomValue = {
+ languages: [
+ { _id: '2', label: 'Spanish', key: 'es', localized_label: 'Español', default: true },
+ ],
};
+ renderComponent();
+ const links = screen.queryAllByRole('link');
+ expect(links.length).toBe(0);
});
- const render = (userType?: 'admin' | 'editor' | 'collaborator') => {
- const storeUser = userType
- ? Immutable.fromJS({ _id: 'user1', role: userType })
- : Immutable.fromJS({});
-
- props.user = userType
- ? Immutable.fromJS({ _id: 'user1', role: userType })
- : Immutable.fromJS({});
-
- ({ renderResult, store } = renderConnectedContainer(
- ,
- () => ({
- ...defaultState,
- user: storeUser,
- }),
- 'MemoryRouter',
- [location]
- ));
- };
+ it('should show as active the current locale', async () => {
+ renderComponent(users[0]);
+ const [listItem] = renderResult
+ .getAllByRole('listitem')
+ .filter(item => item.textContent === 'English');
+ expect(listItem.getAttribute('class')).toBe('menuNav-item active');
+ });
+
+ it('should active toggle translation edit mode when clicking Live translate', async () => {
+ renderComponent(users[0]);
+ expect(renderResult.container).toMatchSnapshot('before turning on live translate');
+ await act(async () => {
+ fireEvent.click(screen.getByText('Live translate').parentElement!);
+ });
+ expect(renderResult.container).toMatchSnapshot('after turning on live translate');
+ });
+
+ describe('when there is a user', () => {
+ it('should render then laguages and the live translate option', () => {
+ renderComponent(users[0]);
+ expect(renderResult.getByText('Live translate')).toBeInTheDocument();
+ });
+
+ it('should not render live translate for unauthorized users', () => {
+ renderComponent(users[1]);
+ expect(renderResult.queryByText('Live translate')).not.toBeInTheDocument();
+ });
+
+ it('should display the language section if there is only one language', () => {
+ settingsAtomValue = {
+ languages: [
+ { _id: '2', label: 'Spanish', key: 'es', localized_label: 'Español', default: true },
+ ],
+ };
+ renderComponent(users[1]);
+ const links = screen.queryAllByRole('link');
+ expect(links.length).toBe(1);
+ expect(links.map(link => link.getAttribute('href'))).toEqual(
+ expect.arrayContaining(['/es/library'])
+ );
+ });
+ });
describe('Paths', () => {
it.each`
@@ -71,10 +142,10 @@ describe('I18NMenu', () => {
`(
'should create the expected links for $pathName',
async ({ locale, currentPath, search, expectedPath }) => {
- props.locale = locale;
- location.pathname = currentPath;
- location.search = search;
- render('admin');
+ localeAtomValue = locale;
+ initialEntry.pathname = currentPath;
+ initialEntry.search = search;
+ renderComponent(users[0]);
const links = screen.getAllByRole('link');
expect(links.map(link => link.getAttribute('href'))).toEqual(
expect.arrayContaining([`/en${expectedPath}`, `/es${expectedPath}`])
@@ -83,89 +154,47 @@ describe('I18NMenu', () => {
);
});
- it('should return empty if there are no languages', () => {
- props.languages = Immutable.fromJS([]);
- render('admin');
- expect(screen.queryByText('Live translate')).not.toBeInTheDocument();
- expect(screen.queryByText('English')).not.toBeInTheDocument();
- });
-
- it('should not show live transtions for not authorized user', async () => {
- render('collaborator');
- expect(screen.queryByText('Live translate')).not.toBeInTheDocument();
- expect(screen.getByText('English')).toBeInTheDocument();
- });
-
- it('should show live transtions for authorized user', async () => {
- render('editor');
- expect(screen.queryByText('Live translate')).toBeInTheDocument();
- const listItems = screen.getAllByRole('link');
- expect(listItems.map(item => item.textContent)).toEqual(['English', 'Español']);
- });
-
- it('should show as active the current locale', async () => {
- render('admin');
- const [listItem] = screen
- .getAllByRole('listitem')
- .filter(item => item.textContent === 'Español');
- expect(listItem.getAttribute('class')).toBe('menuNav-item active');
- });
-
- it('should not display the language section if there is only one language and no user', () => {
- props.languages = Immutable.fromJS([{ _id: '1', key: 'en', label: 'English', default: true }]);
- render();
- expect(screen.queryByText('English')).not.toBeInTheDocument();
- });
-
- it('should display the language section if there is only one language and a user', () => {
- props.languages = Immutable.fromJS([{ _id: '1', key: 'en', label: 'English', default: true }]);
- render('collaborator');
- expect(screen.queryByText('English')).toBeInTheDocument();
- expect(screen.getByRole('link').getAttribute('href')).toBe(
- '/en/templates/2452345?query=weneedmoreclerics'
- );
- });
-
- it('should change to a single button when live translating', async () => {
- props.i18nmode = true;
- render('editor');
- expect(screen.getByRole('button').parentElement!.textContent).toEqual('Live translate');
- const activeIcon = renderResult.container.getElementsByClassName('live-on');
- expect(activeIcon.length).toBe(1);
- const listItems = screen.queryAllByRole('link');
- expect(listItems).toEqual([]);
- });
-
- it('should active toggle translation edit mode when clicking Live translate', async () => {
- render('admin');
- await act(async () => {
- fireEvent.click(screen.getByText('Live translate').parentElement!);
+ describe('reloading after language change', () => {
+ const testStore = createStore();
+ testStore.set(userAtom, users[0]);
+ testStore.set(localeAtom, 'en');
+ testStore.set(settingsAtom, settingsAtomValue);
+
+ it('should trigger a reload if the current language is deleted', async () => {
+ const result = render(
+
+
+
+
+
+ );
+
+ const newSettingsAtomValue = {
+ languages: [
+ {
+ _id: '2',
+ label: 'Spanish',
+ key: 'es' as LanguageISO6391,
+ localized_label: 'Español',
+ default: true,
+ },
+ ],
+ };
+
+ await act(() => {
+ testStore.set(settingsAtom, newSettingsAtomValue);
+ });
+
+ result.rerender(
+
+
+
+
+
+ );
+
+ expect(window.location.assign).toHaveBeenCalledTimes(1);
+ expect(window.location.assign).toHaveBeenCalledWith('/library');
});
- expect(toggleInlineEditMock).toBeCalled();
- });
-
- it('should trigger a reload if the current language is deleted', async () => {
- props.locale = 'en';
- render('admin');
- props.languages = Immutable.fromJS([
- {
- _id: '2',
- key: 'es',
- label: 'Spanish',
- localized_label: 'Español',
- default: true,
- },
- ]);
-
- renderResult.rerender(
-
-
-
-
-
- );
-
- expect(window.location.assign).toHaveBeenCalledTimes(1);
- expect(window.location.assign).toHaveBeenCalledWith('/templates/2452345');
});
});
diff --git a/app/react/I18N/components/specs/Translate.spec.js b/app/react/I18N/components/specs/Translate.spec.js
deleted file mode 100644
index 3dd194e2ea..0000000000
--- a/app/react/I18N/components/specs/Translate.spec.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import Immutable from 'immutable';
-import { Translate, mapStateToProps } from '../Translate';
-
-describe('Translate', () => {
- let component;
- let props;
-
- beforeEach(() => {
- props = {
- translationKey: 'Search',
- edit: jasmine.createSpy('edit'),
- };
- });
-
- const render = () => {
- component = shallow(Search );
- };
-
- describe('render', () => {
- beforeEach(render);
- it('should render the property children inside of a span', () => {
- expect(component.find('span').text()).toBe('Search');
- });
-
- describe('when i18nmode is true', () => {
- beforeEach(() => {
- props.i18nmode = true;
- render();
- });
-
- it('should render the span with class active', () => {
- expect(component.find('span').hasClass('active')).toBe(true);
- });
-
- describe('onClick', () => {
- let mockEvent;
- beforeEach(() => {
- mockEvent = jasmine.createSpyObj(['stopPropagation', 'preventDefault']);
- });
-
- it('should stop the event from going up', () => {
- component.simulate('click', mockEvent);
- expect(mockEvent.stopPropagation).toHaveBeenCalled();
- expect(mockEvent.preventDefault).toHaveBeenCalled();
- });
-
- it('should call edit with context and children', () => {
- component.simulate('click', mockEvent);
- expect(props.edit).toHaveBeenCalledWith('System', 'Search');
- });
- });
- });
- });
-
- describe('markdown support', () => {
- it('should parse line break in multiline text', () => {
- component = shallow(
-
- {`this
- is
- multiline
- text`}
-
- );
- expect(component.find('span').html()).toBe(
- 'this is multiline text '
- );
- });
-
- it('should parse a italic and a highlighted text in translation value by line', () => {
- component = shallow(
-
- {`this
- is
- *an italic*
- text and
- this is **a highlighted** text. *discarted*
- *Markdown*
- `}
-
- );
- expect(component.find('span').html()).toBe(
- // eslint-disable-next-line max-len
- 'this is an italic text and this is a highlighted text. *discarted* Markdown '
- );
- });
- });
-
- describe('resetCachedTranslation', () => {
- it('should set null the current catched translation', () => {
- Translate.translation = 'some catched translation';
- Translate.resetCachedTranslation();
- expect(Translate.translation).toBe(null);
- });
- });
-
- describe('mapStateToProps', () => {
- let translations;
- beforeEach(() => {
- translations = [
- { locale: 'en', contexts: [{ id: 'System', values: { Search: 'Search' } }] },
- { locale: 'es', contexts: [{ id: 'System', values: { Search: 'Buscar' } }] },
- ];
- });
-
- it('should try to translate the children and pass it on children', () => {
- props = { children: 'Search', context: 'System' };
- const state = {
- locale: 'es',
- inlineEdit: Immutable.fromJS({ inlineEdit: true }),
- translations: Immutable.fromJS(translations),
- };
- expect(mapStateToProps(state, props).children).toBe('Buscar');
- expect(mapStateToProps(state, props).i18nmode).toBe(true);
- });
-
- it('should allow overriding translation key', () => {
- props = { children: 'Test', translationKey: 'Search', context: 'System' };
- const state = {
- locale: 'es',
- inlineEdit: Immutable.fromJS({ inlineEdit: true }),
- translations: Immutable.fromJS(translations),
- };
- expect(mapStateToProps(state, props).children).toBe('Buscar');
- expect(mapStateToProps(state, props).i18nmode).toBe(true);
- });
-
- it('should store the current locale translation to be fast', () => {
- props = { children: 'Search', context: 'System' };
- const state = {
- locale: 'es',
- inlineEdit: Immutable.fromJS({ inlineEdit: true }),
- translations: Immutable.fromJS(translations),
- };
- mapStateToProps(state, props);
- expect(Translate.translation.locale).toBe('es');
- state.locale = 'en';
- mapStateToProps(state, props);
- expect(Translate.translation.locale).toBe('en');
- });
- });
-});
diff --git a/app/react/I18N/components/specs/TranslateForm.spec.js b/app/react/I18N/components/specs/TranslateForm.spec.js
deleted file mode 100644
index 78e79d9405..0000000000
--- a/app/react/I18N/components/specs/TranslateForm.spec.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import Immutable from 'immutable';
-import { Form, Field } from 'react-redux-form';
-import Modal from 'app/Layout/Modal';
-import { FormGroup } from 'app/Forms';
-import { TranslateForm } from '../TranslateForm';
-
-describe('TranslateForm', () => {
- let component;
- let props;
- const translations = Immutable.fromJS([
- {
- locale: 'en',
- contexts: [
- { id: 'System', values: { Search: 'Search', Find: 'Find' } },
- { id: '123', values: {} },
- ],
- },
- {
- locale: 'es',
- contexts: [
- { id: 'System', values: { Search: 'Buscar', Find: 'Encontrar' } },
- { id: '123', values: {} },
- ],
- },
- ]);
-
- beforeEach(() => {
- props = {
- saveTranslations: jasmine.createSpy('saveTranslations'),
- close: jasmine.createSpy('close'),
- isOpen: true,
- context: 'System',
- value: 'Search',
- translations,
- };
- });
-
- const render = () => {
- component = shallow( );
- };
-
- describe('render', () => {
- beforeEach(render);
- it('should render a Modal and pass isopen property', () => {
- expect(component.find(Modal).props().isOpen).toBe(true);
- });
-
- it('should render a LocalForm and pass submit function and initialState', () => {
- expect(component.find(Form).props().onSubmit).toBe(component.instance().submit);
- expect(component.find(Form).props().model).toBe('inlineEditModel');
- });
-
- it('should redner a FormGroup for each language', () => {
- expect(component.find(FormGroup).at(0).props().model).toBe('.en');
- expect(component.find(FormGroup).at(1).props().model).toBe('.es');
- });
-
- it('should redner a Field for each language', () => {
- expect(component.find(Field).at(0).props().model).toBe('.en');
- expect(component.find(Field).at(1).props().model).toBe('.es');
- });
- });
-
- describe('cancel', () => {
- it('should call props.close()', () => {
- render();
- component.instance().cancel();
- expect(props.close).toHaveBeenCalled();
- });
- });
-
- describe('submit', () => {
- it('should call saveTranslations with only the updated context', () => {
- const expectedTranslations = [
- {
- locale: 'en',
- contexts: [{ id: 'System', values: { Search: 'Search en', Find: 'Find' } }],
- },
- {
- locale: 'es',
- contexts: [{ id: 'System', values: { Search: 'Buscar es', Find: 'Encontrar' } }],
- },
- ];
- render();
- component.instance().submit({ en: 'Search en', es: 'Buscar es' });
- expect(props.saveTranslations).toHaveBeenCalledWith(expectedTranslations);
- });
-
- it('should call props.close()', () => {
- render();
- component.instance().submit({ en: 'Search en', es: 'Buscar es' });
- expect(props.close).toHaveBeenCalled();
- });
- });
-});
diff --git a/app/react/I18N/components/specs/__snapshots__/I18NMenu.spec.tsx.snap b/app/react/I18N/components/specs/__snapshots__/I18NMenu.spec.tsx.snap
new file mode 100644
index 0000000000..b1c65982ba
--- /dev/null
+++ b/app/react/I18N/components/specs/__snapshots__/I18NMenu.spec.tsx.snap
@@ -0,0 +1,131 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`I18NMenu should active toggle translation edit mode when clicking Live translate: after turning on live translate 1`] = `
+
+
+
+`;
+
+exports[`I18NMenu should active toggle translation edit mode when clicking Live translate: before turning on live translate 1`] = `
+
+
+
+`;
diff --git a/app/react/I18N/index.js b/app/react/I18N/index.js
deleted file mode 100644
index 28c9b81aff..0000000000
--- a/app/react/I18N/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { i18NMenuComponent } from './components/I18NMenu';
-import I18NLink from './components/I18NLink';
-import I18N from './components/I18N';
-import Translate from './components/Translate';
-import TranslateForm from './components/TranslateForm';
-import t from './t';
-import I18NUtils from './utils';
-import I18NApi from './I18NApi';
-import * as actions from './actions/I18NActions';
-
-export {
- i18NMenuComponent as I18NMenu,
- I18NLink,
- I18NUtils,
- I18N,
- Translate,
- TranslateForm,
- t,
- I18NApi,
- actions,
-};
diff --git a/app/react/I18N/index.ts b/app/react/I18N/index.ts
new file mode 100644
index 0000000000..a8f58f3ea4
--- /dev/null
+++ b/app/react/I18N/index.ts
@@ -0,0 +1,13 @@
+import I18NLink from './components/I18NLink';
+import I18NUtils from './utils';
+import I18NApi from './I18NApi';
+
+export { I18NApi };
+export { I18N } from './components/I18N';
+export { I18NMenu } from './components/I18NMenu';
+export * as actions from './actions/I18NActions';
+export { I18NLink as I18NLinkV2 } from './I18NLinkV2';
+export { Translate } from './Translate';
+export { t } from './translateFunction';
+export { TranslateModal } from './TranslateModal';
+export { I18NLink, I18NUtils };
diff --git a/app/react/I18N/specs/I18NLinkV2.spec.tsx b/app/react/I18N/specs/I18NLinkV2.spec.tsx
new file mode 100644
index 0000000000..1ae3d34cbe
--- /dev/null
+++ b/app/react/I18N/specs/I18NLinkV2.spec.tsx
@@ -0,0 +1,55 @@
+/**
+ * @jest-environment jsdom
+ */
+/* eslint-disable react/jsx-props-no-spreading */
+import React from 'react';
+import { render, RenderResult } from '@testing-library/react';
+import { BrowserRouter } from 'react-router-dom';
+import { TestAtomStoreProvider } from 'V2/testing';
+import { localeAtom } from 'V2/atoms';
+import { I18NLink } from '../I18NLinkV2';
+
+describe('I18NLink', () => {
+ let renderResult: RenderResult;
+ let locale = 'fr';
+ let to = '/about';
+ let activeClassname = '';
+
+ const renderComponent = () => {
+ renderResult = render(
+
+
+
+ My link
+
+
+
+ );
+ };
+
+ it('renders a link with the locale prefixed', () => {
+ renderComponent();
+ const link = renderResult.getByText('My link');
+ expect(link.getAttribute('href')).toBe('/fr/about');
+ });
+
+ it('renders a link without a locale if localeAtom is empty', () => {
+ locale = '';
+ to = '/contact';
+
+ renderComponent();
+
+ const link = renderResult.getByText('My link');
+ expect(link.getAttribute('href')).toBe('/contact');
+ });
+
+ it('should apply active classname', () => {
+ activeClassname = 'red';
+ to = '/';
+
+ renderComponent();
+
+ const link = renderResult.getByText('My link');
+ expect(link.getAttribute('class')).toBe(' red');
+ });
+});
diff --git a/app/react/I18N/specs/TranslateModal.spec.tsx b/app/react/I18N/specs/TranslateModal.spec.tsx
new file mode 100644
index 0000000000..0eaeb20b63
--- /dev/null
+++ b/app/react/I18N/specs/TranslateModal.spec.tsx
@@ -0,0 +1,138 @@
+/**
+ * @jest-environment jsdom
+ */
+import React, { act } from 'react';
+import { fireEvent, render, RenderResult } from '@testing-library/react';
+import { TestAtomStoreProvider } from 'V2/testing';
+import { settingsAtom, translationsAtom, inlineEditAtom, notificationAtom } from 'V2/atoms';
+import * as translationsAPI from 'V2/api/translations';
+import { NotificationsContainer } from 'V2/Components/UI';
+import { TranslateModal } from '../TranslateModal';
+import { languages, translations } from './fixtures';
+
+describe('TranslateModal', () => {
+ let renderResult: RenderResult;
+
+ beforeAll(() => {
+ jest.spyOn(translationsAPI, 'postV2').mockImplementation(async () => Promise.resolve(200));
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const renderComponent = (inlineEdit: boolean, context: string, translationKey: string) => {
+ renderResult = render(
+
+
+
+
+ );
+ };
+
+ it('renders the modal with fields for each language', () => {
+ renderComponent(true, 'System', 'Search');
+ const inputFields = renderResult.queryAllByRole('textbox');
+ expect(inputFields).toHaveLength(2);
+ expect(inputFields[0]).toHaveValue('Search');
+ expect(inputFields[1]).toHaveValue('Buscar');
+ expect(renderResult.getByText('EN'));
+ expect(renderResult.getByText('ES'));
+ });
+
+ it('should close the modal without saving', async () => {
+ renderComponent(true, 'System', 'Search');
+ expect(renderResult.getByText('Translate'));
+ await act(() => {
+ fireEvent.click(renderResult.getByText('Cancel'));
+ });
+ expect(renderResult.queryByText('Translate')).not.toBeInTheDocument();
+ expect(translationsAPI.postV2).not.toHaveBeenCalled();
+ });
+
+ // eslint-disable-next-line max-statements
+ it('submits the form with updated values, disables while saving, and closes the modal', async () => {
+ renderComponent(true, 'System', 'Search');
+
+ const saveButton = renderResult.getByTestId('save-button');
+ const inputFields = renderResult.queryAllByRole('textbox');
+ const cancelButton = renderResult.getByText('Cancel');
+
+ await act(() => {
+ fireEvent.change(inputFields[1], { target: { value: 'Busqueda' } });
+ fireEvent.click(saveButton);
+ });
+
+ expect(saveButton).toBeDisabled();
+ expect(inputFields[0]).toBeDisabled();
+ expect(inputFields[1]).toBeDisabled();
+ expect(cancelButton).toBeDisabled();
+
+ expect(translationsAPI.postV2).toHaveBeenCalledWith(
+ [
+ { language: 'en', value: 'Search', key: 'Search' },
+ { language: 'es', value: 'Busqueda', key: 'Search' },
+ ],
+ translations[0].contexts[0]
+ );
+ expect(renderResult.queryByText('Translate')).not.toBeInTheDocument();
+ expect(renderResult.queryByText('Translations saved')).toBeInTheDocument();
+ });
+
+ it('should not allow sending empty fields', async () => {
+ renderComponent(true, 'System', 'Search');
+ const inputFields = renderResult.queryAllByRole('textbox');
+ const saveButton = renderResult.getByTestId('save-button');
+
+ await act(() => {
+ fireEvent.change(inputFields[0], { target: { value: '' } });
+ fireEvent.click(saveButton);
+ });
+
+ expect(translationsAPI.postV2).not.toHaveBeenCalled();
+ });
+
+ it('should use the default context key if translation does not exist', async () => {
+ renderComponent(true, 'System', 'This key is not in the database');
+ const inputFields = renderResult.queryAllByRole('textbox');
+ expect(inputFields[0]).toHaveValue('This key is not in the database');
+ expect(inputFields[1]).toHaveValue('This key is not in the database');
+ const saveButton = renderResult.getByTestId('save-button');
+
+ await act(() => {
+ fireEvent.change(inputFields[0], { target: { value: 'My new key' } });
+ fireEvent.change(inputFields[1], { target: { value: 'Nueva llave' } });
+ fireEvent.click(saveButton);
+ });
+
+ expect(translationsAPI.postV2).toHaveBeenCalledWith(
+ [
+ { language: 'en', value: 'My new key', key: 'This key is not in the database' },
+ { language: 'es', value: 'Nueva llave', key: 'This key is not in the database' },
+ ],
+ translations[0].contexts[0]
+ );
+ expect(renderResult.queryByText('Translate')).not.toBeInTheDocument();
+ });
+
+ it('should not save if there are no changes', async () => {
+ renderComponent(true, 'System', 'Search');
+ const saveButton = renderResult.getByTestId('save-button');
+ const inputFields = renderResult.queryAllByRole('textbox');
+
+ await act(() => {
+ fireEvent.change(inputFields[1], { target: { value: 'Nueva traducción' } });
+ fireEvent.change(inputFields[1], { target: { value: 'Buscar' } });
+ fireEvent.click(saveButton);
+ });
+
+ expect(translationsAPI.postV2).not.toHaveBeenCalled();
+ });
+});
diff --git a/app/react/I18N/specs/fixtures.ts b/app/react/I18N/specs/fixtures.ts
new file mode 100644
index 0000000000..8e7fc17029
--- /dev/null
+++ b/app/react/I18N/specs/fixtures.ts
@@ -0,0 +1,64 @@
+import { ClientTranslationSchema } from 'app/istore';
+import { LanguagesListSchema } from 'shared/types/commonTypes';
+
+const languages: LanguagesListSchema = [
+ {
+ _id: '1',
+ label: 'English',
+ key: 'en',
+ default: true,
+ },
+ {
+ _id: '1',
+ label: 'Spanish',
+ key: 'es',
+ },
+];
+
+const translations: ClientTranslationSchema[] = [
+ {
+ locale: 'en',
+ contexts: [
+ {
+ id: 'System',
+ label: 'System',
+ values: {
+ Search: 'Search',
+ confirmDeleteDocument: 'Are you sure you want to delete this document?',
+ confirmDeleteEntity: 'Are you sure you want to delete this entity?',
+ },
+ },
+ ],
+ },
+ {
+ locale: 'es',
+ contexts: [
+ {
+ id: 'System',
+ label: 'System',
+ values: {
+ Search: 'Buscar',
+ confirmDeleteDocument: '¿Esta seguro que quiere borrar este documento?',
+ },
+ },
+ ],
+ },
+];
+
+const updatedTranslations: ClientTranslationSchema[] = [
+ translations[0],
+ {
+ ...translations[1],
+ contexts: [
+ {
+ ...translations[1].contexts,
+ values: {
+ Search: 'Buscar',
+ confirmDeleteDocument: 'Actualizado!',
+ },
+ },
+ ],
+ },
+];
+
+export { translations, updatedTranslations, languages };
diff --git a/app/react/I18N/specs/t.spec.js b/app/react/I18N/specs/t.spec.js
deleted file mode 100644
index 06ba8f2623..0000000000
--- a/app/react/I18N/specs/t.spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { store } from 'app/store';
-import Immutable from 'immutable';
-import t from '../t';
-
-describe('t', () => {
- let state;
-
- beforeEach(() => {
- t.resetCachedTranslation();
- const dictionaries = [
- {
- locale: 'en',
- contexts: [
- {
- id: 'System',
- label: 'System',
- values: {
- Search: 'Search',
- confirmDeleteDocument: 'Are you sure you want to delete this document?',
- confirmDeleteEntity: 'Are you sure you want to delete this entity?',
- },
- },
- ],
- },
- {
- locale: 'es',
- contexts: [
- {
- id: 'System',
- label: 'System',
- values: {
- Search: 'Buscar',
- confirmDeleteDocument: '¿Esta seguro que quiere borrar este documento?',
- },
- },
- ],
- },
- ];
-
- state = {
- locale: 'es',
- translations: Immutable.fromJS(dictionaries),
- user: Immutable.fromJS({ _id: 'abc' }),
- };
-
- spyOn(store, 'getState').and.returnValue(state);
- });
-
- it('should return the translation', () => {
- expect(
- t('System', 'confirmDeleteDocument', 'Are you sure you want to delete this document?')
- ).toBe('¿Esta seguro que quiere borrar este documento?');
- });
-
- describe('when there is no translation', () => {
- it('should return the default text', () => {
- expect(
- t('System', 'confirmDeleteEntity', 'Are you sure you want to delete this entity?')
- ).toBe('Are you sure you want to delete this entity?');
- });
- });
-
- describe('only passing context and key', () => {
- it('should use it as default text', () => {
- expect(t('System', 'not translated', 'not translated'));
- });
- });
-
- describe('when no context', () => {
- it('should throw an error', () => {
- spyOn(console, 'warn');
- t(undefined, 'confirmDeleteEntity', 'Are you sure you want to delete this entity?');
- //eslint-disable-next-line no-console
- expect(console.warn).toHaveBeenCalled();
- });
- });
-});
diff --git a/app/react/I18N/specs/translateFunction.spec.tsx b/app/react/I18N/specs/translateFunction.spec.tsx
new file mode 100644
index 0000000000..ab439a45be
--- /dev/null
+++ b/app/react/I18N/specs/translateFunction.spec.tsx
@@ -0,0 +1,113 @@
+/**
+ * @jest-environment jsdom
+ */
+import React from 'react';
+import { act, render, RenderResult } from '@testing-library/react';
+import { Provider } from 'jotai';
+import { localeAtom, translationsAtom, atomStore } from 'V2/atoms';
+import { t } from '../translateFunction';
+import { translations, updatedTranslations } from './fixtures';
+
+describe('t function', () => {
+ let renderResult: RenderResult;
+ let locale = 'es';
+
+ const renderEnvironment = (...args: typeof t.arguments) => {
+ renderResult = render({t(...args)} );
+ };
+
+ beforeEach(() => {
+ atomStore.set(translationsAtom, translations);
+ atomStore.set(localeAtom, locale);
+ jest.spyOn(atomStore, 'sub');
+ locale = 'es';
+ });
+
+ it('should return the translation component with the translated text and not subscribe to the store', () => {
+ renderEnvironment(
+ 'System',
+ 'confirmDeleteDocument',
+ 'Are you sure you want to delete this document?'
+ );
+ expect(
+ renderResult.getByText('¿Esta seguro que quiere borrar este documento?')
+ ).toBeInTheDocument();
+ expect(atomStore.sub).toHaveBeenCalledTimes(3);
+ expect(t.translation).toBe(undefined);
+ });
+
+ describe('no component', () => {
+ it('should return the translated string and subscribe to the atom store', () => {
+ renderEnvironment(
+ 'System',
+ 'confirmDeleteDocument',
+ 'Are you sure you want to delete this document?',
+ false
+ );
+ expect(
+ renderResult.getByText('¿Esta seguro que quiere borrar este documento?')
+ ).toBeInTheDocument();
+ expect(atomStore.sub).toHaveBeenCalledTimes(4);
+ expect(t.translation).toEqual({
+ contexts: translations[1].contexts,
+ locale: 'es',
+ });
+ });
+
+ it('should update translation when the atom updates', async () => {
+ renderEnvironment(
+ 'System',
+ 'confirmDeleteDocument',
+ 'Are you sure you want to delete this document?',
+ false
+ );
+ expect(
+ renderResult.getByText('¿Esta seguro que quiere borrar este documento?')
+ ).toBeInTheDocument();
+
+ await act(async () => {
+ atomStore.set(translationsAtom, updatedTranslations);
+ });
+
+ expect(t.translation).toEqual({
+ contexts: updatedTranslations[1].contexts,
+ locale: 'es',
+ });
+ });
+ });
+
+ describe('when there is no translation', () => {
+ it('should return the default text', () => {
+ renderEnvironment(
+ 'System',
+ 'confirmDeleteEntity',
+ 'Are you sure you want to delete this entity?',
+ false
+ );
+ expect(
+ renderResult.getByText('Are you sure you want to delete this entity?')
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('only passing context and key', () => {
+ it('should use it as default text', () => {
+ renderEnvironment('System', 'not translated', undefined, false);
+ expect(renderResult.getByText('not translated')).toBeInTheDocument();
+ });
+ });
+
+ describe('when no context', () => {
+ it('should throw an error', () => {
+ spyOn(console, 'warn');
+ renderEnvironment(
+ undefined,
+ 'confirmDeleteEntity',
+ 'Are you sure you want to delete this entity?',
+ false
+ );
+ //eslint-disable-next-line no-console
+ expect(console.warn).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/app/react/I18N/t.js b/app/react/I18N/t.js
deleted file mode 100644
index 1715fba354..0000000000
--- a/app/react/I18N/t.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { store } from 'app/store';
-import React from 'react';
-import translate, { getLocaleTranslation, getContext } from '../../shared/translate';
-import { Translate } from '.';
-
-const testingEnvironment = process.env.NODE_ENV === 'test';
-
-const t = (contextId, key, _text, returnComponent = true) => {
- if (!contextId) {
- // eslint-disable-next-line no-console
- console.warn(`You cannot translate "${key}", because context id is "${contextId}"`);
- }
-
- if (returnComponent && !testingEnvironment) {
- return {key} ;
- }
-
- const text = _text || key;
-
- if (!t.translation) {
- const state = store.getState();
- const translations = state.translations.toJS();
- t.translation = getLocaleTranslation(translations, state.locale);
- }
-
- const context = getContext(t.translation, contextId);
-
- return translate(context, key, text);
-};
-
-t.resetCachedTranslation = () => {
- t.translation = null;
-};
-
-export default t;
diff --git a/app/react/I18N/translateFunction.tsx b/app/react/I18N/translateFunction.tsx
new file mode 100644
index 0000000000..e69713c385
--- /dev/null
+++ b/app/react/I18N/translateFunction.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { atomStore, translationsAtom, localeAtom } from 'V2/atoms';
+import translate, { getLocaleTranslation, getContext } from 'shared/translate';
+import { Translate } from './Translate';
+
+//return type as any since there is no way to create conditional returns based on parameters
+interface TranslationFunction {
+ (contextId?: string, key?: string, text?: string | null, returnComponent?: boolean): any;
+ translation?: string;
+}
+
+const t: TranslationFunction = (contextId, key, text, returnComponent = true) => {
+ let translations;
+ let locale;
+
+ if (!contextId) {
+ // eslint-disable-next-line no-console
+ console.warn(`You cannot translate "${key}", because context id is "${contextId}"`);
+ }
+
+ if (returnComponent) {
+ return {key} ;
+ }
+
+ const updateTranslations = () => {
+ translations = atomStore.get(translationsAtom);
+ locale = atomStore.get(localeAtom);
+ t.translation = getLocaleTranslation(translations, locale);
+ return { translations, locale };
+ };
+
+ updateTranslations();
+
+ atomStore.sub(translationsAtom, () => {
+ updateTranslations();
+ });
+
+ const context = getContext(t.translation, contextId);
+
+ return translate(context, key, text || key);
+};
+
+export { t };
diff --git a/app/react/Layout/DocumentLanguage.js b/app/react/Layout/DocumentLanguage.js
index de5e20ae3a..58984bb281 100644
--- a/app/react/Layout/DocumentLanguage.js
+++ b/app/react/Layout/DocumentLanguage.js
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { LanguageUtils } from 'shared/language';
-import t from '../I18N/t';
+import { t } from 'app/I18N';
export class DocumentLanguage extends Component {
render() {
diff --git a/app/react/Layout/ItemSnippet.js b/app/react/Layout/ItemSnippet.js
index d0e9f239ce..76c0df4434 100644
--- a/app/react/Layout/ItemSnippet.js
+++ b/app/react/Layout/ItemSnippet.js
@@ -5,7 +5,7 @@ import React from 'react';
import SafeHTML from 'app/utils/SafeHTML';
import getFieldLabel from 'app/Templates/utils/getFieldLabel';
-import t from '../I18N/t';
+import { t } from 'app/I18N';
export const ItemSnippet = ({ snippets, onSnippetClick, template }) => {
let content;
diff --git a/app/react/Layout/TemplateLabel.js b/app/react/Layout/TemplateLabel.js
index d53c110cd9..0dbc514aa2 100644
--- a/app/react/Layout/TemplateLabel.js
+++ b/app/react/Layout/TemplateLabel.js
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { COLORS } from 'app/utils/colors';
-import t from '../I18N/t';
+import { t } from 'app/I18N';
const getTemplateInfo = createSelector(
s => s.templates,
diff --git a/app/react/Layout/specs/DocumentLanguage.spec.js b/app/react/Layout/specs/DocumentLanguage.spec.js
index ae7b446cc8..ac2678397b 100644
--- a/app/react/Layout/specs/DocumentLanguage.spec.js
+++ b/app/react/Layout/specs/DocumentLanguage.spec.js
@@ -3,6 +3,11 @@ import { shallow } from 'enzyme';
import { fromJS as Immutable } from 'immutable';
import { DocumentLanguage, mapStateToProps } from '../DocumentLanguage';
+jest.mock('app/I18N', () => ({
+ t: (_context, key) => key,
+ Translate: ({ children }) => children,
+}));
+
describe('DocumentLanguage', () => {
let component;
let props;
diff --git a/app/react/Layout/specs/__snapshots__/BackButton.spec.js.snap b/app/react/Layout/specs/__snapshots__/BackButton.spec.js.snap
index aec210e42c..3a76161b24 100644
--- a/app/react/Layout/specs/__snapshots__/BackButton.spec.js.snap
+++ b/app/react/Layout/specs/__snapshots__/BackButton.spec.js.snap
@@ -11,7 +11,11 @@ exports[`Icon should render the back button to the provided url 1`] = `
- Back
+
+ Back
+
`;
diff --git a/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap b/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap
index 2790d4f7f9..f4e52c24eb 100644
--- a/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap
+++ b/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap
@@ -9,14 +9,14 @@ exports[`ConfirmModal should render a confirm modal 1`] = `
>
-
+
Confirm action
-
+
-
+
Are you sure you want to continue?
-
+
@@ -25,18 +25,18 @@ exports[`ConfirmModal should render a confirm modal 1`] = `
onClick={[Function]}
type="button"
>
-
+
Cancel
-
+
-
+
Accept
-
+
diff --git a/app/react/Layout/specs/__snapshots__/ItemSnippet.spec.js.snap b/app/react/Layout/specs/__snapshots__/ItemSnippet.spec.js.snap
index d22facb5c7..28767ea691 100644
--- a/app/react/Layout/specs/__snapshots__/ItemSnippet.spec.js.snap
+++ b/app/react/Layout/specs/__snapshots__/ItemSnippet.spec.js.snap
@@ -7,7 +7,11 @@ exports[`ItemSnippet should first metadata snippet if exists 1`] = `
- Title
+
+ Title
+
- Show more
+
+ Show more
+
@@ -34,7 +42,11 @@ exports[`ItemSnippet should first metadata snippet if metadata only has metadata
- Summary
+
+ Summary
+
- Show more
+
+ Show more
+
@@ -61,7 +77,11 @@ exports[`ItemSnippet should first metadata snippet when there are no document co
- Title
+
+ Title
+
- Show more
+
+ Show more
+
@@ -88,7 +112,11 @@ exports[`ItemSnippet should first title snippet if metadata only has title snipp
- Title
+
+ Title
+
- Show more
+
+ Show more
+
@@ -115,7 +147,11 @@ exports[`ItemSnippet should show first document snippet if there are not metadat
- Document contents
+
+ Document contents
+
- Show more
+
+ Show more
+
diff --git a/app/react/Library/actions/exportActions.ts b/app/react/Library/actions/exportActions.ts
index 8eda955c04..b688f4d468 100644
--- a/app/react/Library/actions/exportActions.ts
+++ b/app/react/Library/actions/exportActions.ts
@@ -72,9 +72,11 @@ const requestHandler = (
.catch(err => {
clearState(dispatch);
if (err.status === 403) {
- dispatch(notify(t('System', 'Invalid captcha'), 'danger'));
+ dispatch(notify(t('System', 'Invalid captcha', null, false), 'danger'));
} else {
- dispatch(notify(t('System', 'An error has occurred during data export'), 'danger'));
+ dispatch(
+ notify(t('System', 'An error has occurred during data export', null, false), 'danger')
+ );
}
return err;
});
diff --git a/app/react/Library/components/specs/DocumentTypesList.spec.js b/app/react/Library/components/specs/DocumentTypesList.spec.js
index cf13ce71d2..a9034c955f 100644
--- a/app/react/Library/components/specs/DocumentTypesList.spec.js
+++ b/app/react/Library/components/specs/DocumentTypesList.spec.js
@@ -2,9 +2,13 @@
import React from 'react';
import { shallow } from 'enzyme';
import Immutable from 'immutable';
-
import { DocumentTypesList } from '../DocumentTypesList';
+jest.mock('app/I18N', () => ({
+ t: (_context, key) => key,
+ Translate: ({ children }) => children,
+}));
+
describe('DocumentTypesList', () => {
let component;
let props;
diff --git a/app/react/Library/components/specs/__snapshots__/Doc.spec.js.snap b/app/react/Library/components/specs/__snapshots__/Doc.spec.js.snap
index bb71456b61..752e0ecaa6 100644
--- a/app/react/Library/components/specs/__snapshots__/Doc.spec.js.snap
+++ b/app/react/Library/components/specs/__snapshots__/Doc.spec.js.snap
@@ -54,6 +54,9 @@ exports[`Doc when target reference is specified should pass the target reference
icon="exchange-alt"
/>
+
@@ -88,6 +91,9 @@ exports[`Doc when target reference is specified should pass the target reference
icon="exchange-alt"
/>
+
diff --git a/app/react/Library/components/specs/__snapshots__/NestedFilter.spec.js.snap b/app/react/Library/components/specs/__snapshots__/NestedFilter.spec.js.snap
index fab308b214..6c7c2c359c 100644
--- a/app/react/Library/components/specs/__snapshots__/NestedFilter.spec.js.snap
+++ b/app/react/Library/components/specs/__snapshots__/NestedFilter.spec.js.snap
@@ -25,9 +25,9 @@ exports[`NestedFilter should render a text filter field with a label and passing
>
-
+
Strict mode
-
+
diff --git a/app/react/Library/components/specs/__snapshots__/QuickLabelHeader.spec.tsx.snap b/app/react/Library/components/specs/__snapshots__/QuickLabelHeader.spec.tsx.snap
index f4c3a77266..999ffe41cf 100644
--- a/app/react/Library/components/specs/__snapshots__/QuickLabelHeader.spec.tsx.snap
+++ b/app/react/Library/components/specs/__snapshots__/QuickLabelHeader.spec.tsx.snap
@@ -18,14 +18,22 @@ exports[`QuickLabelPanel should render with thesaurus 1`] = `
- Back to thesaurus
+
+ Back to thesaurus
+
- Quick labeling for
+
+ Quick labeling for
+
thesaurus1
@@ -44,7 +52,11 @@ exports[`QuickLabelPanel should render without thesaurus 1`] = `
className="content-header-title"
>
- Ooops... please go
+
+ Ooops... please go
+
- Back to thesauri
+
+ Back to thesauri
+
diff --git a/app/react/Library/components/specs/__snapshots__/QuickLabelPanel.spec.tsx.snap b/app/react/Library/components/specs/__snapshots__/QuickLabelPanel.spec.tsx.snap
index 56c9fbcf3d..99b3235532 100644
--- a/app/react/Library/components/specs/__snapshots__/QuickLabelPanel.spec.tsx.snap
+++ b/app/react/Library/components/specs/__snapshots__/QuickLabelPanel.spec.tsx.snap
@@ -16,9 +16,9 @@ exports[`QuickLabelPanel should render correctly 1`] = `
2
-
+
selected
-
+
-
+
Auto-save
-
+
-
Note: Make the sample set of documents for each topic diverse and representative. For example, use various methods to find sample documents and don't just search for the term "education" to find documents for the topic "Education".
-
+
-
+
Return to the thesaurus page when you finished labeling to start learning.
-
+
@@ -80,7 +80,11 @@ exports[`QuickLabelPanel should render correctly 1`] = `
className="title"
>
- Opts
+
+ Opts
+
-
+
View
-
+
`;
@@ -26,9 +26,9 @@ exports[`ViewDocButton should render a view button poiting to the doc url with t
icon="angle-right"
/>
-
+
View
-
+
`;
@@ -42,9 +42,9 @@ exports[`ViewDocButton when targetReference is provided should render view butto
icon="angle-right"
/>
-
+
View
-
+
`;
@@ -58,8 +58,8 @@ exports[`ViewDocButton when targetReference is provided should render view butto
icon="angle-right"
/>
-
+
View
-
+
`;
diff --git a/app/react/Markdown/components/specs/EntityData.spec.tsx b/app/react/Markdown/components/specs/EntityData.spec.tsx
index adef62f2e3..72b14bdda4 100644
--- a/app/react/Markdown/components/specs/EntityData.spec.tsx
+++ b/app/react/Markdown/components/specs/EntityData.spec.tsx
@@ -4,7 +4,9 @@
import React, { act } from 'react';
import { screen, RenderResult } from '@testing-library/react';
import { renderConnectedContainer } from 'app/utils/test/renderConnected';
-import { state } from './fixture/state';
+import { TestAtomStoreProvider } from 'V2/testing';
+import { localeAtom, translationsAtom } from 'V2/atoms';
+import { state, translations } from './fixture/state';
import { EntityData, EntityDataProps } from '../EntityData';
describe('EntityData Markdown', () => {
@@ -25,7 +27,17 @@ describe('EntityData Markdown', () => {
const render = async (props: EntityDataProps) => {
await act(async () => {
- ({ renderResult } = renderConnectedContainer( , () => state));
+ ({ renderResult } = renderConnectedContainer(
+
+
+ ,
+ () => state
+ ));
});
};
diff --git a/app/react/Markdown/components/specs/__snapshots__/ContactForm.spec.js.snap b/app/react/Markdown/components/specs/__snapshots__/ContactForm.spec.js.snap
index 329520e05d..3542446a4b 100644
--- a/app/react/Markdown/components/specs/__snapshots__/ContactForm.spec.js.snap
+++ b/app/react/Markdown/components/specs/__snapshots__/ContactForm.spec.js.snap
@@ -31,9 +31,9 @@ exports[`ContactForm should render the ContactForm 1`] = `
className="form-group-label"
htmlFor="name"
>
-
+
Name
-
+
@@ -57,9 +57,9 @@ exports[`ContactForm should render the ContactForm 1`] = `
className="form-group-label"
htmlFor="email"
>
-
+
Email
-
+
@@ -83,9 +83,9 @@ exports[`ContactForm should render the ContactForm 1`] = `
className="form-group-label"
htmlFor="message"
>
-
+
Message
-
+
@@ -115,9 +115,9 @@ exports[`ContactForm should render the ContactForm 1`] = `
-
+
Send
-
+
diff --git a/app/react/Markdown/components/specs/__snapshots__/MarkdownMedia.spec.tsx.snap b/app/react/Markdown/components/specs/__snapshots__/MarkdownMedia.spec.tsx.snap
index a4af7d19a6..ea160de429 100644
--- a/app/react/Markdown/components/specs/__snapshots__/MarkdownMedia.spec.tsx.snap
+++ b/app/react/Markdown/components/specs/__snapshots__/MarkdownMedia.spec.tsx.snap
@@ -217,14 +217,14 @@ exports[`MarkdownMedia render uploaded files should render the edition mode 1`]
type="button"
>
Add timelink
Timelinks
diff --git a/app/react/Markdown/components/specs/fixture/state.ts b/app/react/Markdown/components/specs/fixture/state.ts
index ad60a33d94..3672ab2b40 100644
--- a/app/react/Markdown/components/specs/fixture/state.ts
+++ b/app/react/Markdown/components/specs/fixture/state.ts
@@ -40,20 +40,19 @@ const state = {
},
]),
thesauris: Immutable.fromJS([{}]),
- translations: Immutable.fromJS([
- {
- locale: 'en',
- contexts: [
- {
- id: 't1',
- values: { Title: 'Title translated', 'Main Image': 'Main Image translated' },
- },
- ],
- },
- ]),
settings: { collection: Immutable.fromJS({ newNameGeneration: true }) },
- inlineEdit: Immutable.fromJS({ inlineEdit: false }),
- locale: 'en',
};
-export { state };
+const translations = [
+ {
+ locale: 'en',
+ contexts: [
+ {
+ id: 't1',
+ values: { Title: 'Title translated', 'Main Image': 'Main Image translated' },
+ },
+ ],
+ },
+];
+
+export { state, translations };
diff --git a/app/react/Markdown/specs/__snapshots__/MarkdownViewer.spec.js.snap b/app/react/Markdown/specs/__snapshots__/MarkdownViewer.spec.js.snap
index 79f670d9ca..8db0544671 100644
--- a/app/react/Markdown/specs/__snapshots__/MarkdownViewer.spec.js.snap
+++ b/app/react/Markdown/specs/__snapshots__/MarkdownViewer.spec.js.snap
@@ -306,11 +306,11 @@ exports[`MarkdownViewer render should render customHook components and show an e
-
Custom component markup error: unsupported values! Please check your configuration
-
+
diff --git a/app/react/Metadata/components/specs/__snapshots__/GeolocationViewer.spec.js.snap b/app/react/Metadata/components/specs/__snapshots__/GeolocationViewer.spec.js.snap
index 78f252d949..32d98e666a 100644
--- a/app/react/Metadata/components/specs/__snapshots__/GeolocationViewer.spec.js.snap
+++ b/app/react/Metadata/components/specs/__snapshots__/GeolocationViewer.spec.js.snap
@@ -12,15 +12,15 @@ exports[`GeolocationViewer should not fail if points is just an empty array 1`]
className="print-view-alt"
>
-
+
Latitude
-
+
:
-
+
Longitude
-
+
:
@@ -66,16 +66,16 @@ exports[`GeolocationViewer should render a map with markers when onlyForCards is
className="print-view-alt"
>
-
+
Latitude
-
+
:
13
-
+
Longitude
-
+
:
7
diff --git a/app/react/Metadata/components/specs/__snapshots__/IconField.spec.js.snap b/app/react/Metadata/components/specs/__snapshots__/IconField.spec.js.snap
index 16586ee119..ebd6b15b40 100644
--- a/app/react/Metadata/components/specs/__snapshots__/IconField.spec.js.snap
+++ b/app/react/Metadata/components/specs/__snapshots__/IconField.spec.js.snap
@@ -7,9 +7,9 @@ exports[`IconField should be open when has value 1`] = `
-
+
remove icon
-
+
@@ -19,9 +19,9 @@ exports[`IconField should be open when has value 1`] = `
open={true}
showLabel={
-
+
add icon
-
+
@@ -33,13 +33,13 @@ exports[`IconField should be open when has value 1`] = `
>
-
+
Icon
-
+
/
-
+
Flag
-
+
-
+
remove icon
-
+
@@ -73,9 +73,9 @@ exports[`IconField should render IconSelector with toggleDisplay 1`] = `
open={false}
showLabel={
-
+
add icon
-
+
@@ -87,13 +87,13 @@ exports[`IconField should render IconSelector with toggleDisplay 1`] = `
>
-
+
Icon
-
+
/
-
+
Flag
-
+
- Label
+
+ Label
+
- Geolocation Label
+
+ Geolocation Label
+
- label array
+
+ label array
+
- Link
+
+ Link
+
- Media Label
+
+ Media Label
+
- Image Label
+
+ Image Label
+
- label array
+
+ label array
+
- label array
+
+ label array
+
- withUrl
+
+ withUrl
+
- withUrl
+
+ withUrl
+
- metadata without property
+
+ metadata without property
+
- No property
+
+ No property
+
`;
@@ -330,7 +378,11 @@ exports[`Metadata should render sorted property with sorted styles 1`] = `
- sortedBy
+
+ sortedBy
+
- Label
+
+ Label
+
- label
+
+ label
+
- label
+
+ label
+
- label array
+
+ label array
+
+ >
+
+
diff --git a/app/react/Relationships/components/specs/LoadMoreRelationshipsButton.spec.js b/app/react/Relationships/components/specs/LoadMoreRelationshipsButton.spec.js
index e7dd43429b..166dfd263d 100644
--- a/app/react/Relationships/components/specs/LoadMoreRelationshipsButton.spec.js
+++ b/app/react/Relationships/components/specs/LoadMoreRelationshipsButton.spec.js
@@ -1,9 +1,13 @@
import React from 'react';
import { shallow } from 'enzyme';
import { fromJS } from 'immutable';
-
import { LoadMoreRelationshipsButton, mapStateToProps } from '../LoadMoreRelationshipsButton';
+jest.mock('app/I18N', () => ({
+ t: (_context, key) => key,
+ Translate: ({ children }) => children,
+}));
+
describe('LoadMoreRelationshipsButton', () => {
let component;
let props;
diff --git a/app/react/Relationships/components/specs/RelationshipsFormButtons.spec.tsx b/app/react/Relationships/components/specs/RelationshipsFormButtons.spec.tsx
index a7f54e1c7c..559dfefe31 100644
--- a/app/react/Relationships/components/specs/RelationshipsFormButtons.spec.tsx
+++ b/app/react/Relationships/components/specs/RelationshipsFormButtons.spec.tsx
@@ -78,7 +78,7 @@ describe('RelationshipsFormButtons', () => {
render(user, { editing: false });
const buttons = component.find('button');
expect(buttons.length).toBe(1);
- expect(buttons.at(0).find('span').text()).toEqual('Edit');
+ expect(buttons.at(0).find('span.translation').text()).toEqual('Edit');
}
);
it.each([editorUser, adminUser, collaboratorUser])(
@@ -87,8 +87,8 @@ describe('RelationshipsFormButtons', () => {
render(user, { editing: true });
const buttons = component.find('button');
expect(buttons.length).toBe(2);
- expect(buttons.at(0).find('span').text()).toEqual('Cancel');
- expect(buttons.at(1).find('span').text()).toEqual('Save');
+ expect(buttons.at(0).find('span.translation').text()).toEqual('Cancel');
+ expect(buttons.at(1).find('span.translation').text()).toEqual('Save');
}
);
});
diff --git a/app/react/Relationships/components/specs/__snapshots__/HubRelationshipMetadata.spec.js.snap b/app/react/Relationships/components/specs/__snapshots__/HubRelationshipMetadata.spec.js.snap
index 9fd7bd5a97..54df749c77 100644
--- a/app/react/Relationships/components/specs/__snapshots__/HubRelationshipMetadata.spec.js.snap
+++ b/app/react/Relationships/components/specs/__snapshots__/HubRelationshipMetadata.spec.js.snap
@@ -91,7 +91,11 @@ exports[`HubRelationshipMetadata should render the metadata correctly when text
key="text"
>
- Text
+
+ Text
+
- Back to
+
+ Back to
+
'Thes'
@@ -29,7 +33,11 @@ exports[`EntityViewer should render 1 1`] = `
- Documents including suggestion:
+
+ Documents including suggestion:
+
'Topic1'
@@ -39,13 +47,21 @@ exports[`EntityViewer should render 1 1`] = `
/>
- Document
+
+ Document
+
4
- of
+
+ of
+
>39
@@ -87,19 +103,31 @@ exports[`EntityViewer should render 2 1`] = `
- Documents for custom filter
+
+ Documents for custom filter
+
- Document
+
+ Document
+
1
- of
+
+ of
+
10
diff --git a/app/react/SemanticSearch/components/specs/__snapshots__/DocumentResults.spec.js.snap b/app/react/SemanticSearch/components/specs/__snapshots__/DocumentResults.spec.js.snap
index 4f73e1727a..16b2742933 100644
--- a/app/react/SemanticSearch/components/specs/__snapshots__/DocumentResults.spec.js.snap
+++ b/app/react/SemanticSearch/components/specs/__snapshots__/DocumentResults.spec.js.snap
@@ -59,9 +59,9 @@ exports[`DocumentResults render should render results summary and snippets above
-
+
Threshold
-
+
30.00
%
@@ -73,9 +73,21 @@ exports[`DocumentResults render should render results summary and snippets above
+ Precision
+
+ }
min={0.3}
- minLabel="Exploration"
+ minLabel={
+
+ Exploration
+
+ }
model=".threshold"
step={0.01}
/>
@@ -86,9 +98,9 @@ exports[`DocumentResults render should render results summary and snippets above
className="metadata-type-numeric"
>
-
+
Number of sentences above threshold
-
+
2
@@ -98,9 +110,9 @@ exports[`DocumentResults render should render results summary and snippets above
className="metadata-type-numeric"
>
-
+
% of sentences above threshold
-
+
50.00
diff --git a/app/react/SemanticSearch/components/specs/__snapshots__/ResultsFiltersPanel.spec.js.snap b/app/react/SemanticSearch/components/specs/__snapshots__/ResultsFiltersPanel.spec.js.snap
index a87445724d..030d32035e 100644
--- a/app/react/SemanticSearch/components/specs/__snapshots__/ResultsFiltersPanel.spec.js.snap
+++ b/app/react/SemanticSearch/components/specs/__snapshots__/ResultsFiltersPanel.spec.js.snap
@@ -11,7 +11,11 @@ exports[`ResultsFiltersPanel render should render search filters and instruction
- Fine tune
+
+ Fine tune
+
- Threshold
+
+ Threshold
+
80.00%
@@ -32,9 +40,21 @@ exports[`ResultsFiltersPanel render should render search filters and instruction
+ Precision
+
+ }
min={0.3}
- minLabel="Exploration"
+ minLabel={
+
+ Exploration
+
+ }
model=".threshold"
prefix="threshold"
step={0.01}
@@ -46,16 +66,32 @@ exports[`ResultsFiltersPanel render should render search filters and instruction
key="Minimum relevant sentences per document"
>
- Minimum relevant sentences per document
+
+ Minimum relevant sentences per document
+
+
+
+ }
min={1}
- minLabel=""
+ minLabel={
+
+
+
+ }
model=".minRelevantSentences"
prefix="minRelevantSentences"
step={1}
@@ -75,25 +111,25 @@ exports[`ResultsFiltersPanel render should render search filters and instruction
size="2x"
/>
-
Semantic search is a technique to provide contextual results. Its ability to capture concepts and word associations in human language enables the retrieval of related information such as synonyms, connected categories or entities, etc. .
-
+
-
The threshold determines how close the results match the search concept. Move the slider to the right to narrow down the concept of the search query. The obtained results will be more precise. Move the slider to the left to more broaden the concept and explore related content.
-
+
-
Semantic search is applied to each sentence in a document. Filter the documents by the minimum number of sentences that exceed the threshold.
-
+
diff --git a/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchMultieditPanel.spec.js.snap b/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchMultieditPanel.spec.js.snap
index 628dcd9b04..5c362bf7b7 100644
--- a/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchMultieditPanel.spec.js.snap
+++ b/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchMultieditPanel.spec.js.snap
@@ -51,17 +51,17 @@ exports[`SemanticSearchMultieditPanel open should not open side panel if there a
size="2x"
/>
-
+
Warning: you are editing multiple entities. Fields marked with a
-
+
-
+
will be updated with the same value.
-
+
- Cancel
+
+ Cancel
+
- Save
+
+ Save
+
@@ -153,17 +161,17 @@ exports[`SemanticSearchMultieditPanel should render multi edit form for semantic
size="2x"
/>
-
+
Warning: you are editing multiple entities. Fields marked with a
-
+
-
+
will be updated with the same value.
-
+
- Cancel
+
+ Cancel
+
- Save
+
+ Save
+
diff --git a/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchResults.spec.js.snap b/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchResults.spec.js.snap
index 46235b5664..a55ad71855 100644
--- a/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchResults.spec.js.snap
+++ b/app/react/SemanticSearch/components/specs/__snapshots__/SemanticSearchResults.spec.js.snap
@@ -17,9 +17,9 @@ exports[`SemanticSearchResults should render results in ItemList 1`] = `
>
-
+
Semantic search
-
+
:
-
+
Edit all documents matching this criteria
-
+
-
+
documents
-
+
@@ -66,16 +66,16 @@ exports[`SemanticSearchResults should render results in ItemList 1`] = `
className="metadata-type-text"
>
-
+
Sentences above threshold
-
+
2
-
+
out of
-
+
(
50.00
@@ -117,16 +117,16 @@ exports[`SemanticSearchResults should render results in ItemList 1`] = `
className="metadata-type-text"
>
-
+
Sentences above threshold
-
+
1
-
+
out of
-
+
(
40.00
@@ -169,18 +169,18 @@ exports[`SemanticSearchResults should render results in ItemList 1`] = `
-
+
of
-
+
0
-
+
documents
-
+
-
+
x more
-
+
@@ -215,9 +215,9 @@ exports[`SemanticSearchResults when the search is empty should render not found
className="row panels-layout"
>
-
+
Search not found
-
+
({
...thesaurus,
- name: t(thesaurus._id, thesaurus.name, null, false),
+ name: t(thesaurus._id!.toString(), thesaurus.name, null, false),
})),
'name'
);
diff --git a/app/react/Templates/components/specs/FormConfigSelect.spec.tsx b/app/react/Templates/components/specs/FormConfigSelect.spec.tsx
index 8aa0ebf1c6..36c8b9e53b 100644
--- a/app/react/Templates/components/specs/FormConfigSelect.spec.tsx
+++ b/app/react/Templates/components/specs/FormConfigSelect.spec.tsx
@@ -3,14 +3,41 @@
*/
import React from 'react';
import Immutable from 'immutable';
-import { screen, RenderResult } from '@testing-library/react';
+import { screen, RenderResult, act } from '@testing-library/react';
import { Provider } from 'react-redux';
+import { Provider as AtomProvider } from 'jotai';
import { MockStoreEnhanced } from 'redux-mock-store';
import { defaultState, renderConnectedContainer } from 'app/utils/test/renderConnected';
-import { store } from 'app/store';
-import { t } from 'app/I18N';
+import { atomStore, localeAtom, translationsAtom } from 'V2/atoms';
+import { ClientTranslationSchema } from 'app/istore';
import { FormConfigSelect } from '../FormConfigSelect';
+const translations: ClientTranslationSchema[] = [
+ {
+ locale: 'es',
+ contexts: [
+ {
+ _id: '1',
+ id: '1',
+ label: 'Thesauri 1',
+ values: {
+ 'Thesauri 1': 'Diccionario B',
+ },
+ type: 'Thesaurus',
+ },
+ {
+ _id: '2',
+ id: '2',
+ label: 'Thesauri 2',
+ values: {
+ 'Thesauri 2': 'Diccionario A',
+ },
+ type: 'Thesaurus',
+ },
+ ],
+ },
+];
+
const defineTemplateInStore = (
content: string,
_id?: string,
@@ -36,11 +63,11 @@ describe('FormConfigSelect', () => {
let renderResult: RenderResult;
let reduxStore: MockStoreEnhanced;
let state: any;
+ let locale = 'en';
beforeEach(() => {
state = {
...defaultState,
- locale: 'en',
template: { ...defineTemplateInStore('1', 'id1') },
thesauris: Immutable.fromJS([
{ _id: '1', values: [], name: 'Thesauri 1' },
@@ -49,37 +76,17 @@ describe('FormConfigSelect', () => {
templates: Immutable.fromJS([
{ properties: [{ content: '1', type: 'select' }], name: 'template1' },
]),
- translations: Immutable.fromJS([
- {
- locale: 'es',
- contexts: [
- {
- _id: '1',
- id: '1',
- label: 'Thesauri 1',
- values: {
- 'Thesauri 1': 'Diccionario B',
- },
- type: 'Thesaurus',
- },
- {
- _id: '2',
- id: '2',
- label: 'Thesauri 2',
- values: {
- 'Thesauri 2': 'Diccionario A',
- },
- type: 'Thesaurus',
- },
- ],
- },
- ]),
};
});
const render = () => {
+ atomStore.set(translationsAtom, translations);
+ atomStore.set(localeAtom, locale);
+
({ store: reduxStore, renderResult } = renderConnectedContainer(
- ,
+
+
+ ,
() => state
));
};
@@ -92,9 +99,7 @@ describe('FormConfigSelect', () => {
});
it('should render the select with the sorted translated dictionaries', () => {
- state.locale = 'es';
- jest.spyOn(store!, 'getState').mockImplementationOnce(() => ({ ...state }));
- t.resetCachedTranslation();
+ locale = 'es';
render();
const options = screen.getAllByText('Diccionario', { exact: false });
@@ -103,17 +108,17 @@ describe('FormConfigSelect', () => {
});
describe('validation', () => {
- it('should show a warning when changing the select thesaurus', () => {
- t.resetCachedTranslation();
+ it('should show a warning when changing the select thesaurus', async () => {
render();
state = { ...state, template: { ...defineTemplateInStore('2', 'id1') } };
- renderResult.rerender(
-
-
-
- );
+ await act(() => {
+ reduxStore.dispatch({
+ type: 'rrf/change',
+ value: 'id1',
+ });
+ });
const warning = screen.queryByText(
'By making this change, any values from the previous thesaurus',
diff --git a/app/react/Templates/components/specs/TemplateAsPageControl.spec.tsx b/app/react/Templates/components/specs/TemplateAsPageControl.spec.tsx
index 84380c3862..ff6148d33b 100644
--- a/app/react/Templates/components/specs/TemplateAsPageControl.spec.tsx
+++ b/app/react/Templates/components/specs/TemplateAsPageControl.spec.tsx
@@ -10,6 +10,11 @@ import { TemplateAsPageControl } from '../TemplateAsPageControl';
const middlewares = [thunk];
+jest.mock('app/I18N', () => ({
+ t: (_context: string, key: string) => key,
+ Translate: ({ children }: { children: React.ReactElement }) => children,
+}));
+
describe('TemplateAsPageControl', () => {
const mockStoreCreator: MockStoreCreator = configureStore(middlewares);
let component: ShallowWrapper;
diff --git a/app/react/Templates/components/specs/__snapshots__/FormConfigMultimedia.spec.js.snap b/app/react/Templates/components/specs/__snapshots__/FormConfigMultimedia.spec.js.snap
index 0a7333779f..0dd51cf8d0 100644
--- a/app/react/Templates/components/specs/__snapshots__/FormConfigMultimedia.spec.js.snap
+++ b/app/react/Templates/components/specs/__snapshots__/FormConfigMultimedia.spec.js.snap
@@ -6,9 +6,9 @@ exports[`FormConfigMultimedia should allow excluding "required" 1`] = `
className="form-group"
>
-
+
Name
-
+
-
+
This property will be shown without the label.
-
+
-
+
This property will be shown using all the width available.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
@@ -53,9 +53,9 @@ exports[`FormConfigMultimedia should allow excluding "required" 1`] = `
className="form-group"
>
-
+
Style
-
+
-
+
Fit
-
+
-
will show the entire media inside the container.
-
+
-
+
Fill
-
+
-
will attempt to fill the container, using its entire width. In cards, cropping is likely to occur.
-
+
@@ -117,9 +117,9 @@ exports[`FormConfigMultimedia should allow excluding "show in card" 1`] = `
className="form-group"
>
-
+
Name
-
+
-
+
This property will be shown without the label.
-
+
-
+
This property will be shown using all the width available.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
@@ -166,9 +166,9 @@ exports[`FormConfigMultimedia should allow excluding "show in card" 1`] = `
className="form-group"
>
-
+
Style
-
+
-
+
Fit
-
+
-
will show the entire media inside the container.
-
+
-
+
Fill
-
+
-
will attempt to fill the container, using its entire width. In cards, cropping is likely to occur.
-
+
@@ -230,9 +230,9 @@ exports[`FormConfigMultimedia should allow excluding "style" 1`] = `
className="form-group"
>
-
+
Name
-
+
-
+
This property will be shown without the label.
-
+
-
+
This property will be shown using all the width available.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
+
Name
-
+
-
+
This property will be shown without the label.
-
+
-
+
This property will be shown using all the width available.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
@@ -365,9 +365,9 @@ exports[`FormConfigMultimedia should allow setting a help text 1`] = `
className="form-group"
>
-
+
Style
-
+
-
+
Fit
-
+
-
will show the entire media inside the container.
-
+
-
+
Fill
-
+
-
will attempt to fill the container, using its entire width. In cards, cropping is likely to occur.
-
+
@@ -429,9 +429,9 @@ exports[`FormConfigMultimedia should hold show label, show in card and select ca
className="form-group"
>
-
+
Name
-
+
-
+
This property will be shown without the label.
-
+
-
+
This property will be shown using all the width available.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
@@ -488,9 +488,9 @@ exports[`FormConfigMultimedia should hold show label, show in card and select ca
className="form-group"
>
-
+
Style
-
+
-
+
Fit
-
+
-
will show the entire media inside the container.
-
+
-
+
Fill
-
+
-
will attempt to fill the container, using its entire width. In cards, cropping is likely to occur.
-
+
diff --git a/app/react/Templates/components/specs/__snapshots__/FormConfigRelationship.spec.js.snap b/app/react/Templates/components/specs/__snapshots__/FormConfigRelationship.spec.js.snap
index 40a1358af8..fc256811f7 100644
--- a/app/react/Templates/components/specs/__snapshots__/FormConfigRelationship.spec.js.snap
+++ b/app/react/Templates/components/specs/__snapshots__/FormConfigRelationship.spec.js.snap
@@ -8,9 +8,9 @@ exports[`FormConfigRelationship when the fields are invalid and dirty or the for
-
+
Label
-
+
-
+
Relationship
-
+
@@ -61,9 +61,9 @@ exports[`FormConfigRelationship when the fields are invalid and dirty or the for
className="form-group"
>
-
+
Entities
-
+
-
+
Label
-
+
-
+
Relationship
-
+
@@ -151,9 +151,9 @@ exports[`FormConfigRelationship when the fields are invalid and dirty or the for
className="form-group"
>
-
+
Entities
-
+
-
+
Label
-
+
-
+
Relationship
-
+
@@ -241,9 +241,9 @@ exports[`FormConfigRelationship when use as filter is selected should show the d
className="form-group"
>
-
+
Entities
-
+
-
+
Duplicated label
-
+
`;
@@ -21,10 +21,10 @@ exports[`MetadataProperty simple component when type is custom type errors shoul
-
Cannot use 'any entity or document' if another relationship of the same type is already with a specific entity.
-
+
`;
diff --git a/app/react/Templates/components/specs/__snapshots__/PropertyConfigOption.spec.js.snap b/app/react/Templates/components/specs/__snapshots__/PropertyConfigOption.spec.js.snap
index 312b4848d7..76f253b03b 100644
--- a/app/react/Templates/components/specs/__snapshots__/PropertyConfigOption.spec.js.snap
+++ b/app/react/Templates/components/specs/__snapshots__/PropertyConfigOption.spec.js.snap
@@ -13,11 +13,11 @@ exports[`Tip should render children 1`] = `
type="checkbox"
/>
-
label
-
+
children text!
@@ -36,11 +36,11 @@ exports[`Tip should render label and input for the model 1`] = `
type="checkbox"
/>
-
label
-
+
`;
diff --git a/app/react/Templates/components/specs/__snapshots__/PropertyConfigOptions.spec.js.snap b/app/react/Templates/components/specs/__snapshots__/PropertyConfigOptions.spec.js.snap
index 7d57176247..eac9980daa 100644
--- a/app/react/Templates/components/specs/__snapshots__/PropertyConfigOptions.spec.js.snap
+++ b/app/react/Templates/components/specs/__snapshots__/PropertyConfigOptions.spec.js.snap
@@ -7,9 +7,9 @@ exports[`PropertyConfigOptions Additional options should allow to exclude the "u
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
-
Use this property as a default filter in the library. When there are no entity types selected, this property will show as a default filter for your collection.
-
+
-
Properties marked as priority sorting will be used as default sorting criteria. If more than one property is marked as priority sorting the system will try to pick-up the best fit. When listing mixed template types, the system will pick-up the best combined priority sorting.
-
+
@@ -127,9 +127,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is n
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
@@ -183,9 +183,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is n
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
@@ -239,9 +239,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is n
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
@@ -295,9 +295,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is n
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
@@ -351,9 +351,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is t
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
-
Use this property as a default filter in the library. When there are no entity types selected, this property will show as a default filter for your collection.
-
+
-
Properties marked as priority sorting will be used as default sorting criteria. If more than one property is marked as priority sorting the system will try to pick-up the best fit. When listing mixed template types, the system will pick-up the best combined priority sorting.
-
+
@@ -431,9 +431,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is t
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
-
Use this property as a default filter in the library. When there are no entity types selected, this property will show as a default filter for your collection.
-
+
-
Properties marked as priority sorting will be used as default sorting criteria. If more than one property is marked as priority sorting the system will try to pick-up the best fit. When listing mixed template types, the system will pick-up the best combined priority sorting.
-
+
@@ -511,9 +511,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is t
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
-
Use this property as a default filter in the library. When there are no entity types selected, this property will show as a default filter for your collection.
-
+
-
Properties marked as priority sorting will be used as default sorting criteria. If more than one property is marked as priority sorting the system will try to pick-up the best fit. When listing mixed template types, the system will pick-up the best combined priority sorting.
-
+
@@ -591,9 +591,9 @@ exports[`PropertyConfigOptions priority sorting option when property filter is t
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
-
Use this property as a default filter in the library. When there are no entity types selected, this property will show as a default filter for your collection.
-
+
-
Properties marked as priority sorting will be used as default sorting criteria. If more than one property is marked as priority sorting the system will try to pick-up the best fit. When listing mixed template types, the system will pick-up the best combined priority sorting.
-
+
@@ -671,9 +671,9 @@ exports[`PropertyConfigOptions should render fields with the correct datas 1`] =
model="template.data.properties[2].noLabel"
>
-
+
This property will be shown without the label.
-
+
-
You won't be able to save an entity if this property is empty.
-
+
-
+
This property will appear in the library cards as part of the basic info.
-
+
-
This property will be used for filtering the library results. When properties match in equal name and field type with other entity types, they will be combined for filtering.
-
+
diff --git a/app/react/Templates/components/specs/__snapshots__/TemplateCreator.spec.js.snap b/app/react/Templates/components/specs/__snapshots__/TemplateCreator.spec.js.snap
index 361e58720a..c6fdeff598 100644
--- a/app/react/Templates/components/specs/__snapshots__/TemplateCreator.spec.js.snap
+++ b/app/react/Templates/components/specs/__snapshots__/TemplateCreator.spec.js.snap
@@ -10,9 +10,9 @@ exports[`TemplateCreator Property Options should include document specific optio
-
+
Metadata creator
-
+
-
+
Properties
-
+
-
Relationship fields can not be added until you have at least one relationship type to select.
-
+
@@ -122,9 +122,9 @@ exports[`TemplateCreator Property Options should include most common options by
-
+
Metadata creator
-
+
-
+
Properties
-
+
-
Relationship fields can not be added until you have at least one relationship type to select.
-
+
@@ -234,9 +234,9 @@ exports[`TemplateCreator Property Options should remove all options for relation
-
+
Metadata creator
-
+
{
it('should work when template is an Immutable instance', () => {
template = Immutable.fromJS(template);
field = 'title';
- expect(runGetLabel()).toEqual('Name');
+ expect(runGetLabel().props.children).toEqual('Name');
field = 'metadata.prop1';
- expect(runGetLabel()).toEqual('Prop 1');
+ expect(runGetLabel().props.children).toEqual('Prop 1');
field = 'nonexistent';
expect(runGetLabel()).toEqual('nonexistent');
});
diff --git a/app/react/Uploads/components/specs/__snapshots__/ImportPanel.spec.js.snap b/app/react/Uploads/components/specs/__snapshots__/ImportPanel.spec.js.snap
index 63a7c0f37d..bd653a86cf 100644
--- a/app/react/Uploads/components/specs/__snapshots__/ImportPanel.spec.js.snap
+++ b/app/react/Uploads/components/specs/__snapshots__/ImportPanel.spec.js.snap
@@ -30,18 +30,18 @@ exports[`ImportPanel rendering states should render a form 1`] = `
@@ -65,9 +65,9 @@ exports[`ImportPanel rendering states should render a form 1`] = `
-
+
File
-
+
-
+
Template
-
+
-
+
Import
-
+
@@ -171,9 +171,9 @@ exports[`ImportPanel rendering states when the import starts should render an up
-
+
Uploading file
-
+
23
%
@@ -211,9 +211,9 @@ exports[`ImportPanel rendering states when the upload starts should render an up
-
+
Uploading file
-
+
23
%
diff --git a/app/react/Uploads/components/specs/__snapshots__/ImportProgress.spec.js.snap b/app/react/Uploads/components/specs/__snapshots__/ImportProgress.spec.js.snap
index a7e0f79a26..3e47756291 100644
--- a/app/react/Uploads/components/specs/__snapshots__/ImportProgress.spec.js.snap
+++ b/app/react/Uploads/components/specs/__snapshots__/ImportProgress.spec.js.snap
@@ -11,17 +11,17 @@ exports[`ImportProgress rendering states should render a state for end 1`] = `
-
Import completed. Number of entities created:
-
+
5
-
+
Indexing entities may take a few minutes
-
+
-
+
The import process threw an error:
-
+
-
+
The import process threw an error:
-
+
-
+
Importing data in progress
-
+
:
5
diff --git a/app/react/Users/specs/__snapshots__/Login.spec.js.snap b/app/react/Users/specs/__snapshots__/Login.spec.js.snap
index 0b9aaa3717..537997a59a 100644
--- a/app/react/Users/specs/__snapshots__/Login.spec.js.snap
+++ b/app/react/Users/specs/__snapshots__/Login.spec.js.snap
@@ -35,9 +35,9 @@ exports[`Login on instance should render the component with the login form 1`] =
className="form-group-label"
htmlFor="username"
>
-
+
User
-
+
-
+
Password
-
+
-
+
Forgot Password?
-
+
@@ -87,9 +87,9 @@ exports[`Login on instance should render the component with the login form 1`] =
className="btn btn-block btn-lg btn-primary"
type="submit"
>
-
+
Login
-
+
@@ -127,7 +127,11 @@ exports[`Login submit() on response failure when authorization conflict (2fa req
className="form-group login-token"
>
- Two-step verification
+
+ Two-step verification
+
- Authentication code
+
+ Authentication code
+
-
+
Open the two-factor Authenticator app on your device
-
+
-
+
to view your authentication code and verify your identity.
-
+
-
+
Return to login
-
+
@@ -176,9 +184,9 @@ exports[`Login submit() on response failure when authorization conflict (2fa req
className="btn btn-block btn-lg btn-primary"
type="submit"
>
-
+
Verify
-
+
@@ -216,7 +224,11 @@ exports[`Login submit() on response failure when authorization failure should se
className="form-group login-token has-error"
>
- Two-step verification
+
+ Two-step verification
+
- Authentication code
+
+ Authentication code
+
- Two-factor verification failed
+
+ Two-factor verification failed
+
-
+
Open the two-factor Authenticator app on your device
-
+
-
+
to view your authentication code and verify your identity.
-
+
-
+
Return to login
-
+
@@ -272,9 +292,9 @@ exports[`Login submit() on response failure when authorization failure should se
className="btn btn-block btn-lg btn-primary"
type="submit"
>
-
+
Verify
-
+
diff --git a/app/react/V2/Components/Forms/Geolocation.tsx b/app/react/V2/Components/Forms/Geolocation.tsx
index 6cff296396..48f99d7d93 100644
--- a/app/react/V2/Components/Forms/Geolocation.tsx
+++ b/app/react/V2/Components/Forms/Geolocation.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Translate } from 'app/I18N';
import { Map, Layer } from 'app/Map/MapContainer';
-import { Label, InputField } from 'app/V2/Components/Forms';
+import { Label, InputField } from 'V2/Components/Forms';
interface GeolocationProps {
name: string;
diff --git a/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx b/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx
index 3ecb07d0f1..3ccafd4980 100644
--- a/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx
+++ b/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx
@@ -1,8 +1,6 @@
import React from 'react';
import 'cypress-axe';
-import { Provider } from 'react-redux';
import { mount } from '@cypress/react18';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { MultiselectList } from '../MultiselectList';
describe('MultiselectList.cy.tsx', () => {
@@ -58,16 +56,14 @@ describe('MultiselectList.cy.tsx', () => {
beforeEach(() => {
cy.viewport(450, 650);
mount(
-
-
- {
- selected = selectedItems;
- }}
- />
-
-
+
+ {
+ selected = selectedItems;
+ }}
+ />
+
);
});
@@ -145,17 +141,15 @@ describe('MultiselectList.cy.tsx', () => {
cy.viewport(450, 650);
mount(
-
-
- {
- selections.push(...selectedItems);
- }}
- allowSelelectAll
- />
-
-
+
+ {
+ selections.push(...selectedItems);
+ }}
+ allowSelelectAll
+ />
+
);
cy.contains('button', 'Select all').click();
@@ -179,17 +173,15 @@ describe('MultiselectList.cy.tsx', () => {
cy.viewport(450, 650);
mount(
-
-
- {
- selections.push(...selectedItems);
- }}
- allowSelelectAll
- />
-
-
+
+ {
+ selections.push(...selectedItems);
+ }}
+ allowSelelectAll
+ />
+
);
cy.contains('button', 'Select all').click();
@@ -211,11 +203,9 @@ describe('MultiselectList.cy.tsx', () => {
it('should show matching options even when not selected', () => {
cy.viewport(450, 650);
mount(
-
-
- {}} items={pizzas} value={['MGT']} />
-
-
+
+ {}} items={pizzas} value={['MGT']} />
+
);
cy.get('input[type=text]').type('pepperoni');
@@ -228,11 +218,9 @@ describe('MultiselectList.cy.tsx', () => {
it('should show blank state property if there is no items passed to the component', () => {
cy.viewport(450, 650);
mount(
-
-
- {}} items={[]} />
-
-
+
+ {}} items={[]} />
+
);
cy.contains('No items available').should('be.visible');
});
@@ -240,11 +228,9 @@ describe('MultiselectList.cy.tsx', () => {
it('should accept a blank state string', () => {
cy.viewport(450, 650);
mount(
-
-
- {}} items={[]} blankState="nada" />
-
-
+
+ {}} items={[]} blankState="nada" />
+
);
cy.contains('nada').should('be.visible');
});
@@ -252,15 +238,9 @@ describe('MultiselectList.cy.tsx', () => {
it('should accept a blank state component', () => {
cy.viewport(450, 650);
mount(
-
-
-
{}}
- items={[]}
- blankState={no items string
}
- />
-
-
+
+
{}} items={[]} blankState={no items string
} />
+
);
cy.contains('no items string').should('be.visible');
});
diff --git a/app/react/V2/Components/Layouts/SettingsContent.tsx b/app/react/V2/Components/Layouts/SettingsContent.tsx
index 8407bc1ff1..4c61e29905 100644
--- a/app/react/V2/Components/Layouts/SettingsContent.tsx
+++ b/app/react/V2/Components/Layouts/SettingsContent.tsx
@@ -2,7 +2,7 @@
import React, { PropsWithChildren } from 'react';
import { Breadcrumb } from 'flowbite-react';
import { ChevronLeftIcon } from '@heroicons/react/20/solid';
-import { I18NLink, Translate } from 'app/I18N';
+import { Translate, I18NLinkV2 as I18NLink } from 'app/I18N';
interface SettingsContentProps extends PropsWithChildren {
className?: string;
@@ -38,8 +38,10 @@ const SettingsHeader = ({ contextId, title, children, path, className }: Setting
{Array.from(path?.entries() || []).map(([key, value]) => (
-
- {key}
+
+
+ {key}
+
))}
{title !== undefined && (
diff --git a/app/react/V2/Components/Layouts/specs/SettingsContent.cy.tsx b/app/react/V2/Components/Layouts/specs/SettingsContent.cy.tsx
index 40f4b33491..18c9ac9950 100644
--- a/app/react/V2/Components/Layouts/specs/SettingsContent.cy.tsx
+++ b/app/react/V2/Components/Layouts/specs/SettingsContent.cy.tsx
@@ -1,37 +1,33 @@
import React from 'react';
import 'cypress-axe';
import { BrowserRouter } from 'react-router-dom';
-import { Provider } from 'react-redux';
import { mount } from '@cypress/react18';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { SettingsContent } from '../SettingsContent';
describe('ConfirmationModal', () => {
const render = () => {
mount(
-
-
-
-
-
-
- Body
-
-
-
-
-
-
+
+
+
+
+
+ Body
+
+
+
+
+
);
};
@@ -47,7 +43,7 @@ describe('ConfirmationModal', () => {
cy.get('[data-testid="settings-content-header"]')
.invoke('text')
.should('contain', 'Root PathMiddle PathLeafCurrent page');
- cy.get('a[href="/en/settings"]').should('not.be.visible');
+ cy.get('a[href="/settings"]').should('not.be.visible');
cy.contains('a', 'Root Path').invoke('attr', 'href').should('include', '#top');
cy.contains('a', 'Middle Path').invoke('attr', 'href').should('include', '#bottom');
cy.contains('a', 'Leaf').invoke('attr', 'href').should('include', '#footer');
@@ -58,6 +54,6 @@ describe('ConfirmationModal', () => {
it('should have an arrow to return to settings menu for mobile', () => {
cy.viewport(450, 650);
render();
- cy.get('a[href="/en/settings"]').should('be.visible');
+ cy.get('a[href="/settings"]').should('be.visible');
});
});
diff --git a/app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx b/app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx
index 2e6ea167ff..58f85e5763 100644
--- a/app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx
+++ b/app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx
@@ -5,9 +5,8 @@
import React from 'react';
import { render, act, queryAllByAttribute, cleanup, RenderResult } from '@testing-library/react';
import { configMocks, mockIntersectionObserver } from 'jsdom-testing-mocks';
-import { Provider } from 'react-redux';
import { pdfScaleAtom } from 'V2/atoms';
-import { TestAtomStoreProvider, LEGACY_createStore as createStore } from 'V2/testing';
+import { TestAtomStoreProvider } from 'V2/testing';
import PDF, { PDFProps } from '../PDF';
import * as helpers from '../functions/helpers';
@@ -78,11 +77,9 @@ describe('PDF', () => {
const renderComponet = (scrollToPage?: PDFProps['scrollToPage']) => {
renderResult = render(
-
-
-
-
-
+
+
+
);
};
diff --git a/app/react/V2/Components/UI/Modal.tsx b/app/react/V2/Components/UI/Modal.tsx
index 454f9e64dd..4521a00d37 100644
--- a/app/react/V2/Components/UI/Modal.tsx
+++ b/app/react/V2/Components/UI/Modal.tsx
@@ -6,9 +6,10 @@ type modalSizeType = 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
interface ModalProps {
children: string | React.ReactNode;
size: modalSizeType;
+ id?: string;
}
-const Modal = ({ children, size }: ModalProps) => {
+const Modal = ({ children, size, id }: ModalProps) => {
const sizes = {
sm: 'max-w-sm',
md: 'max-w-md min-w-[24rem]',
@@ -25,6 +26,7 @@ const Modal = ({ children, size }: ModalProps) => {
data-testid="modal"
role="dialog"
aria-label="Modal"
+ id={id}
>
{children}
diff --git a/app/react/V2/Components/UI/specs/CopyValueInput.cy.tsx b/app/react/V2/Components/UI/specs/CopyValueInput.cy.tsx
index ad5c286618..e353765e06 100644
--- a/app/react/V2/Components/UI/specs/CopyValueInput.cy.tsx
+++ b/app/react/V2/Components/UI/specs/CopyValueInput.cy.tsx
@@ -1,17 +1,13 @@
import React from 'react';
import 'cypress-axe';
import { mount } from '@cypress/react18';
-import { Provider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { CopyValueInput } from '../CopyValueInput';
describe('CopyValueInput', () => {
const Component = () => (
-
-
-
-
-
+
+
+
);
before(() => {
@@ -41,9 +37,8 @@ describe('CopyValueInput', () => {
cy.get('input').should('have.value', 'some testing value');
cy.get('[data-testid="copy-value-button"]').focus();
cy.get('[data-testid="copy-value-button"]').realClick();
- cy.window() //
+ cy.window()
.then(async win => win.navigator.clipboard.readText())
-
.should('equal', 'some testing value');
});
});
diff --git a/app/react/V2/Components/UI/specs/NotificationsContainer.cy.tsx b/app/react/V2/Components/UI/specs/NotificationsContainer.cy.tsx
index 91de3a2288..d2c5141a5d 100644
--- a/app/react/V2/Components/UI/specs/NotificationsContainer.cy.tsx
+++ b/app/react/V2/Components/UI/specs/NotificationsContainer.cy.tsx
@@ -1,8 +1,6 @@
import React from 'react';
import { Provider, useSetAtom } from 'jotai';
import { mount } from '@cypress/react18';
-import { Provider as ReduxProvider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { notificationAtom, notificationAtomType } from 'V2/atoms';
import { NotificationsContainer } from '../NotificationsContainer';
@@ -21,14 +19,12 @@ describe('Notifications container', () => {
};
return (
-
- <>
-
-
- Send notification
-
- >
-
+ <>
+
+
+ Send notification
+
+ >
);
};
diff --git a/app/react/V2/Routes/Settings/ActivityLog/components/FiltersSidePanel.tsx b/app/react/V2/Routes/Settings/ActivityLog/components/FiltersSidePanel.tsx
index cdecc22c79..fa7e848d4f 100644
--- a/app/react/V2/Routes/Settings/ActivityLog/components/FiltersSidePanel.tsx
+++ b/app/react/V2/Routes/Settings/ActivityLog/components/FiltersSidePanel.tsx
@@ -6,7 +6,7 @@ import { Translate, t } from 'app/I18N';
import { InputField, DateRangePicker, MultiSelect } from 'app/V2/Components/Forms';
import { useAtomValue } from 'jotai';
import { ClientSettings } from 'app/apiResponseTypes';
-import { settingsAtom, translationsAtom } from 'app/V2/atoms';
+import { settingsAtom, localeAtom } from 'app/V2/atoms';
interface ActivityLogSearch {
username: string;
@@ -36,7 +36,7 @@ const methodOptions = ['CREATE', 'UPDATE', 'DELETE', 'MIGRATE', 'WARNING'].map(m
const FiltersSidePanel = ({ isOpen, onClose, onSubmit, appliedFilters }: FiltersSidePanelProps) => {
const { dateFormat = 'YYYY-MM-DD' } = useAtomValue
(settingsAtom);
- const { locale } = useAtomValue<{ locale: string }>(translationsAtom);
+ const locale = useAtomValue(localeAtom);
const [currentFilters, setCurrentFilters] = useState(appliedFilters);
useEffect(() => {
diff --git a/app/react/V2/Routes/Settings/Languages/components/InstallLanguagesModal.tsx b/app/react/V2/Routes/Settings/Languages/components/InstallLanguagesModal.tsx
index 7287aefb72..131c0674ee 100644
--- a/app/react/V2/Routes/Settings/Languages/components/InstallLanguagesModal.tsx
+++ b/app/react/V2/Routes/Settings/Languages/components/InstallLanguagesModal.tsx
@@ -22,6 +22,7 @@ const InstallLanguagesModal = ({ setShowModal, languages }: InstallLanguagesModa
}));
const install = async () => {
+ setShowModal(false);
await requestAction(
I18NApi.addLanguage,
new RequestParams(languages.filter(l => selected.includes(l.key))),
@@ -57,7 +58,6 @@ const InstallLanguagesModal = ({ setShowModal, languages }: InstallLanguagesModa
{
- setShowModal(false);
await install();
}}
className="grow"
diff --git a/app/react/V2/Routes/Settings/ParagraphExtraction/PXParagraphs.tsx b/app/react/V2/Routes/Settings/ParagraphExtraction/PXParagraphs.tsx
index 7845d66c3e..6a2589214a 100644
--- a/app/react/V2/Routes/Settings/ParagraphExtraction/PXParagraphs.tsx
+++ b/app/react/V2/Routes/Settings/ParagraphExtraction/PXParagraphs.tsx
@@ -1,21 +1,22 @@
import React, { useEffect, useMemo, useState } from 'react';
import { IncomingHttpHeaders } from 'http';
import { LoaderFunction, useLoaderData } from 'react-router-dom';
-import * as pxParagraphApi from 'app/V2/api/paragraphExtractor/paragraphs';
+import { useAtomValue } from 'jotai';
+import { LanguageSchema } from 'shared/types/commonTypes';
+import { Template } from 'app/apiResponseTypes';
+import { I18NApi } from 'app/I18N';
+import { RequestParams } from 'app/utils/RequestParams';
+import * as pxParagraphApi from 'V2/api/paragraphExtractor/paragraphs';
import { SettingsContent } from 'V2/Components/Layouts/SettingsContent';
import { Table, Button } from 'V2/Components/UI';
import { Sidepanel } from 'V2/Components/UI/Sidepanel';
-import { Template } from 'app/apiResponseTypes';
-import { Translate, I18NApi } from 'app/I18N';
-import { LanguageSchema } from 'shared/types/commonTypes';
-import { RequestParams } from 'app/utils/RequestParams';
+import { Translate } from 'app/I18N';
+import { templatesAtom } from 'V2/atoms';
import { tableBuilder } from './components/PXParagraphTableElements';
import { TableTitle } from './components/TableTitle';
import { PXParagraphTable, PXParagraphApiResponse, PXEntityApiResponse } from './types';
import { getTemplateName } from './utils/getTemplateName';
import { ViewParagraph } from './components/ViewParagraph';
-import { templatesAtom } from 'V2/atoms';
-import { useAtomValue } from 'jotai';
const formatParagraphData = (
paragraphs: PXParagraphApiResponse[],
diff --git a/app/react/V2/Routes/Settings/Thesauri/components/TableComponents.tsx b/app/react/V2/Routes/Settings/Thesauri/components/TableComponents.tsx
index ca37c82862..2d16b8305f 100644
--- a/app/react/V2/Routes/Settings/Thesauri/components/TableComponents.tsx
+++ b/app/react/V2/Routes/Settings/Thesauri/components/TableComponents.tsx
@@ -2,7 +2,7 @@
import React from 'react';
import { CellContext, createColumnHelper } from '@tanstack/react-table';
import { t, Translate } from 'app/I18N';
-import { Button, Pill } from 'app/V2/Components/UI';
+import { Button, Pill } from 'V2/Components/UI';
import { ClientThesaurus, ClientThesaurusValue } from 'app/apiResponseTypes';
import { ThesauriRow } from './ThesauriTable';
diff --git a/app/react/V2/Routes/Settings/Thesauri/components/ThesauriGroupFormSidepanel.tsx b/app/react/V2/Routes/Settings/Thesauri/components/ThesauriGroupFormSidepanel.tsx
index 3c1a803fac..4ec49370f4 100644
--- a/app/react/V2/Routes/Settings/Thesauri/components/ThesauriGroupFormSidepanel.tsx
+++ b/app/react/V2/Routes/Settings/Thesauri/components/ThesauriGroupFormSidepanel.tsx
@@ -4,8 +4,8 @@ import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form';
import { isEmpty, last } from 'lodash';
import CheckCircleIcon from '@heroicons/react/20/solid/CheckCircleIcon';
import { Translate } from 'app/I18N';
-import { InputField } from 'app/V2/Components/Forms';
-import { Button, Card, Sidepanel } from 'app/V2/Components/UI';
+import { InputField } from 'V2/Components/Forms';
+import { Button, Card, Sidepanel } from 'V2/Components/UI';
import uniqueID from 'shared/uniqueID';
import { ThesaurusRow } from './TableComponents';
import { emptyThesaurus } from '../helpers';
diff --git a/app/react/V2/Routes/Settings/Thesauri/components/ThesauriTable.tsx b/app/react/V2/Routes/Settings/Thesauri/components/ThesauriTable.tsx
index 6a2323f802..a158f2fd1a 100644
--- a/app/react/V2/Routes/Settings/Thesauri/components/ThesauriTable.tsx
+++ b/app/react/V2/Routes/Settings/Thesauri/components/ThesauriTable.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Row } from '@tanstack/react-table';
import { Translate } from 'app/I18N';
-import { Table } from 'app/V2/Components/UI';
+import { Table } from 'V2/Components/UI';
import { ClientThesaurus, Template } from 'app/apiResponseTypes';
import { ObjectIdSchema } from 'shared/types/commonTypes';
import { columnsThesauri } from './TableComponents';
diff --git a/app/react/V2/Routes/Settings/Thesauri/components/ThesauriValueFormSidepanel.tsx b/app/react/V2/Routes/Settings/Thesauri/components/ThesauriValueFormSidepanel.tsx
index be3e3fb0a9..cfe81bb787 100644
--- a/app/react/V2/Routes/Settings/Thesauri/components/ThesauriValueFormSidepanel.tsx
+++ b/app/react/V2/Routes/Settings/Thesauri/components/ThesauriValueFormSidepanel.tsx
@@ -4,8 +4,8 @@ import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form';
import CheckCircleIcon from '@heroicons/react/20/solid/CheckCircleIcon';
import { isEmpty, last } from 'lodash';
import { Translate } from 'app/I18N';
-import { InputField, Select } from 'app/V2/Components/Forms';
-import { Button, Card, Sidepanel } from 'app/V2/Components/UI';
+import { InputField, Select } from 'V2/Components/Forms';
+import { Button, Card, Sidepanel } from 'V2/Components/UI';
import uniqueID from 'shared/uniqueID';
import { ThesaurusRow } from './TableComponents';
diff --git a/app/react/V2/Routes/Settings/Thesauri/specs/Thesauri.spec.tsx b/app/react/V2/Routes/Settings/Thesauri/specs/Thesauri.spec.tsx
index e698e641b3..571d27efaa 100644
--- a/app/react/V2/Routes/Settings/Thesauri/specs/Thesauri.spec.tsx
+++ b/app/react/V2/Routes/Settings/Thesauri/specs/Thesauri.spec.tsx
@@ -14,11 +14,9 @@ import {
cleanup,
} from '@testing-library/react/pure';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
-import { Provider } from 'react-redux';
import { has } from 'lodash';
-import { fromJS } from 'immutable';
import { templatesAtom } from 'V2/atoms';
-import { LEGACY_createStore as reduxStoreCreator, TestAtomStoreProvider } from 'V2/testing';
+import { TestAtomStoreProvider } from 'V2/testing';
import { ThesauriList, thesauriLoader } from '../ThesauriList';
import { EditThesaurus } from '../EditThesaurus';
import { editThesaurusLoader } from '../helpers';
@@ -100,24 +98,11 @@ describe('Settings Thesauri', () => {
}
);
- const reduxStore = reduxStoreCreator({
- locale: 'en',
- inlineEdit: fromJS({ inlineEdit: true }),
- translations: fromJS([
- {
- locale: 'en',
- contexts: [],
- },
- ]),
- });
-
const renderComponent = () =>
render(
-
-
-
-
-
+
+
+
);
let rows: HTMLElement[];
diff --git a/app/react/V2/Routes/Settings/Thesauri/specs/__snapshots__/Thesauri.spec.tsx.snap b/app/react/V2/Routes/Settings/Thesauri/specs/__snapshots__/Thesauri.spec.tsx.snap
index 89d8ef512f..32324de5e0 100644
--- a/app/react/V2/Routes/Settings/Thesauri/specs/__snapshots__/Thesauri.spec.tsx.snap
+++ b/app/react/V2/Routes/Settings/Thesauri/specs/__snapshots__/Thesauri.spec.tsx.snap
@@ -11,7 +11,7 @@ HTMLCollection [
>
Select
@@ -37,7 +37,7 @@ HTMLCollection [
class="has-[span:not(.active)]:hidden h-full p-1 ml-2 border-2 border-gray-400 border-solid rounded-lg border-y-0"
>
Animals
@@ -60,7 +60,7 @@ HTMLCollection [
type="button"
>
Edit
@@ -75,7 +75,7 @@ HTMLCollection [
>
Select
@@ -101,7 +101,7 @@ HTMLCollection [
class="has-[span:not(.active)]:hidden h-full p-1 ml-2 border-2 border-gray-400 border-solid rounded-lg border-y-0"
>
Colors
@@ -124,7 +124,7 @@ HTMLCollection [
type="button"
>
Edit
@@ -139,7 +139,7 @@ HTMLCollection [
>
Select
@@ -166,7 +166,7 @@ HTMLCollection [
class="has-[span:not(.active)]:hidden h-full p-1 ml-2 border-2 border-gray-400 border-solid rounded-lg border-y-0"
>
Names
@@ -185,7 +185,7 @@ HTMLCollection [
data-testid="pill-comp"
>
Document
@@ -202,7 +202,7 @@ HTMLCollection [
type="button"
>
Edit
@@ -228,13 +228,13 @@ exports[`Settings Thesauri ThesauriList Thesaurus deletion should show the selec
type="button"
>
Delete
Selected
@@ -242,7 +242,7 @@ exports[`Settings Thesauri ThesauriList Thesaurus deletion should show the selec
2
of
@@ -268,8 +268,8 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
data-testid="settings-content-header"
>
Navigate back
@@ -328,7 +328,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
data-testid="flowbite-breadcrumb-item"
>
Thesauri
@@ -360,7 +360,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
class="text-base font-semibold text-left text-gray-900 bg-white"
>
Thesauri
@@ -381,7 +381,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
>
Select all
@@ -402,7 +402,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
class="flex gap-2 cursor-pointer select-none"
>
Label
@@ -431,7 +431,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
class="flex "
>
Templates
@@ -446,7 +446,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
class="flex "
>
Action
@@ -463,7 +463,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
>
Select
@@ -489,7 +489,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
class="has-[span:not(.active)]:hidden h-full p-1 ml-2 border-2 border-gray-400 border-solid rounded-lg border-y-0"
>
Animals
@@ -512,7 +512,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
type="button"
>
Edit
@@ -527,7 +527,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
>
Select
@@ -553,7 +553,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
class="has-[span:not(.active)]:hidden h-full p-1 ml-2 border-2 border-gray-400 border-solid rounded-lg border-y-0"
>
Colors
@@ -576,7 +576,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
type="button"
>
Edit
@@ -591,7 +591,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
>
Select
@@ -618,7 +618,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
class="has-[span:not(.active)]:hidden h-full p-1 ml-2 border-2 border-gray-400 border-solid rounded-lg border-y-0"
>
Names
@@ -637,7 +637,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
data-testid="pill-comp"
>
Document
@@ -654,7 +654,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
type="button"
>
Edit
@@ -702,7 +702,7 @@ exports[`Settings Thesauri ThesauriList render existing thesauri should show a l
type="button"
>
Add thesaurus
diff --git a/app/react/V2/api/translations/index.ts b/app/react/V2/api/translations/index.ts
index 66a2c9026e..91ed0ef745 100644
--- a/app/react/V2/api/translations/index.ts
+++ b/app/react/V2/api/translations/index.ts
@@ -1,8 +1,11 @@
import { Params } from 'react-router-dom';
import { IncomingHttpHeaders } from 'http';
+import api from 'app/utils/api';
import { I18NApi } from 'app/I18N';
-import { ClientTranslationSchema } from 'app/istore';
+import { FetchResponseError } from 'shared/JSONRequest';
+import { ClientTranslationSchema, ClientTranslationContextSchema } from 'app/istore';
import { RequestParams } from 'app/utils/RequestParams';
+import { TranslationValue } from 'V2/shared/types';
import { httpRequest } from 'shared/superagent';
import loadingBar from 'app/App/LoadingProgressBar';
@@ -24,7 +27,19 @@ const get = async (
return response;
};
-const post = async (updatedTranslations: ClientTranslationSchema[], contextId: string) => {
+const getV2 = async (
+ headers?: IncomingHttpHeaders,
+ parameters?: Params
+): Promise => {
+ const params = new RequestParams(parameters, headers);
+ const response = api.get('v2/translations', params);
+ return response;
+};
+
+const post = async (
+ updatedTranslations: ClientTranslationSchema[],
+ contextId: string
+): Promise => {
try {
const translations = await Promise.all(
updatedTranslations.map(language => I18NApi.save(new RequestParams(language)))
@@ -35,6 +50,24 @@ const post = async (updatedTranslations: ClientTranslationSchema[], contextId: s
}
};
+const postV2 = async (
+ updatedTranslations: TranslationValue[],
+ context: ClientTranslationContextSchema,
+ headers?: IncomingHttpHeaders
+): Promise => {
+ try {
+ const translations = updatedTranslations.map(ut => ({
+ ...ut,
+ context: { id: context.id, label: context.label, type: context.type },
+ }));
+ const params = new RequestParams(translations, headers);
+ const response = await api.post('v2/translations', params);
+ return response.status;
+ } catch (e) {
+ return e;
+ }
+};
+
const importTranslations = async (
file: File,
contextId: string
@@ -58,6 +91,4 @@ const importTranslations = async (
}
};
-const { getLanguages } = I18NApi;
-
-export { get, post, importTranslations, getLanguages };
+export { get, getV2, post, postV2, importTranslations };
diff --git a/app/react/V2/atoms/index.ts b/app/react/V2/atoms/index.ts
index 369e099758..46ba2faa26 100644
--- a/app/react/V2/atoms/index.ts
+++ b/app/react/V2/atoms/index.ts
@@ -2,7 +2,7 @@ export { atomStore, hydrateAtomStore } from './store';
export { notificationAtom } from './notificationAtom';
export { settingsAtom } from './settingsAtom';
export { templatesAtom } from './templatesAtom';
-export { translationsAtom } from './translationsAtom';
+export { translationsAtom, inlineEditAtom, localeAtom } from './translationsAtoms';
export { thesauriAtom } from './thesauriAtom';
export { globalMatomoAtom } from './globalMatomoAtom';
export { ciMatomoActiveAtom } from './ciMatomoActiveAtom';
diff --git a/app/react/V2/atoms/store.ts b/app/react/V2/atoms/store.ts
index 325f8bb6dc..2a728e8c65 100644
--- a/app/react/V2/atoms/store.ts
+++ b/app/react/V2/atoms/store.ts
@@ -2,13 +2,13 @@ import { createStore } from 'jotai';
import { isClient } from 'app/utils';
import { store } from 'app/store';
import { ClientSettings, ClientThesaurus, ClientUserSchema } from 'app/apiResponseTypes';
-import { ClientTemplateSchema } from 'app/istore';
+import { ClientTemplateSchema, ClientTranslationSchema } from 'app/istore';
import { globalMatomoAtom } from './globalMatomoAtom';
import { ciMatomoActiveAtom } from './ciMatomoActiveAtom';
import { relationshipTypesAtom } from './relationshipTypes';
import { settingsAtom } from './settingsAtom';
import { templatesAtom } from './templatesAtom';
-import { translationsAtom } from './translationsAtom';
+import { translationsAtom, localeAtom } from './translationsAtoms';
import { userAtom } from './userAtom';
import { thesauriAtom } from './thesauriAtom';
import { pdfScaleAtom } from './pdfScaleAtom';
@@ -21,6 +21,7 @@ type AtomStoreData = {
templates?: ClientTemplateSchema[];
user?: ClientUserSchema;
ciMatomoActive?: boolean;
+ translations: ClientTranslationSchema[];
};
declare global {
@@ -38,7 +39,8 @@ const hydrateAtomStore = (data: AtomStoreData) => {
if (data.thesauri) atomStore.set(thesauriAtom, data.thesauri);
if (data.templates) atomStore.set(templatesAtom, data.templates);
atomStore.set(userAtom, data.user);
- atomStore.set(translationsAtom, { locale: data.locale || 'en' });
+ atomStore.set(translationsAtom, data.translations);
+ atomStore.set(localeAtom, data.locale || 'en');
};
if (isClient && window.__atomStoreData__) {
@@ -65,6 +67,10 @@ if (isClient && window.__atomStoreData__) {
const value = atomStore.get(pdfScaleAtom);
store?.dispatch({ type: 'viewer/documentScale/SET', value });
});
+ atomStore.sub(translationsAtom, () => {
+ const value = atomStore.get(translationsAtom);
+ store?.dispatch({ type: 'translations', value });
+ });
}
export type { AtomStoreData };
diff --git a/app/react/V2/atoms/templatesAtom.tsx b/app/react/V2/atoms/templatesAtom.ts
similarity index 100%
rename from app/react/V2/atoms/templatesAtom.tsx
rename to app/react/V2/atoms/templatesAtom.ts
diff --git a/app/react/V2/atoms/translationsAtom.tsx b/app/react/V2/atoms/translationsAtom.tsx
deleted file mode 100644
index 40e8f13386..0000000000
--- a/app/react/V2/atoms/translationsAtom.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { atom } from 'jotai';
-
-const translationsAtom = atom({ locale: '' });
-
-export { translationsAtom };
diff --git a/app/react/V2/atoms/translationsAtoms.ts b/app/react/V2/atoms/translationsAtoms.ts
new file mode 100644
index 0000000000..463a4cd2d4
--- /dev/null
+++ b/app/react/V2/atoms/translationsAtoms.ts
@@ -0,0 +1,8 @@
+import { atom } from 'jotai';
+import { ClientTranslationSchema } from 'app/istore';
+
+const translationsAtom = atom([] as ClientTranslationSchema[]);
+const localeAtom = atom('');
+const inlineEditAtom = atom({ inlineEdit: false, context: '', translationKey: '' });
+
+export { translationsAtom, inlineEditAtom, localeAtom };
diff --git a/app/react/V2/shared/types.ts b/app/react/V2/shared/types.ts
index 14c9ae005a..9bd66c2fee 100644
--- a/app/react/V2/shared/types.ts
+++ b/app/react/V2/shared/types.ts
@@ -10,6 +10,12 @@ type DraggableValue = T & {
items?: IDraggable[];
};
+type TranslationValue = {
+ language: string;
+ key: string;
+ value: string;
+};
+
interface IDraggable {
dndId?: string;
value: DraggableValue;
@@ -50,4 +56,12 @@ enum ItemTypes {
}
export { ItemTypes };
-export type { IXExtractorInfo, ISublink, ILink, IDraggable, DraggableValue, Page };
+export type {
+ IXExtractorInfo,
+ ISublink,
+ ILink,
+ IDraggable,
+ DraggableValue,
+ Page,
+ TranslationValue,
+};
diff --git a/app/react/Viewer/components/specs/__snapshots__/ConnectionsList.spec.js.snap b/app/react/Viewer/components/specs/__snapshots__/ConnectionsList.spec.js.snap
index 6d624b86d6..851e4e3839 100644
--- a/app/react/Viewer/components/specs/__snapshots__/ConnectionsList.spec.js.snap
+++ b/app/react/Viewer/components/specs/__snapshots__/ConnectionsList.spec.js.snap
@@ -119,10 +119,18 @@ exports[`ConnectionsList when there are no references should render a blank stat
icon="sitemap"
/>
- No Connections
+
+ No Connections
+
- No Connections description
+
+ No Connections description
+
`;
@@ -135,10 +143,18 @@ exports[`ConnectionsList when there are no references should render a blank stat
icon="sitemap"
/>
- No References
+
+ No References
+
- No References description
+
+ No References description
+
`;
diff --git a/app/react/Viewer/components/specs/__snapshots__/Paginator.spec.js.snap b/app/react/Viewer/components/specs/__snapshots__/Paginator.spec.js.snap
index 91de2eb172..a33368199f 100644
--- a/app/react/Viewer/components/specs/__snapshots__/Paginator.spec.js.snap
+++ b/app/react/Viewer/components/specs/__snapshots__/Paginator.spec.js.snap
@@ -45,22 +45,14 @@ exports[`Paginator should render a previous button and next button based on the
onClick={[Function]}
to="undefined?page=4"
>
-
-
+
-
- Previous
-
-
-
+ Previous
+
+
@@ -87,22 +79,14 @@ exports[`Paginator should render a previous button and next button based on the
onClick={[Function]}
to="undefined?page=6"
>
-
-
+
-
- Next
-
-
-
+ Next
+
+
@@ -157,22 +141,14 @@ exports[`Paginator when base Url already has the query string "?" should add the
onClick={[Function]}
to="undefined?page=2"
>
-
-
+
-
- Previous
-
-
-
+ Previous
+
+
@@ -199,22 +175,14 @@ exports[`Paginator when base Url already has the query string "?" should add the
onClick={[Function]}
to="undefined?page=4"
>
-
-
+
-
- Next
-
-
-
+ Next
+
+
@@ -272,22 +240,14 @@ exports[`Paginator when on first page should disable the prev link 1`] = `
rel="nofollow"
to="undefined?page=1"
>
-
-
+
-
- Previous
-
-
-
+ Previous
+
+
@@ -314,22 +274,14 @@ exports[`Paginator when on first page should disable the prev link 1`] = `
onClick={[Function]}
to="undefined?page=2"
>
-
-
+
-
- Next
-
-
-
+ Next
+
+
@@ -384,22 +336,14 @@ exports[`Paginator when on last page should disable the next link 1`] = `
onClick={[Function]}
to="undefined?page=24"
>
-
-
+
-
- Previous
-
-
-
+ Previous
+
+
@@ -429,22 +373,14 @@ exports[`Paginator when on last page should disable the next link 1`] = `
rel="nofollow"
to="undefined?page=25"
>
-
-
+
-
- Next
-
-
-
+ Next
+
+
diff --git a/app/react/entry-server.tsx b/app/react/entry-server.tsx
index 4780605e83..f7eba356a7 100644
--- a/app/react/entry-server.tsx
+++ b/app/react/entry-server.tsx
@@ -24,7 +24,7 @@ import Root from './App/Root';
import RouteHandler from './App/RouteHandler';
import { ErrorBoundary } from './V2/Components/ErrorHandling';
import { atomStore, hydrateAtomStore } from './V2/atoms';
-import { I18NUtils, t, Translate } from './I18N';
+import { I18NUtils } from './I18N';
import { IStore } from './istore';
import { getRoutes } from './Routes';
import createReduxStore from './store';
@@ -154,10 +154,10 @@ const prepareStores = async (req: ExpressRequest, settings: ClientSettings, lang
const reduxData = {
user: userApiResponse.json,
- translations: translationsApiResponse.json.rows,
templates: templatesApiResponse.json.rows,
thesauris: thesaurisApiResponse.json.rows,
relationTypes: relationTypesApiResponse.json.rows,
+ translations: translationsApiResponse.json.rows,
settings: {
collection: { ...settingsApiResponse.json, links: settingsApiResponse.json.links || [] },
},
@@ -176,6 +176,7 @@ const prepareStores = async (req: ExpressRequest, settings: ClientSettings, lang
thesauri: thesaurisApiResponse.json.rows,
templates: templatesApiResponse.json.rows,
user: userApiResponse.json,
+ translations: translationsApiResponse.json.rows,
},
};
};
@@ -268,11 +269,6 @@ const getSSRProperties = async (
};
};
-const resetTranslations = () => {
- t.resetCachedTranslation();
- Translate.resetCachedTranslation();
-};
-
const EntryServer = async (req: ExpressRequest, res: Response) => {
RouteHandler.renderedFromServer = true;
const [settings, assets] = await Promise.all([
@@ -303,7 +299,7 @@ const EntryServer = async (req: ExpressRequest, res: Response) => {
reduxState,
matched
);
- resetTranslations();
+
hydrateAtomStore(atomStoreData);
const componentHtml = ReactDOMServer.renderToString(
diff --git a/app/react/stories/Buttons/EmbededButton.stories.tsx b/app/react/stories/Buttons/EmbededButton.stories.tsx
index f10dd2018f..0ffc3ddc75 100644
--- a/app/react/stories/Buttons/EmbededButton.stories.tsx
+++ b/app/react/stories/Buttons/EmbededButton.stories.tsx
@@ -1,9 +1,7 @@
import React from 'react';
-import { Provider } from 'react-redux';
import { CheckCircleIcon } from '@heroicons/react/24/outline';
import type { Meta, StoryObj } from '@storybook/react';
-import { EmbededButton } from 'app/V2/Components/UI/EmbededButton';
-import { LEGACY_createStore as createStore } from 'V2/testing';
+import { EmbededButton } from 'V2/Components/UI/EmbededButton';
import { Translate } from 'app/I18N';
const meta: Meta = {
@@ -15,18 +13,16 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
- {args.children}
-
-
-
+
+
+ {args.children}
+
+
),
};
diff --git a/app/react/stories/CodeEditor.stories.tsx b/app/react/stories/CodeEditor.stories.tsx
index b0ba61c698..90fee45c70 100644
--- a/app/react/stories/CodeEditor.stories.tsx
+++ b/app/react/stories/CodeEditor.stories.tsx
@@ -1,8 +1,6 @@
import React, { useRef, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
-import { Provider } from 'react-redux';
import { CodeEditor, CodeEditorProps, CodeEditorInstance } from 'V2/Components/CodeEditor';
-import { LEGACY_createStore as createStore } from 'V2/testing';
const sampleJS = `const myButton = document.getElementById('myButton');
myButton.addEventListener('click', function () {
@@ -85,30 +83,28 @@ const Component = ({ language, intialValue, fallbackElement }: CodeEditorProps)
const [updatedCode, setUpdatedCode] = useState();
return (
-
-
-
- {
- editorInstance.current = editor;
- }}
- fallbackElement={fallbackElement}
- />
-
-
- setUpdatedCode(editorInstance.current?.getValue())}
- className="p-2 text-white rounded border bg-primary-700"
- >
- Save
-
-
-
{updatedCode}
+
+
+ {
+ editorInstance.current = editor;
+ }}
+ fallbackElement={fallbackElement}
+ />
-
+
+ setUpdatedCode(editorInstance.current?.getValue())}
+ className="p-2 text-white rounded border bg-primary-700"
+ >
+ Save
+
+
+
{updatedCode}
+
);
};
diff --git a/app/react/stories/ConfirmationModal.stories.tsx b/app/react/stories/ConfirmationModal.stories.tsx
index 3a545ba018..26c1cee8bb 100644
--- a/app/react/stories/ConfirmationModal.stories.tsx
+++ b/app/react/stories/ConfirmationModal.stories.tsx
@@ -2,9 +2,7 @@ import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { action } from '@storybook/addon-actions';
-import { Provider } from 'react-redux';
import { ConfirmationModal } from 'app/V2/Components/UI/ConfirmationModal';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { Translate } from 'app/I18N';
const meta: Meta
= {
@@ -22,24 +20,22 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
),
};
diff --git a/app/react/stories/ErrorBoundary.stories.tsx b/app/react/stories/ErrorBoundary.stories.tsx
index 1275fb5fe0..22182ee724 100644
--- a/app/react/stories/ErrorBoundary.stories.tsx
+++ b/app/react/stories/ErrorBoundary.stories.tsx
@@ -1,7 +1,5 @@
import React, { ComponentClass } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
-import { Provider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { ErrorBoundary } from 'app/V2/Components/ErrorHandling';
import type { ErrorBoundaryProps } from 'app/V2/Components/ErrorHandling';
@@ -14,11 +12,9 @@ type Story = StoryObj
;
const Primary: Story = {
render: args => (
-
-
- {args.children}
-
-
+
+ {args.children}
+
),
};
diff --git a/app/react/stories/Forms/Checkbox.stories.tsx b/app/react/stories/Forms/Checkbox.stories.tsx
index 0865a6099f..6d454e218f 100644
--- a/app/react/stories/Forms/Checkbox.stories.tsx
+++ b/app/react/stories/Forms/Checkbox.stories.tsx
@@ -3,8 +3,6 @@ import { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { fn } from '@storybook/test';
import { Checkbox } from 'app/V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
-import { Provider } from 'react-redux';
const meta: Meta = {
title: 'Forms/Checkbox',
@@ -18,17 +16,15 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
+
),
};
diff --git a/app/react/stories/Forms/ColorPicker.stories.tsx b/app/react/stories/Forms/ColorPicker.stories.tsx
index 197496d078..8ec3da957c 100644
--- a/app/react/stories/Forms/ColorPicker.stories.tsx
+++ b/app/react/stories/Forms/ColorPicker.stories.tsx
@@ -1,10 +1,8 @@
import React from 'react';
-import { Provider as ReduxProvider } from 'react-redux';
import { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { action } from '@storybook/addon-actions';
import { ColorPicker } from 'app/V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
const meta: Meta = {
title: 'Forms/ColorPicker',
@@ -23,16 +21,14 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
+
+
),
};
diff --git a/app/react/stories/Forms/DatePicker.stories.tsx b/app/react/stories/Forms/DatePicker.stories.tsx
index 32cb6c873a..4c7b1dcc1b 100644
--- a/app/react/stories/Forms/DatePicker.stories.tsx
+++ b/app/react/stories/Forms/DatePicker.stories.tsx
@@ -1,10 +1,9 @@
import React from 'react';
-import { Provider as ReduxProvider } from 'react-redux';
import { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { fn } from '@storybook/test';
-import { DatePicker } from 'app/V2/Components/Forms';
-import { LEGACY_createStore as createStore, TestAtomStoreProvider } from 'V2/testing';
+import { DatePicker } from 'V2/Components/Forms';
+import { TestAtomStoreProvider } from 'V2/testing';
import { settingsAtom } from 'V2/atoms';
const meta: Meta = {
@@ -26,24 +25,22 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
+
+
),
};
diff --git a/app/react/stories/Forms/DateRangePicker.stories.tsx b/app/react/stories/Forms/DateRangePicker.stories.tsx
index d502972c38..7dbdbd1ea3 100644
--- a/app/react/stories/Forms/DateRangePicker.stories.tsx
+++ b/app/react/stories/Forms/DateRangePicker.stories.tsx
@@ -1,10 +1,8 @@
import React from 'react';
-import { Provider } from 'react-redux';
import { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { fn } from '@storybook/test';
import { DateRangePicker } from 'app/V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
const meta: Meta = {
title: 'Forms/DateRangePicker',
@@ -24,23 +22,21 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
),
};
diff --git a/app/react/stories/Forms/EnableButtonCheckbox.stories.tsx b/app/react/stories/Forms/EnableButtonCheckbox.stories.tsx
index 940a502932..5f79b6b5d0 100644
--- a/app/react/stories/Forms/EnableButtonCheckbox.stories.tsx
+++ b/app/react/stories/Forms/EnableButtonCheckbox.stories.tsx
@@ -1,8 +1,6 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { EnableButtonCheckbox } from 'app/V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
-import { Provider } from 'react-redux';
const meta: Meta
= {
title: 'Forms/EnableButtonCheckbox',
@@ -14,14 +12,12 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
),
};
diff --git a/app/react/stories/Forms/FileDropzone.stories.tsx b/app/react/stories/Forms/FileDropzone.stories.tsx
index 149229e9e0..ca390cc4c5 100644
--- a/app/react/stories/Forms/FileDropzone.stories.tsx
+++ b/app/react/stories/Forms/FileDropzone.stories.tsx
@@ -1,8 +1,6 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { FileDropzone } from 'app/V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
-import { Provider } from 'react-redux';
const meta: Meta = {
title: 'Forms/FileDropzone',
@@ -13,11 +11,9 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
+
+
),
};
diff --git a/app/react/stories/Forms/InputField.stories.tsx b/app/react/stories/Forms/InputField.stories.tsx
index 6f756da7e2..5b54f7a511 100644
--- a/app/react/stories/Forms/InputField.stories.tsx
+++ b/app/react/stories/Forms/InputField.stories.tsx
@@ -1,8 +1,6 @@
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
-import { Provider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { InputField } from 'V2/Components/Forms';
const meta: Meta = {
@@ -14,13 +12,11 @@ type Story = StoryObj;
const InputFieldStory: Story = {
render: args => (
-
-
-
-
-
+
),
};
diff --git a/app/react/stories/Forms/MultiSelect.stories.tsx b/app/react/stories/Forms/MultiSelect.stories.tsx
index 6b0fadf717..f1ef8c122c 100644
--- a/app/react/stories/Forms/MultiSelect.stories.tsx
+++ b/app/react/stories/Forms/MultiSelect.stories.tsx
@@ -1,8 +1,6 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
-import { Provider } from 'react-redux';
import { MultiSelect } from 'V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
const meta: Meta
= {
title: 'Forms/MultiSelect',
@@ -13,23 +11,21 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
Multiselect component
-
-
+
+
+
Multiselect component
+
-
+
),
};
diff --git a/app/react/stories/Forms/MultiselectList.stories.tsx b/app/react/stories/Forms/MultiselectList.stories.tsx
index c61c6d7374..1f4d396f83 100644
--- a/app/react/stories/Forms/MultiselectList.stories.tsx
+++ b/app/react/stories/Forms/MultiselectList.stories.tsx
@@ -1,8 +1,6 @@
import React, { useState } from 'react';
import { Meta, StoryObj } from '@storybook/react';
-import { Provider } from 'react-redux';
import { MultiselectList } from 'V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
const meta: Meta
= {
title: 'Forms/MultiselectList',
@@ -15,32 +13,30 @@ const StoryComponent = ({ args }: any) => {
const [searchAndFocus, setSearchAndFocus] = useState('');
return (
-
- <>
-
-
-
-
+ <>
+
+
+
-
setSearchAndFocus('another')}>
- Search & Focus
-
-
setSearchAndFocus('')}>
- Clear
-
- >
-
+
+
setSearchAndFocus('another')}>
+ Search & Focus
+
+
setSearchAndFocus('')}>
+ Clear
+
+ >
);
};
diff --git a/app/react/stories/Forms/RadioSelect.stories.tsx b/app/react/stories/Forms/RadioSelect.stories.tsx
index f0449a6c83..2d29fe2d32 100644
--- a/app/react/stories/Forms/RadioSelect.stories.tsx
+++ b/app/react/stories/Forms/RadioSelect.stories.tsx
@@ -3,8 +3,6 @@ import { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { fn } from '@storybook/test';
import { RadioSelect } from 'app/V2/Components/Forms';
-import { LEGACY_createStore as createStore } from 'V2/testing';
-import { Provider } from 'react-redux';
const meta: Meta
= {
title: 'Forms/RadioSelect',
@@ -18,17 +16,15 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
+
+
),
};
diff --git a/app/react/stories/MediaPlayer.stories.tsx b/app/react/stories/MediaPlayer.stories.tsx
index 92587ffd53..a663c482da 100644
--- a/app/react/stories/MediaPlayer.stories.tsx
+++ b/app/react/stories/MediaPlayer.stories.tsx
@@ -1,7 +1,5 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
-import { Provider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { MediaPlayer } from 'V2/Components/UI';
const meta: Meta = {
@@ -13,18 +11,16 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
),
};
diff --git a/app/react/stories/Notification.stories.tsx b/app/react/stories/Notification.stories.tsx
index 64a10a6bbd..d913bdb31a 100644
--- a/app/react/stories/Notification.stories.tsx
+++ b/app/react/stories/Notification.stories.tsx
@@ -1,8 +1,6 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Notification } from 'V2/Components/UI/Notification';
-import { Provider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
const meta: Meta
= {
title: 'Components/Notification',
@@ -13,18 +11,16 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
-
-
+
),
};
diff --git a/app/react/stories/PDF.stories.tsx b/app/react/stories/PDF.stories.tsx
index 6670cccfe8..8f08152bd3 100644
--- a/app/react/stories/PDF.stories.tsx
+++ b/app/react/stories/PDF.stories.tsx
@@ -1,8 +1,6 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
-import { Provider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { PDF } from 'V2/Components/PDFViewer';
import { highlights } from './fixtures/PDFStoryFixtures';
@@ -16,22 +14,20 @@ type Story = StoryObj
;
const Primary: Story = {
render: args => (
-
-
-
PDF Container:
-
-
End of container
+
+
PDF Container:
+
-
+
End of container
+
),
};
diff --git a/app/react/stories/Paginator.stories.tsx b/app/react/stories/Paginator.stories.tsx
index 9664ce3461..42b45c6e8b 100644
--- a/app/react/stories/Paginator.stories.tsx
+++ b/app/react/stories/Paginator.stories.tsx
@@ -1,9 +1,7 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
-import { Provider } from 'react-redux';
import type { Meta, StoryObj } from '@storybook/react';
import { Paginator } from 'app/V2/Components/UI';
-import { LEGACY_createStore as createStore } from 'V2/testing';
const meta: Meta
= {
title: 'Components/Paginator',
@@ -15,18 +13,16 @@ type Story = StoryObj;
const Primary: Story = {
render: args => (
-
-
-
+
),
};
diff --git a/app/react/stories/Sidepanel.stories.tsx b/app/react/stories/Sidepanel.stories.tsx
index 2aaa56b1bd..c5f689fa82 100644
--- a/app/react/stories/Sidepanel.stories.tsx
+++ b/app/react/stories/Sidepanel.stories.tsx
@@ -1,8 +1,6 @@
import React, { useState } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
-import { Provider } from 'react-redux';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { Sidepanel, Button } from 'V2/Components/UI';
import { SidePanelProps } from 'app/V2/Components/UI/Sidepanel';
import { GeneratedContent } from './helpers/GeneratedContent';
@@ -16,70 +14,65 @@ const SidePanelContainer = (args: SidePanelProps) => {
const [showSidepanel, setShowSidepanel] = useState(false);
return (
-
-
-
-
- This a content title
+
+
+
+ This a content title
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit.
-
+ Lorem ipsum dolor sit amet consectetur adipisicing elit.
-
- Fusce id mi eu mauris bibendum dignissim nec in sem. Sed ultrices varius mauris quis
- placerat. Donec imperdiet sodales diam sed imperdiet. Aenean a nisl venenatis lectus
- mattis pellentesque. Duis fermentum ante a ultricies feugiat. Proin dapibus luctus
- purus id viverra. Aenean a aliquet nibh. Aenean facilisis justo quis sem auctor, nec
- mollis tortor placerat. Cras eget enim mollis, mollis risus gravida, pharetra risus.
- Mauris dapibus malesuada mi, quis ornare felis imperdiet eget. Donec sed quam non
- dolor sodales hendrerit. Aenean suscipit, velit sed laoreet cursus, ante odio
- tristique lectus, a porta eros felis eu sem. Curabitur eu gravida dolor. Ut iaculis
- lacus vitae libero viverra interdum. Phasellus ac est consectetur, malesuada nisl nec,
- blandit lorem.
-
+
+ Fusce id mi eu mauris bibendum dignissim nec in sem. Sed ultrices varius mauris quis
+ placerat. Donec imperdiet sodales diam sed imperdiet. Aenean a nisl venenatis lectus
+ mattis pellentesque. Duis fermentum ante a ultricies feugiat. Proin dapibus luctus purus
+ id viverra. Aenean a aliquet nibh. Aenean facilisis justo quis sem auctor, nec mollis
+ tortor placerat. Cras eget enim mollis, mollis risus gravida, pharetra risus. Mauris
+ dapibus malesuada mi, quis ornare felis imperdiet eget. Donec sed quam non dolor sodales
+ hendrerit. Aenean suscipit, velit sed laoreet cursus, ante odio tristique lectus, a
+ porta eros felis eu sem. Curabitur eu gravida dolor. Ut iaculis lacus vitae libero
+ viverra interdum. Phasellus ac est consectetur, malesuada nisl nec, blandit lorem.
+
-
- Fusce id mi eu mauris bibendum dignissim nec in sem. Sed ultrices varius mauris quis
- placerat. Donec imperdiet sodales diam sed imperdiet. Aenean a nisl venenatis lectus
- mattis pellentesque. Duis fermentum ante a ultricies feugiat.
-
- Proin dapibus luctus purus id viverra.
-
- Aenean a aliquet nibh. Aenean facilisis justo quis sem auctor, nec mollis tortor
- placerat. Cras eget enim mollis, mollis risus gravida, pharetra risus. Mauris dapibus
- malesuada mi, quis ornare felis imperdiet eget. Donec sed quam non dolor sodales
- hendrerit. Aenean suscipit, velit sed laoreet cursus, ante odio tristique lectus, a
- porta eros felis eu sem. Curabitur eu gravida dolor. Ut iaculis lacus vitae libero
- viverra interdum. Phasellus ac est consectetur, malesuada nisl nec, blandit lorem.
-
+
+ Fusce id mi eu mauris bibendum dignissim nec in sem. Sed ultrices varius mauris quis
+ placerat. Donec imperdiet sodales diam sed imperdiet. Aenean a nisl venenatis lectus
+ mattis pellentesque. Duis fermentum ante a ultricies feugiat.
+
+ Proin dapibus luctus purus id viverra.
+
+ Aenean a aliquet nibh. Aenean facilisis justo quis sem auctor, nec mollis tortor
+ placerat. Cras eget enim mollis, mollis risus gravida, pharetra risus. Mauris dapibus
+ malesuada mi, quis ornare felis imperdiet eget. Donec sed quam non dolor sodales
+ hendrerit. Aenean suscipit, velit sed laoreet cursus, ante odio tristique lectus, a
+ porta eros felis eu sem. Curabitur eu gravida dolor. Ut iaculis lacus vitae libero
+ viverra interdum. Phasellus ac est consectetur, malesuada nisl nec, blandit lorem.
+
-
+
- setShowSidepanel(!showSidepanel)}>Open/Close sidepanel
-
-
setShowSidepanel(false)}
- size={args.size}
- >
-
-
-
-
- setShowSidepanel(false)}>Close
-
-
-
+
setShowSidepanel(!showSidepanel)}>Open/Close sidepanel
+
+
setShowSidepanel(false)}
+ size={args.size}
+ >
+
+
+
+
+ setShowSidepanel(false)}>Close
+
+
-
+
);
};
diff --git a/app/react/stories/Table.stories.tsx b/app/react/stories/Table.stories.tsx
index 3fad235cf6..91077cbc5e 100644
--- a/app/react/stories/Table.stories.tsx
+++ b/app/react/stories/Table.stories.tsx
@@ -3,9 +3,7 @@ import React, { useRef, useState } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { Cell, createColumnHelper, SortingState } from '@tanstack/react-table';
-import { Provider } from 'react-redux';
import { Button, Table } from 'V2/Components/UI';
-import { LEGACY_createStore as createStore } from 'V2/testing';
import { BasicData, DataWithGroups, basicData, dataWithGroups } from './table/fixtures';
type StoryProps = {
@@ -225,17 +223,15 @@ type Story = StoryObj
;
const Primary: Story = {
render: args => (
-
-
-
+
),
};
diff --git a/app/react/utils/useOnClickOutsideElementHook.ts b/app/react/utils/useOnClickOutsideElementHook.ts
index 050e479b3f..0640322475 100644
--- a/app/react/utils/useOnClickOutsideElementHook.ts
+++ b/app/react/utils/useOnClickOutsideElementHook.ts
@@ -13,7 +13,7 @@ export function useOnClickOutsideElement(
cb(event);
};
- document.addEventListener('click', onClickHandler);
+ document.addEventListener('click', onClickHandler, { capture: true });
return () => {
document.removeEventListener('click', onClickHandler);
};
diff --git a/app/shared/translate.js b/app/shared/translate.js
index 0cd87d33ac..d52c4d828f 100644
--- a/app/shared/translate.js
+++ b/app/shared/translate.js
@@ -1,10 +1,8 @@
-/** @format */
-
-export function getLocaleTranslation(translations, locale) {
+function getLocaleTranslation(translations, locale) {
return translations.find(d => d.locale === locale) || { contexts: [] };
}
-export function getContext(translation, contextId = '') {
+function getContext(translation, contextId = '') {
return (
translation.contexts.find(ctx => ctx.id.toString() === contextId.toString()) || { values: {} }
);
@@ -13,3 +11,5 @@ export function getContext(translation, contextId = '') {
export default function translate(context, key, text) {
return context.values[key] || text;
}
+
+export { getLocaleTranslation, getContext };
diff --git a/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show in the correct page #0.png b/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show in the correct page #0.png
deleted file mode 100644
index 981fe7e34c..0000000000
Binary files a/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show in the correct page #0.png and /dev/null differ
diff --git a/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show the correct page #0.png b/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show the correct page #0.png
new file mode 100644
index 0000000000..d63b45bea5
Binary files /dev/null and b/cypress/e2e/__image_snapshots__/PDF display responsiveness IX sidepanel should open the pdf sidepanel and show the correct page #0.png differ
diff --git a/cypress/e2e/pdf-display.cy.ts b/cypress/e2e/pdf-display.cy.ts
index 999821840b..afbb5ddde4 100644
--- a/cypress/e2e/pdf-display.cy.ts
+++ b/cypress/e2e/pdf-display.cy.ts
@@ -250,7 +250,8 @@ describe('PDF display', () => {
cy.contains('td', 'Entity with pdf (es)');
});
- it('should open the pdf sidepanel and show in the correct page', () => {
+ it('should open the pdf sidepanel and show the correct page', () => {
+ cy.contains('button', 'Open PDF').scrollIntoView();
cy.contains('button', 'Open PDF').realTouch();
cy.contains('Loading').should('not.exist');
cy.get('#pdf-container').within(() => {
@@ -262,23 +263,21 @@ describe('PDF display', () => {
cy.get('#root').toMatchImageSnapshot();
});
- it('should only show visible pages', () => {
- cy.get('#pdf-container').within(() => {
- cy.get('#page-1-container .page').should('be.empty');
- cy.get('#page-2-container .page').should('not.be.empty');
- cy.get('#page-3-container .page').should('not.be.empty');
- cy.get('#page-10-container .page').should('be.empty');
- cy.get('#page-10-container').scrollIntoView();
- cy.get('#page-1-container .page').should('be.empty');
- cy.get('#page-2-container .page').should('be.empty');
- cy.get('#page-3-container .page').should('be.empty');
- cy.get('#page-10-container .page').should('not.be.empty');
- cy.get('#page-11-container .page').should('not.be.empty');
- cy.contains(
- 'span[role="presentation"]',
- 'El artículo 63.2 de la Convención exige que para que la Corte pueda disponer de'
- ).should('be.visible');
- });
+ it('should check page rendering', () => {
+ cy.get('#page-1-container .page').should('be.empty');
+ cy.get('#page-2-container .page').should('not.be.empty');
+ cy.get('#page-3-container .page').should('not.be.empty');
+ cy.get('#page-10-container .page').should('be.empty');
+ cy.get('#page-10-container').scrollIntoView();
+ cy.get('#page-1-container .page').should('be.empty');
+ cy.get('#page-2-container .page').should('be.empty');
+ cy.get('#page-3-container .page').should('be.empty');
+ cy.get('#page-10-container .page').should('not.be.empty');
+ cy.get('#page-11-container .page').should('not.be.empty');
+ cy.contains(
+ 'span[role="presentation"]',
+ 'El artículo 63.2 de la Convención exige que para que la Corte pueda disponer de'
+ ).should('be.visible');
});
});
});
diff --git a/cypress/e2e/settings/__snapshots__/activitylog.cy.ts.snap b/cypress/e2e/settings/__snapshots__/activitylog.cy.ts.snap
index cee99896ce..b2bd629931 100644
--- a/cypress/e2e/settings/__snapshots__/activitylog.cy.ts.snap
+++ b/cypress/e2e/settings/__snapshots__/activitylog.cy.ts.snap
@@ -2,7 +2,7 @@ exports[`Activity log > should list the last activity log entries #0`] = `
UPDATE UPDATE
`;
@@ -14,7 +14,7 @@ exports[`Activity log > should list the last activity log entries #2`] = `
Updated user :editor (58ada34d299e82674854510e)
@@ -25,7 +25,7 @@ exports[`Activity log > should list the last activity log entries #3`] = `
CREATE CREATE
`;
@@ -44,78 +44,6 @@ exports[`Activity log > should list the last activity log entries #5`] = `
`;
exports[`Activity log > should open the detail of an entry #0`] = `
-
-`;
-
-exports[`Activity log > should open the detail of an entry #1`] = `
- editor (58ada34d299e82674854510e)
+ editor (58ada34d299e82674854510e)
UPDATE UPDATE
@@ -178,7 +108,7 @@ exports[`Activity log > should open the detail of an entry #1`] = `
>Body {"_id":"58ada34d299e82674854510e","username":"editor","__v":0,"role":"editor","groups":[{"_id":"5fda2b675917fe58048a88f4","name":"Activistas"},{"_id":"5fda2b6f5917fe58048a88f6","name":"Asesores
+ >{"_id":"58ada34d299e82674854510e","username":"editor","__v":1,"role":"editor","groups":[{"_id":"5fda2b675917fe58048a88f4","name":"Activistas"},{"_id":"5fda2b6f5917fe58048a88f6","name":"Asesores
legales"}],"email":"editor@uwazi.com"}
diff --git a/cypress/e2e/settings/languages.cy.ts b/cypress/e2e/settings/languages.cy.ts
index 91c43de4ea..a62e2429de 100644
--- a/cypress/e2e/settings/languages.cy.ts
+++ b/cypress/e2e/settings/languages.cy.ts
@@ -1,15 +1,29 @@
import { clearCookiesAndLogin } from '../helpers/login';
import 'cypress-axe';
-const addLanguages = (languages: string[]) => {
- languages.forEach(lang => {
- cy.clearAndType('[data-testid=modal] input[type=text]', lang);
- cy.contains('button', lang).click();
+const stringToTranslate = "*please keep this key secret and don't share it.";
+
+const addLanguages = () => {
+ cy.contains('Install Language').click();
+ cy.get('[data-testid=modal]')
+ .should('be.visible')
+ .within(() => {
+ cy.get('input[type=text]').realClick().realType('Spanish');
+ cy.contains('button', 'Spanish').should('be.visible').realClick();
+ cy.get('input[type=text]').clear();
+ cy.get('input[type=text]').realType('French');
+ cy.contains('button', 'French').should('be.visible').realClick();
+ cy.get('input[type=text]').clear();
+ cy.contains('label', '(2)').click();
+ cy.contains('span', '* French (fr)').should('be.visible');
+ cy.contains('span', '* Spanish (es)').should('be.visible');
+ });
+ cy.get('[data-testid=modal]').within(() => {
+ cy.contains('button', 'Install (2)').realClick();
});
+ cy.get('[data-testid=modal]').should('not.exist');
};
-const stringToTranslate = "*please keep this key secret and don't share it.";
-
describe('Languages', () => {
before(() => {
cy.blankState();
@@ -22,26 +36,31 @@ describe('Languages', () => {
describe('Languages List', () => {
it('should open the install language modal', () => {
cy.contains('Install Language').click();
+ cy.get('[data-testid=modal]').should('be.visible');
cy.checkA11y();
+ cy.get('[data-testid=modal]').within(() => {
+ cy.contains('button', 'Cancel').click();
+ });
});
it('should install new languages', () => {
const BACKEND_LANGUAGE_INSTALL_DELAY = 25000;
cy.intercept('POST', 'api/translations/languages').as('addLanguage');
- addLanguages(['Spanish', 'French']);
- cy.contains('[data-testid=modal] button', 'Install').click();
+
+ addLanguages();
+
cy.wait('@addLanguage');
cy.contains('Dismiss').click();
- cy.contains('Spanish', { timeout: BACKEND_LANGUAGE_INSTALL_DELAY });
- cy.contains('French', { timeout: BACKEND_LANGUAGE_INSTALL_DELAY });
+ cy.contains('tr', 'Spanish', { timeout: BACKEND_LANGUAGE_INSTALL_DELAY });
+ cy.contains('tr', 'French', { timeout: BACKEND_LANGUAGE_INSTALL_DELAY });
cy.contains('Languages installed successfully').click();
});
it('should render the list of installed languages', () => {
cy.get('[data-testid=settings-languages]').toMatchImageSnapshot();
- cy.contains('English');
- cy.contains('Spanish');
- cy.contains('French');
+ cy.contains('tr', 'English');
+ cy.contains('tr', 'Spanish');
+ cy.contains('tr', 'French');
cy.checkA11y();
});
});
diff --git a/cypress/e2e/settings/translations.cy.ts b/cypress/e2e/settings/translations.cy.ts
index d32c6af17f..9458c24f75 100644
--- a/cypress/e2e/settings/translations.cy.ts
+++ b/cypress/e2e/settings/translations.cy.ts
@@ -52,7 +52,7 @@ describe('Translations', () => {
});
it('should have breadcrumb navigation', () => {
- cy.contains('li > a > .translation', 'Translations').click();
+ cy.contains('li a > .translation', 'Translations').click();
cy.contains('caption', 'System translations');
});
@@ -145,12 +145,14 @@ describe('Translations', () => {
it('should translate a text', () => {
cy.contains('span', 'Filters').click();
- cy.get('input[id=es]').clear();
- cy.get('input[id=es]').type('Filtros', { delay: 0 });
- cy.get('input[id=en]').clear();
- cy.get('input[id=en]').type('Filtering', { delay: 0 });
- cy.contains('button', 'Submit').click();
- cy.get('[data-testid=modal]').should('not.exist');
+ cy.get('#translationsFormModal').within(() => {
+ cy.get('input[name="data.1.value"]').clear();
+ cy.get('input[name="data.1.value"]').type('Filtros', { delay: 0 });
+ cy.get('input[name="data.0.value"]').clear();
+ cy.get('input[name="data.0.value"]').type('Filtering', { delay: 0 });
+ cy.contains('button', 'Save').click();
+ });
+ cy.get('#translationsFormModal').should('not.exist');
});
it('should deactive the live translate and check the translatations in english and spanish', () => {
diff --git a/tailwind.config.js b/tailwind.config.js
index 028918c9b4..6aeeb98522 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -3,7 +3,7 @@ const colors = require('tailwindcss/colors');
module.exports = {
content: [
- './app/react/V2/**/*.{js,jsx,ts,tsx}',
+ './app/react/**/*.{js,jsx,ts,tsx}',
'./app/react/stories/**/*.{js,jsx,ts,tsx}',
'node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}',
'node_modules/flowbite-datepicker/**/*.{js,jsx,ts,tsx,css}',