Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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' }]);
});
});
});
8 changes: 1 addition & 7 deletions translate/src/api/other-locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,15 @@ export async function fetchOtherLocales(
}

export async function fetchAllLocales(): Promise<LocaleOption[]> {
const search = new URLSearchParams({
fields: 'code,name',
ordering: 'name',
});

const search = new URLSearchParams({ fields: 'code,name', ordering: 'name' });
const locales: LocaleOption[] = [];
let url: string | null = `/api/v2/locales/?${search}`;

while (url) {
const result = await GET(url);
if (Array.isArray(result?.results)) {
locales.push(...result.results);
}
url = result?.next ?? null;
}

Comment thread
MundiaNderi marked this conversation as resolved.
return locales;
}
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,
),
};
}
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 @@ -29,8 +29,23 @@ vi.mock('../actions', () => ({
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 +54,6 @@ describe('<BatchActions>', () => {
Hooks.useAppSelector.mockRestore();
Actions.resetSelection.mockRestore();
Actions.selectAll.mockRestore();
OtherLocales.fetchAllLocales.mockClear();
});

const WrapBatchAction = () => {
Expand Down
12 changes: 3 additions & 9 deletions translate/src/modules/batchactions/components/BatchActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ 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';
/**
* Renders batch editor, used for performing mass actions on translations.
*/
Expand All @@ -35,7 +34,8 @@ export function BatchActions(): React.ReactElement<'div'> {
const replace = useRef<HTMLInputElement>(null);

const [otherLocale, setOtherLocale] = useState('');
const [locales, setLocales] = useState<LocaleOption[]>([]);

const { locales } = useProject();
Comment thread
MundiaNderi marked this conversation as resolved.
Outdated

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

Expand All @@ -50,12 +50,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
5 changes: 4 additions & 1 deletion 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 @@ -18,6 +19,7 @@ type ReceiveAction = {
readonly name: string;
readonly info: string;
readonly tags: Tag[];
readonly locales: LocaleOption[];
};

/**
Expand All @@ -27,13 +29,14 @@ export const getProject = (slug: string) => async (dispatch: AppDispatch) => {
// When 'all-projects' are selected, we do not fetch data.
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,
});
}
};
4 changes: 4 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 @@ -40,6 +43,7 @@ export function reducer(
name: action.name,
info: action.info,
tags: action.tags,
locales: action.locales,
};
default:
return state;
Expand Down