Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions translate/src/api/other-locales.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { fetchAllLocales } from './other-locales';
import { GET } from './utils/base';

vi.mock('./utils/base', () => ({
GET: vi.fn(),
}));

describe('fetchAllLocales', () => {
afterEach(() => {
vi.mocked(GET).mockReset();
});

describe('all-projects', () => {
it('hits the global locale list endpoint, not a project endpoint', async () => {
vi.mocked(GET).mockResolvedValueOnce({ results: [], next: null });

await fetchAllLocales();

expect(GET).toHaveBeenCalledWith(
expect.stringMatching(/^\/api\/v2\/locales\/\?/),
);
expect(GET).not.toHaveBeenCalledWith(
expect.stringContaining('/api/v2/projects/'),
);
});

it('follows pagination until next is null', async () => {
vi.mocked(GET)
.mockResolvedValueOnce({
results: [{ code: 'a', name: 'Locale A' }],
next: '/api/v2/locales/?page=2',
})
.mockResolvedValueOnce({
results: [{ code: 'b', name: 'Locale B' }],
next: null,
});

const result = await fetchAllLocales();

expect(GET).toHaveBeenCalledTimes(2);
expect(result).toEqual([
{ code: 'a', name: 'Locale A' },
{ code: 'b', name: 'Locale B' },
]);
});

it('stops looping and does not hang if next is missing rather than null', async () => {
vi.mocked(GET).mockResolvedValueOnce({
results: [{ code: 'a', name: 'A' }],
});
const result = await fetchAllLocales();
expect(result).toEqual([{ code: 'a', name: 'A' }]);
});
});
});
11 changes: 10 additions & 1 deletion translate/src/api/project.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GET } from './utils/base';
import type { LocaleOption } from './other-locales';

export type Tag = {
readonly slug: string;
Expand All @@ -11,8 +12,16 @@ export type Project = {
name: string;
info: string;
tags: Tag[];
locales: LocaleOption[];
};

export async function fetchProject(slug: string): Promise<Project> {
return await GET(`/api/v2/projects/${slug}`);
const result = await GET(`/api/v2/projects/${slug}`);

return {
...result,
locales: (result?.localizations ?? []).map(
(l: { locale: LocaleOption }) => l.locale,
),
};
}
29 changes: 18 additions & 11 deletions translate/src/modules/batchactions/components/BatchActions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react';

import * as Hooks from '~/hooks';
import * as Actions from '../actions';
import * as OtherLocales from '~/api/other-locales';
import { BATCHACTIONS } from '../reducer';

import { BatchActions } from './BatchActions';
import { vi } from 'vitest';
import { fireEvent, render } from '@testing-library/react';
import { MockLocalizationProvider } from '~/test/utils';
import { PROJECT } from '~/modules/project';

const DEFAULT_BATCH_ACTIONS = {
entities: [],
Expand All @@ -17,20 +17,28 @@ const DEFAULT_BATCH_ACTIONS = {
response: null,
};

vi.mock('~/hooks', () => ({
useAppDispatch: vi.fn(() => vi.fn()),
useAppSelector: vi.fn((selector) =>
selector({ [BATCHACTIONS]: DEFAULT_BATCH_ACTIONS }),
),
}));

vi.mock('../actions', () => ({
resetSelection: vi.fn(() => ({ type: 'whatever' })),
selectAll: vi.fn(() => ({ type: 'whatever' })),
}));

vi.mock('~/api/other-locales', () => ({
fetchAllLocales: vi.fn(() => Promise.resolve([])),
const DEFAULT_PROJECT_STATE = {
fetching: false,
slug: '',
name: '',
info: '',
tags: [],
locales: [],
};

vi.mock('~/hooks', () => ({
Comment thread
MundiaNderi marked this conversation as resolved.
useAppDispatch: vi.fn(() => vi.fn()),
useAppSelector: vi.fn((selector) =>
selector({
[BATCHACTIONS]: DEFAULT_BATCH_ACTIONS,
[PROJECT]: DEFAULT_PROJECT_STATE,
}),
),
}));

describe('<BatchActions>', () => {
Expand All @@ -39,7 +47,6 @@ describe('<BatchActions>', () => {
Hooks.useAppSelector.mockRestore();
Actions.resetSelection.mockRestore();
Actions.selectAll.mockRestore();
OtherLocales.fetchAllLocales.mockClear();
});

const WrapBatchAction = () => {
Expand Down
30 changes: 22 additions & 8 deletions translate/src/modules/batchactions/components/BatchActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import './BatchActions.css';
import { RejectAll } from './RejectAll';
import { ReplaceAll } from './ReplaceAll';
import { CopyFromLocale } from './CopyFromLocale';
import { fetchAllLocales } from '~/api/other-locales';
import type { LocaleOption } from '~/api/other-locales';
import LocaleMenu from '~/modules/locale/components/LocaleMenu';
import { useProject } from '~/modules/project';
import { fetchAllLocales, LocaleOption } from '~/api/other-locales';

/**
* Renders batch editor, used for performing mass actions on translations.
*/
Expand All @@ -35,8 +36,27 @@ export function BatchActions(): React.ReactElement<'div'> {
const replace = useRef<HTMLInputElement>(null);

const [otherLocale, setOtherLocale] = useState('');

const { slug, locales: projectLocales } = useProject();
const [locales, setLocales] = useState<LocaleOption[]>([]);

useEffect(() => {
let cancelled = false;

if (slug === 'all-projects') {
fetchAllLocales().then((result) => {
if (!cancelled) {
setLocales(result);
}
});
} else {
setLocales(projectLocales);
}
return () => {
cancelled = true;
};
}, [slug, projectLocales]);

Comment thread
MundiaNderi marked this conversation as resolved.
const quitBatchActions = useCallback(() => dispatch(resetSelection()), []);

useEffect(() => {
Expand All @@ -50,12 +70,6 @@ export function BatchActions(): React.ReactElement<'div'> {
return () => document.removeEventListener('keydown', handleShortcuts);
}, []);

useEffect(() => {
fetchAllLocales().then((all) => {
setLocales(all);
});
}, []);

const selectAllEntities = useCallback(
() => dispatch(selectAll(location)),
[location],
Expand Down
8 changes: 6 additions & 2 deletions translate/src/modules/project/actions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LocaleOption } from '~/api/other-locales';
import { fetchProject, Tag } from '~/api/project';
import type { AppDispatch } from '~/store';

Expand All @@ -9,6 +10,7 @@ export type Action = ReceiveAction | RequestAction;
/** Notify that project data is being fetched. */
type RequestAction = {
readonly type: typeof REQUEST;
readonly slug: string;
};

/** Receive project data. */
Expand All @@ -18,22 +20,24 @@ type ReceiveAction = {
readonly name: string;
readonly info: string;
readonly tags: Tag[];
readonly locales: LocaleOption[];
};

/**
* Get data about the current project.
*/
export const getProject = (slug: string) => async (dispatch: AppDispatch) => {
// When 'all-projects' are selected, we do not fetch data.
dispatch({ type: REQUEST, slug });
if (slug !== 'all-projects') {
dispatch({ type: REQUEST });
const { info, name, slug: slug_, tags } = await fetchProject(slug);
const { info, name, slug: slug_, tags, locales } = await fetchProject(slug);
dispatch({
type: RECEIVE,
slug: slug_,
name: name,
info: info,
tags: tags.sort((a, b) => b.priority - a.priority),
locales: locales,
});
}
};
13 changes: 13 additions & 0 deletions translate/src/modules/project/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Tag } from '~/api/project';

import { Action, RECEIVE, REQUEST } from './actions';
import { LocaleOption } from '~/api/other-locales';

// Name of this module.
// Used as the key to store this module's reducer.
Expand All @@ -12,6 +13,7 @@ export type ProjectState = {
readonly name: string;
readonly info: string;
readonly tags: Tag[];
readonly locales: LocaleOption[];
};

const initial: ProjectState = {
Expand All @@ -20,6 +22,7 @@ const initial: ProjectState = {
name: '',
info: '',
tags: [],
locales: [],
};

export function reducer(
Expand All @@ -28,9 +31,18 @@ export function reducer(
): ProjectState {
switch (action.type) {
case REQUEST:
if (action.slug === 'all-projects') {
return {
...initial,
slug: action.slug,
};
}

return {
Comment thread
MundiaNderi marked this conversation as resolved.
...state,
fetching: true,
slug: action.slug,
locales: [],
};
case RECEIVE:
return {
Expand All @@ -40,6 +52,7 @@ export function reducer(
name: action.name,
info: action.info,
tags: action.tags,
locales: action.locales,
};
default:
return state;
Expand Down