From 88dd0c84e7765b00d316ac6ad9af2fa204dc18d5 Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 18 Apr 2025 11:15:06 +0800 Subject: [PATCH 01/13] fix: ensure custom request data must be JSON (#6701) --- .../src/server/actions/send.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts index bf1ba18fa6..f22ce9d294 100644 --- a/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts +++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts @@ -15,6 +15,17 @@ import Application from '@nocobase/server'; import axios from 'axios'; import CustomRequestPlugin from '../plugin'; +function toJSON(value) { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch (error) { + return value; + } + } + return value; +} + const getHeaders = (headers: Record) => { return Object.keys(headers).reduce((hds, key) => { if (key.toLocaleLowerCase().startsWith('x-')) { @@ -168,7 +179,7 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next) ...omitNullAndUndefined(getParsedValue(arrayToObject(headers), variables)), }, params: getParsedValue(arrayToObject(params), variables), - data: getParsedValue(data, variables), + data: getParsedValue(toJSON(data), variables), }; const requestUrl = axios.getUri(axiosRequestConfig); From f5818066ca0834d83ed2c060d627dec93f2974af Mon Sep 17 00:00:00 2001 From: Sheldon Guo Date: Fri, 18 Apr 2025 12:23:25 +0800 Subject: [PATCH 02/13] fix(in-app-message): disconnect SSE on mobile when message component unmounts or tab is switched to avoid excessive persistent connections. --- .../mobile/MobileTabBarMessageItem.tsx | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx b/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx index 8531d4a1aa..4233489589 100644 --- a/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx +++ b/packages/plugins/@nocobase/plugin-notification-in-app-message/src/client/components/mobile/MobileTabBarMessageItem.tsx @@ -7,11 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { useEffect } from 'react'; import { observer } from '@formily/reactive-react'; -import { useNavigate, useLocation } from 'react-router-dom'; import { MobileTabBarItem } from '@nocobase/plugin-mobile/client'; -import { unreadMsgsCountObs, startMsgSSEStreamWithRetry, updateUnreadMsgsCount } from '../../observables'; +import React, { useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { startMsgSSEStreamWithRetry, unreadMsgsCountObs, updateUnreadMsgsCount } from '../../observables'; const InnerMobileTabBarMessageItem = (props) => { const navigate = useNavigate(); @@ -19,9 +19,32 @@ const InnerMobileTabBarMessageItem = (props) => { const onClick = () => { navigate('/page/in-app-message'); }; + useEffect(() => { - startMsgSSEStreamWithRetry(); + const disposes: Array<() => void> = []; + disposes.push(startMsgSSEStreamWithRetry()); + const disposeAll = () => { + while (disposes.length > 0) { + const dispose = disposes.pop(); + dispose && dispose(); + } + }; + + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') { + disposes.push(startMsgSSEStreamWithRetry()); + } else { + disposeAll(); + } + }; + + document.addEventListener('visibilitychange', onVisibilityChange); + return () => { + disposeAll(); + document.removeEventListener('visibilitychange', onVisibilityChange); + }; }, []); + const selected = props.url && location.pathname.startsWith(props.url); return ( From 8ac84e6e2c0a3d02c2db8793c3fe6390f81d9029 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 18 Apr 2025 13:59:39 +0800 Subject: [PATCH 03/13] fix: export button shown without export permission (#6689) --- packages/core/client/src/acl/ACLProvider.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index 09eaf964ad..2fb3d4b3c1 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -309,15 +309,15 @@ export const ACLActionProvider = (props) => { const schema = useFieldSchema(); const currentUid = schema['x-uid']; let actionPath = schema['x-acl-action']; - const editablePath = ['create', 'update', 'destroy', 'importXlsx']; + // 只兼容这些数据表资源按钮 + const resourceActionPath = ['create', 'update', 'destroy', 'importXlsx', 'export']; - if (!actionPath && resource && schema['x-action'] && editablePath.includes(schema['x-action'])) { + if (!actionPath && resource && schema['x-action'] && resourceActionPath.includes(schema['x-action'])) { actionPath = `${resource}:${schema['x-action']}`; } if (actionPath && !actionPath?.includes(':')) { actionPath = `${resource}:${actionPath}`; } - const params = useMemo( () => actionPath && parseAction(actionPath, { schema, recordPkValue }), [parseAction, actionPath, schema, recordPkValue], @@ -335,7 +335,7 @@ export const ACLActionProvider = (props) => { return {props.children}; } //视图表无编辑权限时不显示 - if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) { + if (resourceActionPath.includes(actionPath) || resourceActionPath.includes(actionPath?.split(':')[1])) { if ((collection && collection.template !== 'view') || collection?.writableView) { return {props.children}; } From deb85d30bddf464fb80a71bc8746628b75f75c83 Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 18 Apr 2025 14:42:19 +0800 Subject: [PATCH 04/13] chore: build internal image --- .github/workflows/build-internal-image.yml | 119 +++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 .github/workflows/build-internal-image.yml diff --git a/.github/workflows/build-internal-image.yml b/.github/workflows/build-internal-image.yml new file mode 100644 index 0000000000..0601be438d --- /dev/null +++ b/.github/workflows/build-internal-image.yml @@ -0,0 +1,119 @@ +name: Build Image (Internal) + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + inputs: + ref_name: + description: 'Branch or tag name to release' + +jobs: + get-plugins: + uses: nocobase/nocobase/.github/workflows/get-plugins.yml@main + secrets: inherit + push-docker: + runs-on: ubuntu-latest + needs: get-plugins + services: + verdaccio: + image: verdaccio/verdaccio:5 + ports: + - 4873:4873 + steps: + - name: Set Node.js 20 + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Get info + id: get-info + shell: bash + run: | + if [[ "${{ inputs.ref_name || github.ref_name }}" =~ "beta" ]]; then + echo "defaultTag=$(echo 'beta')" >> $GITHUB_OUTPUT + echo "proRepos=$(echo '${{ needs.get-plugins.outputs.beta-plugins }}')" >> $GITHUB_OUTPUT + elif [[ "${{ inputs.ref_name || github.ref_name }}" =~ "alpha" ]]; then + echo "defaultTag=$(echo 'alpha')" >> $GITHUB_OUTPUT + echo "proRepos=$(echo '${{ needs.get-plugins.outputs.alpha-plugins }}')" >> $GITHUB_OUTPUT + else + # rc + echo "defaultTag=$(echo 'latest')" >> $GITHUB_OUTPUT + echo "proRepos=$(echo '${{ needs.get-plugins.outputs.rc-plugins }}')" >> $GITHUB_OUTPUT + fi + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-info.outputs.proRepos), ',') }},${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ',') }} + skip-token-revoke: true + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref_name || github.ref_name }} + - name: yarn install + run: | + yarn install + - name: Checkout pro-plugins + uses: actions/checkout@v3 + with: + repository: nocobase/pro-plugins + path: packages/pro-plugins + ref: ${{ inputs.ref_name || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + - name: Clone pro repos + shell: bash + run: | + for repo in ${{ join(fromJSON(steps.get-info.outputs.proRepos), ' ') }} ${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ' ') }} + do + git clone -b ${{ inputs.ref_name || github.ref_name }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo + done + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + driver-opts: network=host + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + nocobase/nocobase + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - name: Login to Aliyun Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ secrets.ALI_DOCKER_REGISTRY }} + username: ${{ secrets.ALI_DOCKER_USERNAME }} + password: ${{ secrets.ALI_DOCKER_PASSWORD }} + - name: Set variables + run: | + target_directory="./packages/pro-plugins/@nocobase" + subdirectories=$(find "$target_directory" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | tr '\n' ' ') + trimmed_variable=$(echo "$subdirectories" | xargs) + packageNames="@nocobase/${trimmed_variable// / @nocobase/}" + pluginNames="${trimmed_variable//plugin-/}" + BEFORE_PACK_NOCOBASE="yarn add @nocobase/plugin-notifications @nocobase/plugin-disable-pm-add $packageNames -W --production" + APPEND_PRESET_LOCAL_PLUGINS="notifications,disable-pm-add,${pluginNames// /,}" + echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT + echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + build-args: | + VERDACCIO_URL=http://localhost:4873/ + COMMIT_HASH=${GITHUB_SHA} + PLUGINS_DIRS=pro-plugins + BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }} + APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }} + push: true + tags: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:${{ steps.get-info.outputs.defaultTag }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }} From d550a628d8514b47b0894cf10e05b00083a3dd36 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 18 Apr 2025 17:29:07 +0800 Subject: [PATCH 05/13] fix(linkage-rule): variable conversion in sub-table/sub-form linkage rule conditions (#6702) * fix: variable conversion in sub-table/sub-form linkage rule conditions * fix: bug * fix: style in sub-table * fix: bug --- .../antd/linkageFilter/useValues.ts | 2 +- .../src/schema-settings/LinkageRules/index.tsx | 17 ++++++++++------- .../src/schema-settings/SchemaSettings.tsx | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/core/client/src/schema-component/antd/linkageFilter/useValues.ts b/packages/core/client/src/schema-component/antd/linkageFilter/useValues.ts index 6f2c5463ed..54b87eb91d 100644 --- a/packages/core/client/src/schema-component/antd/linkageFilter/useValues.ts +++ b/packages/core/client/src/schema-component/antd/linkageFilter/useValues.ts @@ -93,7 +93,7 @@ export const useValues = (): UseValuesReturn => { }, 100); }; - useEffect(value2data, [field.value, scopes]); + useEffect(value2data, [field.value.leftVar, scopes]); const setLeftValue = useCallback( (leftVar, paths) => { diff --git a/packages/core/client/src/schema-settings/LinkageRules/index.tsx b/packages/core/client/src/schema-settings/LinkageRules/index.tsx index 5fbc3b7bf4..5988ff898e 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/index.tsx +++ b/packages/core/client/src/schema-settings/LinkageRules/index.tsx @@ -24,6 +24,7 @@ import { useCurrentFormContext } from '../VariableInput/hooks/useFormVariable'; import { LinkageRuleActionGroup } from './LinkageRuleActionGroup'; import { EnableLinkage } from './components/EnableLinkage'; import { ArrayCollapse } from './components/LinkageHeader'; +import { useFlag } from '../../flag-provider'; export interface Props { dynamicComponent: any; @@ -65,11 +66,12 @@ function transformConditionData(condition: Condition, variableKey: '$nForm' | '$ rightVar: value, }; } -function getActiveContextName(contextList: { name: string; ctx: any }[]): string | null { - const priority = ['$nForm', '$nRecord']; - for (const name of priority) { - const item = contextList.find((ctx) => ctx.name === name && ctx.ctx); - if (item) return name; +function getActiveContextName(underNester, shouldDisplayCurrentForm): string | null { + if (underNester) { + return '$iteration'; + } + if (shouldDisplayCurrentForm) { + return '$nForm'; } return '$nRecord'; } @@ -97,15 +99,16 @@ export const FormLinkageRules = withDynamicSchemaProps( const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); const parentRecordData = useCollectionParentRecordData(); const { shouldDisplayCurrentForm } = useCurrentFormContext(); - const variableKey = getActiveContextName(localVariables); const components = useMemo(() => ({ ArrayCollapse }), []); + const { isInSubTable, isInSubForm } = useFlag(); + const variableKey = getActiveContextName(isInSubTable || isInSubForm, shouldDisplayCurrentForm); const schema = useMemo( () => ({ type: 'object', properties: { rules: { type: 'array', - default: transformDefaultValue(defaultValues, shouldDisplayCurrentForm ? variableKey : '$nRecord'), + default: transformDefaultValue(defaultValues, variableKey), 'x-component': 'ArrayCollapse', 'x-decorator': 'FormItem', 'x-component-props': { diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index bf44536334..c277f087b7 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -84,7 +84,7 @@ import { AssociationOrCollectionProvider, useDataBlockProps } from '../data-sour import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider'; import { useFilterBlock } from '../filter-provider/FilterProvider'; -import { FlagProvider } from '../flag-provider'; +import { FlagProvider, useFlag } from '../flag-provider'; import { useGlobalTheme } from '../global-theme'; import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem'; import { @@ -1129,6 +1129,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) { return gridSchema?.[dataKey] || fieldSchema?.[dataKey] || []; }, [gridSchema, fieldSchema, dataKey]); const title = titleMap[category] || t('Linkage rules'); + const flagVales = useFlag(); const schema = useMemo( () => ({ type: 'object', @@ -1180,7 +1181,16 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) { ); return ( - + { + return {props.children}; + }} + /> ); }; From aa6a46924a7a02ee3a61087e8036d9578001eb42 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 18 Apr 2025 17:29:52 +0800 Subject: [PATCH 06/13] fix(date-picker): picker switching issue in date field of filter button (#6695) * fix: picker switching issue in date field of filter button * fix: bug * fix: bug --- .../schema-component/antd/date-picker/DatePicker.tsx | 12 ++++-------- .../LinkageRules/ValueDynamicComponent.tsx | 3 +++ packages/core/utils/src/date.ts | 5 +++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx index cc1099775a..16df4de501 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx @@ -268,8 +268,9 @@ DatePicker.FilterWithPicker = function FilterWithPicker(props: any) { const value = Array.isArray(props.value) ? props.value[0] : props.value; const compile = useCompile(); const fieldSchema = useFieldSchema(); - const targetPicker = value ? inferPickerType(value, picker) : picker; - const targetDateFormat = getPickerFormat(targetPicker) || format; + const initPicker = value ? inferPickerType(value, picker) : picker; + const [targetPicker, setTargetPicker] = useState(initPicker); + const targetDateFormat = getPickerFormat(initPicker) || format; const newProps = { utc, inputReadOnly: isMobileMedia, @@ -287,12 +288,6 @@ DatePicker.FilterWithPicker = function FilterWithPicker(props: any) { }; const field: any = useField(); const [stateProps, setStateProps] = useState(newProps); - useEffect(() => { - newProps.picker = targetPicker; - const dateTimeFormat = getDateTimeFormat(targetPicker, targetDateFormat, showTime, timeFormat); - newProps.format = dateTimeFormat; - setStateProps(newProps); - }, [targetPicker]); return (