Merge branch 'next' into develop

This commit is contained in:
Zeke Zhang 2024-12-05 16:36:42 +08:00
commit 92e87ee1da
9 changed files with 273 additions and 12 deletions

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { APIClient as APIClientSDK, getSubAppName } from '@nocobase/sdk';
import { APIClient as APIClientSDK } from '@nocobase/sdk';
import { Result } from 'ahooks/es/useRequest/src/types';
import { notification } from 'antd';
import React from 'react';
@ -91,10 +91,10 @@ export class APIClient extends APIClientSDK {
interceptors() {
this.axios.interceptors.request.use((config) => {
config.headers['X-With-ACL-Meta'] = true;
const appName = this.app ? getSubAppName(this.app.getPublicPath()) : null;
if (appName) {
config.headers['X-App'] = appName;
}
const headers = this.getHeaders();
Object.keys(headers).forEach((key) => {
config.headers[key] = headers[key];
});
return config;
});
super.interceptors();

View File

@ -83,7 +83,16 @@ export const FormItem: any = withDynamicSchemaProps(
return (
<CollectionFieldProvider allowNull={true}>
<BlockItem className={'nb-form-item'}>
<BlockItem
className={cx(
'nb-form-item',
css`
.ant-formily-item-layout-horizontal .ant-formily-item-control {
max-width: ${showTitle === false ? '100% !important' : null};
}
`,
)}
>
<ACLCollectionFieldProvider>
<Item className={className} {...props} extra={extra} wrapperStyle={wrapperStyle} />
</ACLCollectionFieldProvider>

View File

@ -723,6 +723,8 @@ const InternalNocoBaseTable = React.memo(
field,
...others
} = props;
const { token } = useToken();
return (
<div
className={cx(
@ -747,10 +749,10 @@ const InternalNocoBaseTable = React.memo(
padding: 16px 8px;
}
.ant-table-middle .ant-table-cell {
padding: 12px 8px;
padding: 12px ${token.paddingXS}px;
}
.ant-table-small .ant-table-cell {
padding: 8px 8px;
padding: 8px ${token.paddingXS}px;
}
}
}

View File

@ -71,8 +71,8 @@ interface Resource {
}
interface ExtendedAgent extends SuperAgentTest {
login: (user: any) => ExtendedAgent;
loginUsingId: (userId: number) => ExtendedAgent;
login: (user: any, roleName?: string) => ExtendedAgent;
loginUsingId: (userId: number, roleName?: string) => ExtendedAgent;
resource: (name: string, resourceOf?: any) => Resource;
}
@ -124,13 +124,14 @@ export class MockServer extends Application {
const proxy = new Proxy(agent, {
get(target, method: string, receiver) {
if (['login', 'loginUsingId'].includes(method)) {
return (userOrId: any) => {
return (userOrId: any, roleName?: string) => {
return proxy
.auth(
jwt.sign(
{
userId: typeof userOrId === 'number' ? userOrId : userOrId?.id,
temp: true,
roleName,
},
process.env.APP_KEY,
{

View File

@ -92,6 +92,7 @@ const InternalIcons = () => {
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
margin: 0 0 0 0;
}
.ant-list-item-meta-title button {
font-size: 14px;

View File

@ -47,6 +47,10 @@ export const useStyles = genStyleHook('nb-mobile', (token) => {
'.nb-action-panel': {
paddingTop: '10px',
},
'.ant-list-item': {
paddingTop: '8px',
paddingBottom: '8px',
},
'.nb-action-panel .ant-avatar-circle': {
width: '48px !important',
height: '48px !important',

View File

@ -22,6 +22,9 @@ import {
useRequest,
ACLCustomContext,
VariablesProvider,
AssociationField,
Action,
DatePicker,
} from '@nocobase/client';
import { css } from '@emotion/css';
import { isDesktop } from 'react-device-detect';
@ -31,6 +34,10 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
import { useParams } from 'react-router';
import { usePublicSubmitActionProps } from '../hooks';
import { UnEnabledFormPlaceholder, UnFoundFormPlaceholder } from './UnEnabledFormPlaceholder';
import { Button as MobileButton, Dialog as MobileDialog } from 'antd-mobile';
import { MobilePicker } from './components/MobilePicker';
import { MobileDateTimePicker } from './components/MobileDatePicker';
class PublicDataSource extends DataSource {
async getDataSource() {
return {};
@ -110,10 +117,43 @@ const PublicFormMessageProvider = ({ children }) => {
</PublicFormMessageContext.Provider>
);
};
function isMobile() {
return window.matchMedia('(max-width: 768px)').matches;
}
const AssociationFieldMobile = (props) => {
return <AssociationField {...props} popupMatchSelectWidth={true} />;
};
AssociationFieldMobile.SubTable = AssociationField.SubTable;
AssociationFieldMobile.Nester = AssociationField.Nester;
AssociationFieldMobile.AddNewer = Action.Container;
AssociationFieldMobile.Selector = Action.Container;
AssociationFieldMobile.Viewer = Action.Container;
AssociationFieldMobile.InternalSelect = AssociationField.InternalSelect;
AssociationFieldMobile.ReadPretty = AssociationField.ReadPretty;
AssociationFieldMobile.FileSelector = AssociationField.FileSelector;
const DatePickerMobile = (props) => {
return <MobileDateTimePicker {...props} />;
};
DatePickerMobile.FilterWithPicker = DatePicker.FilterWithPicker;
DatePickerMobile.RangePicker = DatePicker.RangePicker;
const mobileComponents = {
Button: MobileButton,
Select: (props) => {
return <MobilePicker {...props} />;
},
DatePicker: DatePickerMobile,
UnixTimestamp: MobileDateTimePicker,
Modal: MobileDialog,
AssociationField: AssociationFieldMobile,
};
function InternalPublicForm() {
const params = useParams();
const apiClient = useAPIClient();
const isMobileMedia = isMobile();
const { error, data, loading, run } = useRequest<any>(
{
url: `publicForms:getMeta/${params.name}`,
@ -189,6 +229,7 @@ function InternalPublicForm() {
if (!data?.data) {
return <UnEnabledFormPlaceholder />;
}
const components = isMobileMedia ? mobileComponents : {};
return (
<ACLCustomContext.Provider value={{ allowAll: true }}>
<PublicAPIClientProvider>
@ -217,7 +258,7 @@ function InternalPublicForm() {
scope={{
useCreateActionProps: usePublicSubmitActionProps,
}}
components={{ PublicFormMessageProvider: PublicFormMessageProvider }}
components={{ PublicFormMessageProvider: PublicFormMessageProvider, ...components }}
/>
</SchemaComponentContext.Provider>
</VariablesProvider>

View File

@ -0,0 +1,91 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useState, useCallback } from 'react';
import { DatePicker } from 'antd-mobile';
import { mapDatePicker, DatePicker as NBDatePicker } from '@nocobase/client';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { useTranslation } from 'react-i18next';
const MobileDateTimePicker = connect(
(props) => {
const { t } = useTranslation();
const {
value,
onChange,
dateFormat = 'YYYY-MM-DD',
timeFormat = 'HH:mm',
showTime = false,
picker,
...rest
} = props;
const [visible, setVisible] = useState(false);
// 性能优化:使用 useCallback 缓存函数
const handleConfirm = useCallback(
(value) => {
setVisible(false);
const selectedDateTime = new Date(value);
onChange(selectedDateTime);
},
[showTime, onChange],
);
// 清空选择的日期和时间
const handleClear = useCallback(() => {
setVisible(false);
onChange(null);
}, [onChange]);
const labelRenderer = useCallback((type: string, data: number) => {
switch (type) {
case 'year':
return data;
case 'quarter':
return data;
default:
return data;
}
}, []);
return (
<>
<div contentEditable="false" onClick={() => setVisible(true)}>
<NBDatePicker
onClick={() => setVisible(true)}
value={value}
picker={picker}
{...rest}
popupStyle={{ display: 'none' }}
style={{ pointerEvents: 'none', width: '100%' }}
/>
</div>
<DatePicker
cancelText={t('Cancel')}
confirmText={t('Confirm')}
visible={visible}
title={<a onClick={handleClear}>{t('Clear')}</a>}
onClose={() => {
setVisible(false);
}}
precision={showTime ? 'second' : picker === 'date' ? 'day' : picker}
renderLabel={labelRenderer}
min={new Date(1000, 0, 1)}
max={new Date(9999, 11, 31)}
onConfirm={(val) => {
handleConfirm(val);
}}
/>
</>
);
},
mapProps(mapDatePicker()),
mapReadPretty(NBDatePicker.ReadPretty),
);
export { MobileDateTimePicker };

View File

@ -0,0 +1,112 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
import { connect, mapProps } from '@formily/react';
import { Select } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
const MobilePicker = connect(
(props) => {
const { value, onChange, options = [], mode } = props;
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [selected, setSelected] = useState(value || []);
const [searchText, setSearchText] = useState(null);
const filteredItems = useMemo(() => {
if (searchText) {
return options.filter((item) => item.label.toLowerCase().includes(searchText.toLowerCase()));
}
return options;
}, [options, searchText]);
const handleConfirm = () => {
onChange(selected);
setVisible(false);
};
useEffect(() => {
!visible && setSearchText(null);
}, [visible]);
return (
<>
<div contentEditable="false" onClick={() => setVisible(true)}>
<Select
placeholder={t('Select')}
value={value}
dropdownStyle={{ display: 'none' }}
multiple={mode === 'multiple'}
onClear={() => {
setVisible(false);
onChange(null);
setSelected(null);
}}
onFocus={(e) => e.preventDefault()}
style={{ pointerEvents: 'none' }}
/>
</div>
<Popup
visible={visible}
onMaskClick={() => {
setVisible(false);
if (!value || value?.length === 0) {
setSelected([]);
}
}}
destroyOnClose
>
<div style={{ margin: '10px' }}>
<SearchBar
placeholder={t('search')}
value={searchText}
onChange={(v) => setSearchText(v)}
showCancelButton
/>
</div>
<div
style={{
maxHeight: '60vh',
overflowY: 'auto',
}}
>
<CheckList
multiple={mode === 'multiple'}
value={Array.isArray(selected) ? selected : [selected] || []}
onChange={(val) => {
if (mode === 'multiple') {
setSelected(val);
} else {
setSelected(val[0]);
onChange(val[0]);
setVisible(false);
}
}}
>
{filteredItems.map((item) => (
<CheckList.Item key={item.value} value={item.value}>
{item.label}
</CheckList.Item>
))}
</CheckList>
</div>
{mode === 'multiple' && (
<Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
{t('Confirm')}
</Button>
)}
</Popup>
</>
);
},
mapProps({ dataSource: 'options' }),
);
export { MobilePicker };