Merge branch 'main' into next

This commit is contained in:
Zeke Zhang 2025-02-25 05:57:38 +08:00
commit 739fecbbe7
18 changed files with 221 additions and 236 deletions

View File

@ -52,8 +52,14 @@ module.exports = (cli) => {
}
};
const pkg = require('../../package.json');
let distTag = 'latest';
if (pkg.version.includes('alpha')) {
distTag = 'alpha';
} else if (pkg.version.includes('beta')) {
distTag = 'beta';
}
// get latest version
const { stdout } = await run('npm', ['info', options.next ? '@nocobase/cli@next' : '@nocobase/cli', 'version'], {
const { stdout } = await run('npm', ['info', `@nocobase/cli@${distTag}`, 'version'], {
stdio: 'pipe',
});
if (pkg.version === stdout) {
@ -62,13 +68,7 @@ module.exports = (cli) => {
await rmAppDir();
return;
}
const currentY = 1 * pkg.version.split('.')[1];
const latestY = 1 * stdout.split('.')[1];
if (options.next || currentY > latestY) {
await run('yarn', ['add', '@nocobase/cli@next', '@nocobase/devtools@next', '-W']);
} else {
await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']);
}
await run('yarn', ['add', `@nocobase/cli@${distTag}`, `@nocobase/devtools@${distTag}`, '-W']);
await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade');

View File

@ -34,6 +34,7 @@ test.describe('where to open a popup and what can be added to it', () => {
// add blocks
await page.getByLabel('schema-initializer-Grid-popup:addNew:addBlock-general').hover();
await page.getByText('Markdown').click();
await page.waitForTimeout(500);
await page.getByLabel('schema-initializer-Grid-popup:addNew:addBlock-general').hover();
await page.getByText('Form').hover();
await page.getByRole('menuitem', { name: 'Current collection' }).click();

View File

@ -18,24 +18,12 @@ import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'
import { useAssociationFieldContext, useInsertSchema } from './hooks';
import schema from './schema';
const InternalNesterCss = css`
& .ant-formily-item-layout-vertical {
margin-bottom: 10px;
}
.ant-card-body {
padding: 15px 20px 5px;
}
.ant-divider-horizontal {
margin: 10px 0;
}
`;
const InternalNesterCardCss = css`
.ant-card-bordered {
border: none;
}
.ant-card-body {
padding: 0px 20px 20px 0px;
padding: 0px 20px 0px 0px;
}
`;
@ -56,6 +44,20 @@ export const InternalNester = observer(
labelWrap = true,
} = fieldSchema?.['x-component-props'] || {};
const InternalNesterCss = css`
margin-top: 0.4em;
& .ant-formily-item-layout-vertical {
margin-bottom: 10px;
}
.ant-card-body {
padding: ${token.padding}px ${token.paddingLG}px;
}
.ant-divider-horizontal {
margin: 10px 0;
}
`;
useEffect(() => {
insertNester(schema.Nester);
}, []);

View File

@ -11,8 +11,19 @@ import { genStyleHook } from '../__builtins__';
const useStyles = genStyleHook('nb-grid-card', (token) => {
const { componentCls } = token;
return {
[componentCls]: {
'.nb-action-bar': {
borderRadius: token.borderRadiusBlock,
marginBottom: `${token.marginBlock / 2}px !important`,
},
'.ant-list-pagination': {
borderRadius: token.borderRadiusBlock,
marginTop: `${token.marginBlock / 2}px !important`,
},
'& > .nb-block-item': {
marginBottom: token.marginLG,
'& > .nb-action-bar:has(:first-child:not(:empty))': {

View File

@ -28,15 +28,10 @@ const itemCss = css`
const gridCardCss = css`
height: 100%;
> .ant-card-body {
padding: 24px 24px 0px;
height: 100%;
button {
margin-bottom: 0px !important;
margin-top: 5px;
}
}
.nb-action-bar {
padding: 5px 0;
padding-top: 5px;
}
`;

View File

@ -17,6 +17,7 @@ import { getCardItemSchema } from '../../../block-provider';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent';
import { useToken } from '../../../style/useToken';
import { SortableItem } from '../../common';
import { SchemaComponentOptions } from '../../core';
import { useDesigner, useProps } from '../../hooks';
@ -141,6 +142,7 @@ const InternalGridCard = withSkeletonComponent(
},
[fieldSchema.properties],
);
const { token } = useToken();
const onPaginationChange: PaginationProps['onChange'] = useCallback(
(page, pageSize) => {
@ -207,7 +209,7 @@ const InternalGridCard = withSkeletonComponent(
...columnCount,
sm: columnCount.xs,
xl: columnCount.lg,
gutter: [rowGutter, rowGutter],
gutter: [token.marginBlock / 2, token.marginBlock / 2],
}}
renderItem={(item, index) => {
return (

View File

@ -400,7 +400,7 @@ export function Uploader({ rules, ...props }: UploadProps) {
if (pendingFiles.length) {
setUploadedList(valueList);
} else {
onChange?.([...value, ...valueList]);
onChange?.([...(value || []), ...valueList]);
setUploadedList([]);
}
}

View File

@ -18,13 +18,20 @@ import { WorkbenchLayout } from './workbenchBlockSettings';
const useStyles = createStyles(({ token, css }) => ({
// 支持 css object 的写法
action: css`
display: flex;
background-color: transparent;
border: 0;
height: auto;
box-shadow: none;
padding-top: 8px;
`,
avatar: css`
width: 4em;
`,
title: css`
margin-top: ${token.marginSM}px;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`,
@ -39,9 +46,9 @@ function Button() {
const compile = useCompile();
const title = compile(fieldSchema.title);
return layout === WorkbenchLayout.Grid ? (
<div title={title} style={{ width: '100%', overflow: 'hidden' }} className="nb-action-panel-container">
<Avatar style={{ backgroundColor }} size={54} icon={<Icon type={icon} />} />
<div className={cx(styles.title)}>{title}</div>
<div title={fieldSchema.title} className={cx(styles.avatar)}>
<Avatar style={{ backgroundColor }} size={48} icon={<Icon type={icon} />} />
<div className={cx(styles.title)}>{fieldSchema.title}</div>
</div>
) : (
<span>{title}</span>
@ -57,6 +64,7 @@ export const WorkbenchAction = withDynamicSchemaProps((props) => {
<Component
className={cx(className, styles.action, 'nb-action-panel')}
{...others}
type="text"
icon={null}
title={<Button />}
confirmTitle={fieldSchema.title}

View File

@ -7,23 +7,21 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react';
import {
CollectionContext,
createStyles,
DataSourceContext,
DndContext,
Icon,
NocoBaseRecursionField,
useBlockHeight,
useDesignable,
useOpenModeContext,
useSchemaInitializerRender,
withDynamicSchemaProps,
useBlockHeightProps,
useOpenModeContext,
} from '@nocobase/client';
import { Avatar, List, Space, theme } from 'antd';
import React, { createContext, useEffect, useState, useRef, useMemo, useLayoutEffect } from 'react';
import { Avatar, Space } from 'antd';
import { Grid, List } from 'antd-mobile';
import React, { createContext } from 'react';
import { WorkbenchLayout } from './workbenchBlockSettings';
const ConfigureActionsButton = observer(
@ -46,105 +44,27 @@ const ResponsiveSpace = () => {
const isMobileMedia = isMobile();
const { isMobile: underMobileCtx } = useOpenModeContext() || {};
const { itemsPerRow = 4 } = fieldSchema.parent['x-decorator-props'] || {};
const isUnderMobile = isMobileMedia || underMobileCtx;
const containerRef = useRef(null); // 引用容器
const [containerWidth, setContainerWidth] = useState(0); // 容器宽度
// 使用 ResizeObserver 动态获取容器宽度
useEffect(() => {
const handleResize = () => {
if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth); // 更新宽度
}
};
// 初始化 ResizeObserver
const resizeObserver = new ResizeObserver(handleResize);
// 监听容器宽度变化
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
handleResize(); // 初始化时获取一次宽度
}
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, []);
useLayoutEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setContainerWidth(entry.contentRect.width); // 更新宽度
}
});
observer.observe(containerRef.current);
return () => {
observer.unobserve(containerRef.current);
};
}, []);
// 计算每个元素的宽度
const itemWidth = useMemo(() => {
if (isUnderMobile) {
const totalGapWidth = gap * itemsPerRow;
const availableWidth = containerWidth - totalGapWidth;
return availableWidth / itemsPerRow;
}
return 70;
}, [itemsPerRow, gap, containerWidth]);
// 计算 Avatar 的宽度
const avatarSize = useMemo(() => {
return isUnderMobile ? (Math.floor(itemWidth * 0.8) > 70 ? 60 : Math.floor(itemWidth * 0.8)) : 54; // Avatar 大小为 item 宽度的 60%
}, [itemWidth, itemsPerRow, containerWidth]);
if (underMobileCtx || isMobileMedia) {
return (
<Grid columns={itemsPerRow} gap={gap}>
{fieldSchema.mapProperties((s, key) => {
return (
<Grid.Item style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} key={key}>
<NocoBaseRecursionField name={key} schema={s} />
</Grid.Item>
);
})}
</Grid>
);
}
return (
<div ref={containerRef} style={{ width: '100%' }}>
<Space
wrap
style={{ width: '100%', display: 'flex' }}
size={gap}
align="start"
className={css`
.ant-space-item {
width: ${isUnderMobile ? itemWidth + 'px' : '100%'}
display: flex;
.ant-nb-action {
padding: ${isUnderMobile ? '4px 0px' : null};
}
.nb-action-panel-container {
width: ${itemWidth}px !important;
}
.ant-avatar-circle {
width: ${avatarSize}px !important;
height: ${avatarSize}px !important;
line-height: ${avatarSize}px !important;
}
}
`}
>
{fieldSchema.mapProperties((s, key) => (
<div
key={key}
style={
isUnderMobile && {
flexBasis: `${itemWidth}px`,
flexShrink: 0,
flexGrow: 0,
display: 'flex',
}
}
>
<NocoBaseRecursionField name={key} schema={s} />
</div>
))}
</Space>
</div>
<Space wrap size={gap} align="start">
{fieldSchema.mapProperties((s, key) => {
return <NocoBaseRecursionField name={key} schema={s} />;
})}
</Space>
);
};
@ -157,36 +77,17 @@ const InternalIcons = () => {
{layout === WorkbenchLayout.Grid ? (
<ResponsiveSpace />
) : (
<List itemLayout="horizontal">
<List>
{fieldSchema.mapProperties((s, key) => {
const icon = s['x-component-props']?.['icon'];
const backgroundColor = s['x-component-props']?.['iconColor'];
return (
<List.Item
key={key}
className={css`
.ant-list-item-meta-avatar {
margin-inline-end: 0px !important;
}
.ant-list-item-meta-title {
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
margin: 0 0 0 0;
}
.ant-list-item-meta-title button {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
text-align: left;
}
`}
prefix={<Avatar style={{ backgroundColor }} icon={<Icon type={icon} />} />}
onClick={() => {}}
>
<List.Item.Meta
avatar={<Avatar style={{ backgroundColor }} icon={<Icon type={icon} />} />}
title={<NocoBaseRecursionField name={key} schema={s} key={key} />}
></List.Item.Meta>
<NocoBaseRecursionField name={key} schema={s} />
</List.Item>
);
})}
@ -199,44 +100,55 @@ const InternalIcons = () => {
export const WorkbenchBlockContext = createContext({ layout: 'grid' });
const useStyles = createStyles(({ token, css }) => ({
containerClass: css`
&.list {
margin: -${token.paddingLG}px;
border-radius: ${(token as any).borderRadiusBlock}px;
overflow: hidden;
.adm-list {
--padding-left: ${token.paddingLG}px;
--padding-right: ${token.paddingLG}px;
.adm-list-item-content-main {
display: flex;
button {
background-color: transparent;
border: none;
height: auto;
box-shadow: none;
padding: 16px 32px;
margin: -12px -32px;
width: calc(100% + 64px);
text-align: start;
color: ${token.colorText};
}
}
}
button[aria-label*='schema-initializer-WorkbenchBlock.ActionBar-workbench:configureActions'] {
margin-bottom: ${token.paddingLG}px;
margin-left: ${token.paddingLG}px;
}
}
`,
}));
export const WorkbenchBlock: any = withDynamicSchemaProps(
(props) => {
const fieldSchema = useFieldSchema();
const { layout = 'grid' } = fieldSchema['x-component-props'] || {};
const { title } = fieldSchema['x-decorator-props'] || {};
const targetHeight = useBlockHeight();
const { token } = theme.useToken();
const { designable } = useDesignable();
const titleHeight = title ? token.fontSizeLG * token.lineHeightLG + token.padding * 2 - 1 : 0;
const internalHeight = 2 * token.paddingLG + token.controlHeight + token.marginLG + titleHeight;
const warperHeight =
targetHeight - (designable ? internalHeight : 2 * token.paddingLG + token.marginLG + titleHeight);
const targetWarperHeight = warperHeight > 0 ? warperHeight + 'px' : '100%';
const { styles } = useStyles();
return (
<div
className="nb-action-penal-container"
style={{ height: targetHeight ? targetHeight - 2 * token.paddingLG - gap - titleHeight : '100%' }}
>
<div
className={css`
.nb-action-panel-warp {
height: ${targetHeight ? targetWarperHeight : '100%'};
overflow-y: auto;
margin-left: -24px;
margin-right: -24px;
padding-left: 24px;
padding-right: 24px;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
`}
>
<WorkbenchBlockContext.Provider value={{ layout }}>
<DataSourceContext.Provider value={undefined}>
<CollectionContext.Provider value={undefined}>{props.children}</CollectionContext.Provider>
</DataSourceContext.Provider>
</WorkbenchBlockContext.Provider>
</div>
<div className={`nb-action-penal-container ${layout} ${styles.containerClass}`}>
<WorkbenchBlockContext.Provider value={{ layout }}>
<DataSourceContext.Provider value={undefined}>
<CollectionContext.Provider value={undefined}>{props.children}</CollectionContext.Provider>
</DataSourceContext.Provider>
</WorkbenchBlockContext.Provider>
</div>
);
},

View File

@ -104,6 +104,7 @@ export class FileCollectionTemplate extends CollectionTemplate {
type: 'string',
name: 'url',
deletable: false,
length: 1024,
uiSchema: {
type: 'string',
title: `{{t("URL")}}`,

View File

@ -71,6 +71,7 @@ export default defineCollection({
comment: '网络访问地址',
type: 'string',
name: 'url',
length: 1024,
// formula: '{{ storage.baseUrl }}{{ path }}/{{ filename }}'
},
],

View File

@ -0,0 +1,55 @@
/**
* 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 { DataTypes } from 'sequelize';
import { Migration } from '@nocobase/server';
export default class extends Migration {
on = 'afterLoad'; // 'beforeLoad' or 'afterLoad'
appVersion = '<1.5.14';
async up() {
const queryInterface = this.db.sequelize.getQueryInterface();
const CollectionRepo = this.db.getRepository('collections');
const FieldRepo = this.db.getRepository('fields');
await this.db.sequelize.transaction(async (transaction) => {
const collections = await CollectionRepo.find({
filter: {
'options.template': 'file',
},
transaction,
});
collections.push({
name: 'attachments',
});
for (const item of collections) {
const collection = this.db.getCollection(item.name) || this.db.collection(item);
const tableName = collection.getTableNameWithSchema();
await queryInterface.changeColumn(
tableName,
'url',
{
type: DataTypes.STRING(1024),
},
{ transaction },
);
await FieldRepo.update({
filter: {
collectionName: item.name,
name: 'url',
},
values: {
length: 1024,
},
transaction,
});
}
});
}
}

View File

@ -7,13 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import crypto from 'crypto';
import { uid } from '@nocobase/utils';
import path from 'path';
export function getFilename(req, file, cb) {
crypto.pseudoRandomBytes(16, function (err, raw) {
cb(err, err ? undefined : `${raw.toString('hex')}${path.extname(file.originalname)}`);
});
const baseName = path.basename(file.originalname, path.extname(file.originalname));
cb(null, `${baseName}-${uid(6)}${path.extname(file.originalname)}`);
}
export const cloudFilenameGetter = (storage) => (req, file, cb) => {

View File

@ -56,14 +56,27 @@ test.describe('zIndex', () => {
await page.getByLabel('Close lightbox').click();
// 3. 进入第二层子页面,然后点击图片预览, 图片不能被子页面盖住
await page.getByLabel('action-Action-Edit-update-').click();
await page.getByLabel('action-Action-Edit-update-').click({
position: {
x: 5,
y: 5,
},
});
await page.getByRole('link', { name: title }).nth(2).click();
await page.waitForTimeout(300);
await check(2);
await page.getByLabel('Close lightbox').click();
// 4. 进入第三层子页面,然后点击图片预览, 图片不能被子页面盖住
await page.getByLabel('action-Action-Edit-update-').nth(2).click();
await page
.getByLabel('action-Action-Edit-update-')
.nth(2)
.click({
position: {
x: 5,
y: 5,
},
});
await page.getByRole('link', { name: title }).nth(3).click();
await page.waitForTimeout(300);
await check(3);

View File

@ -21,8 +21,10 @@ export const InternalSettings = () => {
const style = useMemo(() => {
return {
marginBottom: token.marginBlock,
borderRadius: token.borderRadiusBlock,
overflow: 'hidden',
};
}, [token.marginBlock]);
}, [token.borderRadiusBlock, token.marginBlock]);
return (
<SortableItem className={cx('nb-mobile-setting')} style={style}>

View File

@ -23,6 +23,7 @@ import {
import React from 'react';
import { isDesktop } from 'react-device-detect';
import { theme } from 'antd';
import { ActionDrawerUsedInMobile, useToAdaptActionDrawerToMobile } from '../adaptor-of-desktop/ActionDrawer';
import { useToAdaptFilterActionToMobile } from '../adaptor-of-desktop/FilterAction';
import { InternalPopoverNesterUsedInMobile } from '../adaptor-of-desktop/InternalPopoverNester';
@ -92,11 +93,13 @@ export const Mobile = () => {
<GlobalThemeProvider
theme={{
token: {
marginBlock: 18,
borderRadiusBlock: 0,
boxShadowTertiary: 'none',
paddingPageHorizontal: 8,
paddingPageVertical: 8,
marginBlock: 12,
borderRadiusBlock: 8,
fontSize: 14,
},
algorithm: theme.compactAlgorithm,
}}
>
<AntdAppProvider className={`mobile-container ${componentCls} ${hashId}`}>

View File

@ -8,43 +8,30 @@
*/
import { genStyleHook } from '@nocobase/client';
import { PageBackgroundColor } from '../constants';
export const useStyles = genStyleHook('nb-mobile', (token) => {
const { componentCls } = token;
return {
[componentCls]: {
// 调整移动端 Grid card 区块顶部按钮和卡片之间的间距
'--nb-spacing': '12px',
WebkitOverflowScrolling: 'touch',
display: 'initial',
'& ::-webkit-scrollbar': {
display: 'none',
},
body: {
backgroundColor: PageBackgroundColor,
},
'.nb-details .ant-formily-item-feedback-layout-loose': {
marginBottom: '5px',
},
'.nb-details .ant-formily-item-layout-vertical .ant-formily-item-label': {
marginBottom: '-8px',
},
'.ant-card .ant-card-body': {
paddingBottom: '10px',
paddingTop: '10px',
},
'.ant-pagination-simple': {
marginTop: '0px !important',
},
'.nb-action-penal-container': {
marginTop: '-10px',
marginBottom: '-10px',
},
'.nb-action-penal-container button[aria-label*="schema-initializer-WorkbenchBlock.ActionBar-workbench:configureActions"]':
{
marginBottom: '10px',
},
'.ant-list-item': {
paddingTop: '8px',
paddingBottom: '8px',
@ -58,9 +45,6 @@ export const useStyles = genStyleHook('nb-mobile', (token) => {
paddingBottom: '0px',
paddingTop: '0px',
},
'.nb-chart-block .noco-card-item': {
marginBottom: '-13px',
},
'.ant-table-thead button[aria-label*="schema-initializer-TableV2-table:configureColumns"] > span:last-child': {
display: 'none !important',
},
@ -90,22 +74,6 @@ export const useStyles = genStyleHook('nb-mobile', (token) => {
'.ant-pagination .ant-pagination-item-active': {
display: 'inline-block',
},
'.ant-card-body .nb-action-bar .ant-btn': {
justifyContent: 'space-between',
display: 'flex',
alignItems: 'center',
gap: '8px',
'& span': {
display: 'contents',
},
},
'.ant-card-body .nb-action-bar .ant-btn-icon': {
marginInlineEnd: '0px !important',
},
'.ant-card-body .nb-table-container': {
marginRight: '-20px',
marginLeft: '-10px',
},
'.nb-action-bar button[aria-label*="schema-initializer-ActionBar-table:configureActions"] > span:last-child': {
display: 'none !important',
},
@ -124,6 +92,11 @@ export const useStyles = genStyleHook('nb-mobile', (token) => {
'[data-menu-id$="-theme"]': {
display: 'none',
},
'& > .ant-menu > .ant-menu-item': {
marginInline: 8,
width: `calc(100% - 16px)`,
},
},
},
};

View File

@ -7,12 +7,16 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useToken } from '@nocobase/client';
import _ from 'lodash';
import React, { FC, useEffect } from 'react';
import { PageBackgroundColor } from '../../../constants';
export const MobilePageContentContainer: FC<{ hideTabBar?: boolean }> = ({ children, hideTabBar }) => {
const [mobileTabBarHeight, setMobileTabBarHeight] = React.useState(0);
const [mobilePageHeader, setMobilePageHeader] = React.useState(0);
const { token } = useToken();
useEffect(() => {
const navigationBar = _.last(document.querySelectorAll<HTMLDivElement>('.mobile-page-header'));
setMobilePageHeader(navigationBar?.offsetHeight);
@ -34,6 +38,9 @@ export const MobilePageContentContainer: FC<{ hideTabBar?: boolean }> = ({ child
boxSizing: 'border-box',
maxWidth: '100%',
overflowX: 'hidden',
backgroundColor: PageBackgroundColor,
paddingInline: token.paddingPageHorizontal,
paddingBlock: token.paddingPageVertical,
}}
>
{children}