tachybase_todo/packages/plugins/@nocobase/plugin-localization-management/src/client/Localization.tsx
Zeke Zhang 6bd0568d08
chore: upgrade antd to v5.12.2 (#3185)
* chore: upgrade antd to v5.12.2

* refactor: rename to StablePopover

* fix: fix tests

* test: fix e2e

* chore(CI): optimize E2E workflow

* chore: fix playwright

* fix: re-generate yarn.lock to fix e2e errors

* test: update locator

* fix: fix build errors

* fix: fix unit tests

* fix: fix e2e tests

* fix: should not auto close when clicking menu item

* fix: fix column width

* test: increase e2e timeout-minutes to 90

* test: fix e2e

* fix: update dockerfile

* fix: npmignore

* fix: app component false

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2023-12-18 15:21:57 +08:00

289 lines
7.7 KiB
TypeScript

import { SyncOutlined } from '@ant-design/icons';
import { Form, createForm } from '@formily/core';
import { Field, useField, useForm } from '@formily/react';
import {
FormProvider,
Input,
Radio,
SchemaComponent,
Select,
StablePopover,
locale,
useAPIClient,
useActionContext,
useRecord,
useResourceActionContext,
useResourceContext,
} from '@nocobase/client';
import { useMemoizedFn } from 'ahooks';
import { Input as AntdInput, Button, Card, Checkbox, Col, Divider, Row, Tag, Typography, message } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { useLocalTranslation } from './locale';
import { localizationSchema } from './schemas/localization';
const { Text } = Typography;
const useUpdateTranslationAction = () => {
const field = useField();
const form = useForm();
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
const { targetKey } = useResourceContext();
const { [targetKey]: textId } = useRecord();
const api = useAPIClient();
const locale = api.auth.getLocale();
return {
async run() {
await form.submit();
field.data = field.data || {};
field.data.loading = true;
try {
await api.resource('localizationTranslations').updateOrCreate({
filterKeys: ['textId', 'locale'],
values: {
textId,
locale,
translation: form.values.translation,
},
});
ctx.setVisible(false);
await form.reset();
refresh();
} catch (e) {
console.log(e);
} finally {
field.data.loading = false;
}
},
};
};
const useDestroyTranslationAction = () => {
const { refresh } = useResourceActionContext();
const api = useAPIClient();
const { translationId: filterByTk } = useRecord();
return {
async run() {
await api.resource('localizationTranslations').destroy({ filterByTk });
refresh();
},
};
};
const useBulkDestroyTranslationAction = () => {
const { state, setState, refresh } = useResourceActionContext();
const api = useAPIClient();
const { t } = useLocalTranslation();
return {
async run() {
if (!state?.selectedRowKeys?.length) {
return message.error(t('Please select the records you want to delete'));
}
await api.resource('localizationTranslations').destroy({ filterByTk: state?.selectedRowKeys });
setState?.({ selectedRowKeys: [] });
refresh();
},
};
};
const usePublishAction = () => {
const api = useAPIClient();
return {
async run() {
await api.resource('localization').publish();
window.location.reload();
},
};
};
const Sync = () => {
const { t } = useLocalTranslation();
const { refresh } = useResourceActionContext();
const api = useAPIClient();
const [loading, setLoading] = useState(false);
const plainOptions = ['local', 'menu', 'db'];
const [checkedList, setCheckedList] = useState<any[]>(plainOptions);
const [indeterminate, setIndeterminate] = useState(false);
const [checkAll, setCheckAll] = useState(true);
const onChange = (list: any[]) => {
setCheckedList(list);
setIndeterminate(!!list.length && list.length < plainOptions.length);
setCheckAll(list.length === plainOptions.length);
};
const onCheckAllChange = (e) => {
setCheckedList(e.target.checked ? plainOptions : []);
setIndeterminate(false);
setCheckAll(e.target.checked);
};
return (
<StablePopover
placement="bottomRight"
content={
<>
<Checkbox indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
{t('All')}
</Checkbox>
<Divider style={{ margin: '5px 0' }} />
<Checkbox.Group onChange={onChange} value={checkedList}>
<Col>
<Row>
<Checkbox value="local">{t('System & Plugins')}</Checkbox>
</Row>
<Row>
<Checkbox value="db">{t('Collections & Fields')}</Checkbox>
</Row>
<Row>
<Checkbox value="menu">{t('Menu')}</Checkbox>
</Row>
</Col>
</Checkbox.Group>
</>
}
>
<Button
icon={<SyncOutlined />}
loading={loading}
onClick={async () => {
if (!checkedList.length) {
return message.error(t('Please select the resources you want to synchronize'));
}
setLoading(true);
await api.resource('localization').sync({
values: {
type: checkedList,
},
});
setLoading(false);
refresh();
}}
>
{t('Sync')}
</Button>
</StablePopover>
);
};
const useModules = () => {
const { t: lang } = useLocalTranslation();
const t = useMemoizedFn(lang);
const { data } = useResourceActionContext();
return useMemo(
() =>
data?.meta?.modules?.map((module) => ({
value: module.value,
label: t(module.label),
})) || [],
[data?.meta?.modules, t],
);
};
const Filter = () => {
const { t } = useLocalTranslation();
const { run } = useResourceActionContext();
const modules = useModules();
const form = useMemo<Form>(
() =>
createForm({
initialValues: {
hasTranslation: true,
},
}),
[],
);
const filter = (values?: any) => {
run({
...(values || form.values),
});
};
useEffect(() => {
const module = form.query('module').take() as any;
module.dataSource = modules;
}, [form, modules]);
return (
<FormProvider form={form}>
<div style={{ display: 'flex' }}>
<Field
name="module"
dataSource={modules}
component={[
Select,
{
allowClear: true,
placeholder: t('Module'),
onChange: (module: string) => filter({ ...form.values, module }),
},
]}
/>
<Field
name="keyword"
component={[
AntdInput.Search,
{
placeholder: t('Keyword'),
allowClear: true,
style: {
marginLeft: '8px',
width: 'fit-content',
},
onSearch: (keyword) => filter({ ...form.values, keyword }),
},
]}
/>
<Field
name="hasTranslation"
dataSource={[
{ label: t('All'), value: true },
{ label: t('No translation'), value: false },
]}
component={[
Radio.Group,
{
defaultValue: true,
style: {
marginLeft: '8px',
width: 'fit-content',
},
optionType: 'button',
onChange: () => filter(),
},
]}
/>
</div>
</FormProvider>
);
};
export const Localization = () => {
const { t } = useLocalTranslation();
const api = useAPIClient();
const curLocale = api.auth.getLocale();
const localeLabel = locale[curLocale]?.label || curLocale;
const CurrentLang = () => (
<Typography>
<Text strong>{t('Current language')}</Text>
<Tag style={{ marginLeft: '10px' }}>{localeLabel}</Tag>
</Typography>
);
const TranslationField = (props) => (props.value !== undefined ? <Input.TextArea {...props} /> : <div></div>);
return (
<Card bordered={false}>
<SchemaComponent
schema={localizationSchema}
components={{ TranslationField, CurrentLang, Sync, Filter }}
scope={{
t,
useDestroyTranslationAction,
useBulkDestroyTranslationAction,
useUpdateTranslationAction,
usePublishAction,
useModules,
}}
/>
</Card>
);
};