mirror of
https://gh-proxy.org/https://github.com/klinsmaya/portal-template-default.git
synced 2026-11-08 00:57:57 +08:00
feat: add upgradeable Portal SDK (#13)
* feat: add upgradeable Portal SDK * ci: auto-detect Portal release targets * refactor: consume Portal SDK source in workspace * fix: validate Portal SDK compatibility metadata
This commit is contained in:
+275
-18
@@ -3,8 +3,22 @@ name: Publish npm package
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_target:
|
||||
description: Packages to release (auto detects changes from release tags)
|
||||
required: true
|
||||
default: auto
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- template
|
||||
- sdk
|
||||
- both
|
||||
version:
|
||||
description: Exact version (leave blank to increment patch)
|
||||
description: Exact template version (template/both; blank increments patch)
|
||||
required: false
|
||||
type: string
|
||||
sdk_version:
|
||||
description: Exact Portal SDK version (sdk/both; blank uses package.json)
|
||||
required: false
|
||||
type: string
|
||||
tag:
|
||||
@@ -57,50 +71,293 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Resolve release version
|
||||
- name: Resolve release
|
||||
id: release
|
||||
env:
|
||||
REF_TYPE: ${{ github.ref_type }}
|
||||
REQUESTED_RELEASE_TARGET: ${{ inputs.release_target }}
|
||||
REQUESTED_VERSION: ${{ inputs.version }}
|
||||
REQUESTED_SDK_VERSION: ${{ inputs.sdk_version }}
|
||||
run: |
|
||||
if [ "$REF_TYPE" != "branch" ]; then
|
||||
echo "Run this workflow from a branch, not a tag."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$REQUESTED_VERSION" ]; then
|
||||
npm version "$REQUESTED_VERSION" --no-git-tag-version
|
||||
else
|
||||
npm version patch --no-git-tag-version
|
||||
RELEASE_TARGET=$REQUESTED_RELEASE_TARGET
|
||||
if [ "$RELEASE_TARGET" = auto ]; then
|
||||
LATEST_TEMPLATE_TAG=$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n 1)
|
||||
LATEST_SDK_TAG=$(git tag --list 'portal-sdk-v*' --sort=-version:refname | head -n 1)
|
||||
|
||||
TEMPLATE_CHANGED=true
|
||||
if [ -n "$LATEST_TEMPLATE_TAG" ] && git diff --quiet "${LATEST_TEMPLATE_TAG}..HEAD" -- . ':(exclude)sdk'; then
|
||||
TEMPLATE_CHANGED=false
|
||||
fi
|
||||
|
||||
SDK_CHANGED=true
|
||||
if [ -n "$LATEST_SDK_TAG" ] && git diff --quiet "${LATEST_SDK_TAG}..HEAD" -- sdk; then
|
||||
SDK_CHANGED=false
|
||||
fi
|
||||
|
||||
if [ "$TEMPLATE_CHANGED" = true ] && [ "$SDK_CHANGED" = true ]; then
|
||||
RELEASE_TARGET=both
|
||||
elif [ "$TEMPLATE_CHANGED" = true ]; then
|
||||
RELEASE_TARGET=template
|
||||
elif [ "$SDK_CHANGED" = true ]; then
|
||||
RELEASE_TARGET=sdk
|
||||
else
|
||||
echo "No SDK or template changes were found after the latest release tags."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected release target: ${RELEASE_TARGET}"
|
||||
echo "Latest template tag: ${LATEST_TEMPLATE_TAG:-none}"
|
||||
echo "Latest SDK tag: ${LATEST_SDK_TAG:-none}"
|
||||
fi
|
||||
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
if git rev-parse --verify --quiet "refs/tags/v${VERSION}"; then
|
||||
echo "Git tag v${VERSION} already exists."
|
||||
RELEASE_TEMPLATE=false
|
||||
RELEASE_SDK=false
|
||||
case "$RELEASE_TARGET" in
|
||||
template)
|
||||
RELEASE_TEMPLATE=true
|
||||
;;
|
||||
sdk)
|
||||
RELEASE_SDK=true
|
||||
;;
|
||||
both)
|
||||
RELEASE_TEMPLATE=true
|
||||
RELEASE_SDK=true
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported release target: ${RELEASE_TARGET}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
VERSION=$CURRENT_VERSION
|
||||
PUBLISH_TEMPLATE=false
|
||||
VERIFY_TEMPLATE=false
|
||||
|
||||
if [ "$RELEASE_TEMPLATE" = true ]; then
|
||||
CURRENT_TEMPLATE_TAG_EXISTS=false
|
||||
if git rev-parse --verify --quiet "refs/tags/v${CURRENT_VERSION}"; then
|
||||
CURRENT_TEMPLATE_TAG_EXISTS=true
|
||||
fi
|
||||
|
||||
if [ -n "$REQUESTED_VERSION" ] && [ "$REQUESTED_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
npm version "$REQUESTED_VERSION" --no-git-tag-version
|
||||
elif [ -z "$REQUESTED_VERSION" ] && [ "$CURRENT_TEMPLATE_TAG_EXISTS" = true ]; then
|
||||
npm version patch --no-git-tag-version
|
||||
fi
|
||||
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
node -e "const fs=require('fs');const pkg=require('./package.json');pkg.nocobase.defaultTemplateVersion=pkg.version;fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n')"
|
||||
|
||||
if git rev-parse --verify --quiet "refs/tags/v${VERSION}"; then
|
||||
echo "Git tag v${VERSION} already exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if npm view "@nocobase/portal-template-default@${VERSION}" version >/dev/null 2>&1; then
|
||||
VERIFY_TEMPLATE=true
|
||||
else
|
||||
PUBLISH_TEMPLATE=true
|
||||
fi
|
||||
elif [ -n "$REQUESTED_VERSION" ]; then
|
||||
echo "version can only be used for template or both releases."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_SDK_VERSION=$(node -p "require('./sdk/package.json').version")
|
||||
if [ "$RELEASE_SDK" = true ]; then
|
||||
CURRENT_SDK_TAG_EXISTS=false
|
||||
if git rev-parse --verify --quiet "refs/tags/portal-sdk-v${CURRENT_SDK_VERSION}"; then
|
||||
CURRENT_SDK_TAG_EXISTS=true
|
||||
fi
|
||||
|
||||
if [ -n "$REQUESTED_SDK_VERSION" ] && [ "$REQUESTED_SDK_VERSION" != "$CURRENT_SDK_VERSION" ]; then
|
||||
npm version "$REQUESTED_SDK_VERSION" --no-git-tag-version --prefix sdk
|
||||
elif [ -z "$REQUESTED_SDK_VERSION" ] && [ "$CURRENT_SDK_TAG_EXISTS" = true ]; then
|
||||
npm version patch --no-git-tag-version --prefix sdk
|
||||
fi
|
||||
elif [ -n "$REQUESTED_SDK_VERSION" ]; then
|
||||
echo "sdk_version can only be used for sdk or both releases."
|
||||
exit 1
|
||||
fi
|
||||
SDK_VERSION=$(node -p "require('./sdk/package.json').version")
|
||||
|
||||
SDK_EXISTS=false
|
||||
if npm view "@nocobase/portal-sdk@${SDK_VERSION}" version >/dev/null 2>&1; then
|
||||
SDK_EXISTS=true
|
||||
fi
|
||||
|
||||
SDK_TAG="portal-sdk-v${SDK_VERSION}"
|
||||
SDK_TAG_EXISTS=false
|
||||
SDK_MATCHES_TAG=false
|
||||
if git rev-parse --verify --quiet "refs/tags/${SDK_TAG}"; then
|
||||
SDK_TAG_EXISTS=true
|
||||
if git diff --quiet "${SDK_TAG}..HEAD" -- sdk; then
|
||||
SDK_MATCHES_TAG=true
|
||||
fi
|
||||
fi
|
||||
|
||||
PUBLISH_SDK=false
|
||||
VERIFY_SDK=false
|
||||
TAG_SDK=false
|
||||
|
||||
if [ "$RELEASE_SDK" = true ]; then
|
||||
if [ "$SDK_EXISTS" = false ] && [ "$SDK_TAG_EXISTS" = true ]; then
|
||||
echo "Git tag ${SDK_TAG} exists, but @nocobase/portal-sdk@${SDK_VERSION} is not published."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SDK_EXISTS" = false ]; then
|
||||
PUBLISH_SDK=true
|
||||
elif [ "$SDK_MATCHES_TAG" = false ]; then
|
||||
VERIFY_SDK=true
|
||||
fi
|
||||
|
||||
if [ "$SDK_TAG_EXISTS" = false ]; then
|
||||
TAG_SDK=true
|
||||
fi
|
||||
|
||||
if [ "$PUBLISH_SDK" = false ] && [ "$VERIFY_SDK" = false ] && [ "$TAG_SDK" = false ]; then
|
||||
echo "@nocobase/portal-sdk@${SDK_VERSION} is already fully released."
|
||||
echo "Choose template, or provide a new sdk_version."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [ "$SDK_EXISTS" = false ]; then
|
||||
echo "Template releases require a published @nocobase/portal-sdk@${SDK_VERSION}."
|
||||
echo "Publish the SDK first or choose release_target=both."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$SDK_MATCHES_TAG" = false ]; then
|
||||
VERIFY_SDK=true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "release_target=${RELEASE_TARGET}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_template=${RELEASE_TEMPLATE}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_sdk=${RELEASE_SDK}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "sdk_version=${SDK_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "publish_sdk=${PUBLISH_SDK}" >> "$GITHUB_OUTPUT"
|
||||
echo "verify_sdk=${VERIFY_SDK}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag_sdk=${TAG_SDK}" >> "$GITHUB_OUTPUT"
|
||||
echo "publish_template=${PUBLISH_TEMPLATE}" >> "$GITHUB_OUTPUT"
|
||||
echo "verify_template=${VERIFY_TEMPLATE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Materialize default extensions
|
||||
if: steps.release.outputs.release_template == 'true'
|
||||
run: node scripts/registry.mjs materialize
|
||||
|
||||
- name: Inspect package
|
||||
run: npm pack --dry-run
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish package
|
||||
run: npm publish --access public --tag "${{ inputs.tag }}" --loglevel verbose
|
||||
- name: Test SDK and integrations
|
||||
run: |
|
||||
pnpm --dir sdk test
|
||||
node tests/auth-session-regression.mjs
|
||||
node registry/nocobase-acl/tests/acl-regression.mjs
|
||||
node registry/nocobase-i18n/tests/i18n-regression.mjs
|
||||
node --experimental-strip-types --test tests/file-upload-regression.test.ts
|
||||
node registry/nocobase-mail/tests/mail-regression.mjs
|
||||
node registry/nocobase-route-surfaces/tests/route-surfaces-regression.mjs
|
||||
|
||||
- name: Pack SDK
|
||||
if: steps.release.outputs.release_sdk == 'true' || steps.release.outputs.verify_sdk == 'true'
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/portal-sdk-local" "$RUNNER_TEMP/portal-sdk-local-unpacked"
|
||||
pnpm --dir sdk pack --pack-destination "$RUNNER_TEMP/portal-sdk-local"
|
||||
tar -xzf "$RUNNER_TEMP/portal-sdk-local/"*.tgz -C "$RUNNER_TEMP/portal-sdk-local-unpacked"
|
||||
node sdk/tests/verify-packed-package.mjs "$RUNNER_TEMP/portal-sdk-local-unpacked/package"
|
||||
|
||||
- name: Pack template
|
||||
if: steps.release.outputs.release_template == 'true'
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/portal-template-local"
|
||||
pnpm pack --pack-destination "$RUNNER_TEMP/portal-template-local"
|
||||
|
||||
- name: Verify existing SDK package
|
||||
if: steps.release.outputs.verify_sdk == 'true'
|
||||
env:
|
||||
SDK_VERSION: ${{ steps.release.outputs.sdk_version }}
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/portal-sdk-remote" "$RUNNER_TEMP/portal-sdk-remote-unpacked"
|
||||
npm pack "@nocobase/portal-sdk@${SDK_VERSION}" --pack-destination "$RUNNER_TEMP/portal-sdk-remote"
|
||||
tar -xzf "$RUNNER_TEMP/portal-sdk-remote/"*.tgz -C "$RUNNER_TEMP/portal-sdk-remote-unpacked"
|
||||
if ! diff -ru "$RUNNER_TEMP/portal-sdk-remote-unpacked/package" "$RUNNER_TEMP/portal-sdk-local-unpacked/package"; then
|
||||
echo "Published @nocobase/portal-sdk@${SDK_VERSION} differs from the current SDK package."
|
||||
echo "Rerun with a new sdk_version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify existing template package
|
||||
if: steps.release.outputs.verify_template == 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/portal-template-remote" "$RUNNER_TEMP/portal-template-local-unpacked" "$RUNNER_TEMP/portal-template-remote-unpacked"
|
||||
npm pack "@nocobase/portal-template-default@${VERSION}" --pack-destination "$RUNNER_TEMP/portal-template-remote"
|
||||
tar -xzf "$RUNNER_TEMP/portal-template-local/"*.tgz -C "$RUNNER_TEMP/portal-template-local-unpacked"
|
||||
tar -xzf "$RUNNER_TEMP/portal-template-remote/"*.tgz -C "$RUNNER_TEMP/portal-template-remote-unpacked"
|
||||
if ! diff -ru "$RUNNER_TEMP/portal-template-remote-unpacked/package" "$RUNNER_TEMP/portal-template-local-unpacked/package"; then
|
||||
echo "Published @nocobase/portal-template-default@${VERSION} differs from the current template package."
|
||||
echo "Rerun with a new template version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish SDK
|
||||
if: steps.release.outputs.publish_sdk == 'true'
|
||||
run: pnpm --dir sdk publish --no-git-checks --access public --tag "${{ inputs.tag }}"
|
||||
|
||||
- name: Publish template
|
||||
if: steps.release.outputs.publish_template == 'true'
|
||||
run: pnpm publish --no-git-checks --access public --tag "${{ inputs.tag }}"
|
||||
|
||||
- name: Commit and tag release
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
RELEASE_TARGET: ${{ steps.release.outputs.release_target }}
|
||||
RELEASE_TEMPLATE: ${{ steps.release.outputs.release_template }}
|
||||
RELEASE_SDK: ${{ steps.release.outputs.release_sdk }}
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
SDK_VERSION: ${{ steps.release.outputs.sdk_version }}
|
||||
TAG_SDK: ${{ steps.release.outputs.tag_sdk }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add package.json
|
||||
git commit -m "chore(release): v${VERSION}"
|
||||
git tag -a "v${VERSION}" -m "v${VERSION}"
|
||||
git push --atomic origin "HEAD:refs/heads/${TARGET_BRANCH}" "refs/tags/v${VERSION}"
|
||||
|
||||
if [ "$RELEASE_TEMPLATE" = true ]; then
|
||||
git add package.json
|
||||
fi
|
||||
if [ "$RELEASE_SDK" = true ]; then
|
||||
git add sdk/package.json
|
||||
fi
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
case "$RELEASE_TARGET" in
|
||||
sdk)
|
||||
git commit -m "chore(release): portal SDK v${SDK_VERSION}"
|
||||
;;
|
||||
template)
|
||||
git commit -m "chore(release): portal template v${VERSION}"
|
||||
;;
|
||||
both)
|
||||
git commit -m "chore(release): portal template v${VERSION} and SDK v${SDK_VERSION}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
PUSH_REFS=("HEAD:refs/heads/${TARGET_BRANCH}")
|
||||
if [ "$RELEASE_TEMPLATE" = true ]; then
|
||||
git tag -a "v${VERSION}" -m "v${VERSION}"
|
||||
PUSH_REFS+=("refs/tags/v${VERSION}")
|
||||
fi
|
||||
if [ "$TAG_SDK" = true ]; then
|
||||
git tag -a "portal-sdk-v${SDK_VERSION}" -m "@nocobase/portal-sdk@${SDK_VERSION}"
|
||||
PUSH_REFS+=("refs/tags/portal-sdk-v${SDK_VERSION}")
|
||||
fi
|
||||
git push --atomic origin "${PUSH_REFS[@]}"
|
||||
|
||||
@@ -12,4 +12,7 @@ registry.config.json
|
||||
registry.json
|
||||
public/r/
|
||||
scripts/registry.mjs
|
||||
sdk/
|
||||
tests/
|
||||
.github/
|
||||
!.gitignore
|
||||
|
||||
@@ -28,6 +28,6 @@ Route access currently centralizes role constraints only. Keep NocoBase resource
|
||||
|
||||
## Develop Portal Registry items
|
||||
|
||||
Canonical NocoBase Registry source lives under `registry/`. In this source repository, normal development and builds load it directly; do not copy it into `src/extensions` for preview. Imports from a Registry item to the host application must use the `@/` alias. Relative imports must stay within that Registry item's own root so the item remains portable after installation. Registry items target this Portal Template's React, shadcn Base UI, and pnpm toolchain. They must never import Ant Design or NocoBase's Ant Design-based client components.
|
||||
Canonical NocoBase Registry source lives under `registry/`. In this source repository, normal development and builds load it directly; do not copy it into `src/extensions` for preview. Registry items must import stable Portal runtime, client, authentication, ACL, routing, and extension contracts from documented `@nocobase/portal-sdk` exports. Imports to user-owned host UI and composition must use the `@/` alias. Relative imports must stay within that Registry item's own root so the item remains portable after installation. Registry items target this Portal Template's React, shadcn Base UI, and pnpm toolchain. They must never import Ant Design or NocoBase's Ant Design-based client components.
|
||||
|
||||
Keep Registry items portable and focused on reusable API adapters, hooks, components, and small demos. Update `registry.config.json` whenever an item's files, dependencies, or installation target changes. Validate Registry changes with the normal application build and the relevant regression scripts.
|
||||
|
||||
@@ -55,9 +55,17 @@ pnpm build:html
|
||||
Application pages and resources live under `src`. The included Users page is a
|
||||
working example based on the standard NocoBase `users` collection.
|
||||
|
||||
Application translations live in `src/locales`. Shared UI components live in
|
||||
Application translations live in `src/locales`. Default locale, available
|
||||
locale labels, i18next options, and post-switch behavior remain customizable in
|
||||
`src/providers/i18n/runtime.ts`. Shared UI components live in
|
||||
`src/components/ui` and follow the shadcn Base UI conventions.
|
||||
|
||||
Stable NocoBase runtime behavior is provided by `@nocobase/portal-sdk`.
|
||||
The template records its inherited base version in
|
||||
`nocobase.defaultTemplateVersion`; SDK installation, development, and builds
|
||||
stop with an actionable error when that version is outside the SDK's supported
|
||||
range. Application composition and visual source remain owned by this project.
|
||||
|
||||
## Extensions
|
||||
|
||||
Installed extensions live in `src/extensions` and are ordinary
|
||||
@@ -73,6 +81,11 @@ be installed from the `@nocobase` Registry configured in `components.json`.
|
||||
After installation, the generated source can be customized and committed with
|
||||
the application.
|
||||
|
||||
Registry implementations import runtime APIs from documented
|
||||
`@nocobase/portal-sdk` subpaths and continue to use `@/components` for
|
||||
application-owned visual composition. Each Registry item declares the minimum
|
||||
compatible SDK version in `registry.config.json`.
|
||||
|
||||
In this Registry source repository, canonical extension source lives in
|
||||
`registry` and is loaded directly by `pnpm dev` and `pnpm build`. The published
|
||||
npm template contains the default extensions under `src/extensions` instead.
|
||||
@@ -87,14 +100,23 @@ Customize `src/pages/login/index.tsx` to replace the whole login page, or use
|
||||
`DefaultSignInPage` to keep dynamic authentication while overriding individual
|
||||
authenticator UI.
|
||||
|
||||
## Tests
|
||||
## Source repository checks
|
||||
|
||||
```bash
|
||||
pnpm test:auth
|
||||
pnpm test:acl
|
||||
pnpm test:i18n
|
||||
pnpm --dir sdk test
|
||||
node tests/auth-session-regression.mjs
|
||||
node registry/nocobase-acl/tests/acl-regression.mjs
|
||||
node registry/nocobase-i18n/tests/i18n-regression.mjs
|
||||
node --experimental-strip-types --test tests/file-upload-regression.test.ts
|
||||
node registry/nocobase-mail/tests/mail-regression.mjs
|
||||
node registry/nocobase-route-surfaces/tests/route-surfaces-regression.mjs
|
||||
```
|
||||
|
||||
Registry source checks are maintainer commands for this source repository; the
|
||||
published template contains materialized extensions instead of `registry/`.
|
||||
SDK breaking-change releases and project upgrade steps are documented in the
|
||||
[Portal SDK migration guide](https://github.com/nocobase/portal-template-default/blob/main/sdk/MIGRATION.md).
|
||||
|
||||
## Learn more
|
||||
|
||||
- [NocoBase documentation](https://docs.nocobase.com/)
|
||||
|
||||
+9
-9
@@ -1,9 +1,15 @@
|
||||
{
|
||||
"name": "@nocobase/portal-template-default",
|
||||
"displayName": "Default Template",
|
||||
"version": "1.0.22",
|
||||
"version": "2.0.0",
|
||||
"packageManager": "[email protected]",
|
||||
"type": "module",
|
||||
"nocobase": {
|
||||
"defaultTemplateVersion": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/portal-sdk": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/react": "2.0.0",
|
||||
"@base-ui/react": "^1.6.0",
|
||||
@@ -52,7 +58,6 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"eventsource-parser": "3.1.0",
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.2",
|
||||
"ignore": "^7.0.6",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.487.0",
|
||||
@@ -88,14 +93,9 @@
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "tsc && refine build",
|
||||
"build:html": "node ./scripts/build-html.mjs",
|
||||
"test:auth": "node tests/auth-session-regression.mjs",
|
||||
"test:acl": "node registry/nocobase-acl/tests/acl-regression.mjs",
|
||||
"test:i18n": "node registry/nocobase-i18n/tests/i18n-regression.mjs",
|
||||
"test:file-upload": "node --experimental-strip-types --test tests/file-upload-regression.test.ts",
|
||||
"test:mail": "node registry/nocobase-mail/tests/mail-regression.mjs",
|
||||
"test:route-surfaces": "node registry/nocobase-route-surfaces/tests/route-surfaces-regression.mjs",
|
||||
"start": "refine start",
|
||||
"refine": "refine"
|
||||
"refine": "refine",
|
||||
"sdk:check": "pnpm exec portal-sdk check"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
Generated
+45
-4
@@ -7,6 +7,10 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@nocobase/portal-sdk':
|
||||
specifier: workspace:^
|
||||
version: link:sdk
|
||||
devDependencies:
|
||||
'@ai-sdk/react':
|
||||
specifier: 2.0.0
|
||||
@@ -149,9 +153,6 @@ importers:
|
||||
globals:
|
||||
specifier: ^16.0.0
|
||||
version: 16.5.0
|
||||
i18next:
|
||||
specifier: ^25.3.2
|
||||
version: 25.10.10([email protected])
|
||||
ignore:
|
||||
specifier: ^7.0.6
|
||||
version: 7.0.6
|
||||
@@ -222,6 +223,46 @@ importers:
|
||||
specifier: ^3.25.76
|
||||
version: 3.25.76
|
||||
|
||||
sdk:
|
||||
dependencies:
|
||||
'@refinedev/core':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.12(@tanstack/[email protected]([email protected]))(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.0.0
|
||||
version: 5.101.4([email protected])
|
||||
i18next:
|
||||
specifier: ^25.3.2
|
||||
version: 25.10.10([email protected])
|
||||
ignore:
|
||||
specifier: ^7.0.6
|
||||
version: 7.0.6
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.1.0
|
||||
react-router:
|
||||
specifier: ^7.0.0
|
||||
version: 7.18.1([email protected]([email protected]))([email protected])
|
||||
semver:
|
||||
specifier: ^7.8.5
|
||||
version: 7.8.5
|
||||
vite:
|
||||
specifier: '>=6.0.0 <8.0.0'
|
||||
version: 6.4.3(@types/[email protected])([email protected])([email protected])
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.0.0
|
||||
version: 20.19.43
|
||||
'@types/react':
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.17
|
||||
'@types/semver':
|
||||
specifier: ^7.7.0
|
||||
version: 7.7.1
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@ai-sdk/[email protected]':
|
||||
@@ -6709,7 +6750,7 @@ snapshots:
|
||||
json-schema-typed: 7.0.3
|
||||
onetime: 5.1.2
|
||||
pkg-up: 3.1.0
|
||||
semver: 7.5.2
|
||||
semver: 7.8.5
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
packages:
|
||||
- sdk
|
||||
|
||||
allowBuilds:
|
||||
'@nocobase/portal-sdk': true
|
||||
esbuild: true
|
||||
|
||||
+32
-7
@@ -8,6 +8,7 @@
|
||||
"title": "NocoBase Route Surfaces",
|
||||
"description": "URL-backed drawer, dialog, and page containers with nested drawer stacking and a shared close lifecycle.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@base-ui/react@^1.6.0",
|
||||
"@refinedev/core@^5.0.8",
|
||||
"lucide-react@^0.487.0",
|
||||
@@ -36,6 +37,7 @@
|
||||
"title": "NocoBase Users Example",
|
||||
"description": "Complete Users resource example with ACL-aware list, filters, route-backed drawers, nested role details, and optional AI page context.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@refinedev/core@^5.0.8",
|
||||
"@refinedev/react-hook-form@^5.0.0",
|
||||
"@refinedev/react-table@^6.0.0",
|
||||
@@ -66,8 +68,9 @@
|
||||
"name": "i18n",
|
||||
"type": "registry:block",
|
||||
"title": "NocoBase i18n",
|
||||
"description": "Optional NocoBase server-language resources, user preference persistence, reusable language controls, and an integrated showcase for the Starter's built-in i18n runtime.",
|
||||
"description": "Optional NocoBase server-language resources, user preference persistence, reusable language controls, and an integrated showcase for the Portal SDK i18n runtime.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@refinedev/core@^5.0.8",
|
||||
"lucide-react@^0.487.0",
|
||||
"react-router@^7.0.2"
|
||||
@@ -82,7 +85,7 @@
|
||||
"select",
|
||||
"textarea"
|
||||
],
|
||||
"docs": "Requires a compatible NocoBase Starter with the built-in i18n provider, shared System Settings bootstrap, and AppExtension Provider/UserMenuItems host fields. The Registry reuses systemSettings:get data and calls app:getLang only for registered dynamic namespaces such as lm-collections.",
|
||||
"docs": "Requires a compatible Portal SDK i18n and System Settings runtime plus the template's AppExtension Provider/UserMenuItems host fields. The Registry reuses cached systemSettings:get data and calls app:getLang only for registered dynamic namespaces such as lm-collections.",
|
||||
"source": {
|
||||
"root": "registry/nocobase-i18n",
|
||||
"target": "src/extensions/nocobase-i18n",
|
||||
@@ -95,6 +98,7 @@
|
||||
"title": "NocoBase OIDC Authentication",
|
||||
"description": "Dynamic OIDC sign-in adapter, headless sign-in hook, and integrated component demo.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"lucide-react@^0.487.0",
|
||||
"react-router@^7.0.2"
|
||||
],
|
||||
@@ -111,6 +115,7 @@
|
||||
"title": "NocoBase SAML Authentication",
|
||||
"description": "Dynamic SAML sign-in adapter, automatic redirect provider, headless hook, and integrated demo.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"lucide-react@^0.487.0",
|
||||
"react-router@^7.0.2"
|
||||
],
|
||||
@@ -127,6 +132,7 @@
|
||||
"title": "NocoBase CAS Authentication",
|
||||
"description": "Dynamic CAS sign-in adapter, headless sign-in hook, and integrated component demo.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"lucide-react@^0.487.0",
|
||||
"react-router@^7.0.2"
|
||||
],
|
||||
@@ -142,6 +148,7 @@
|
||||
"type": "registry:block",
|
||||
"title": "NocoBase LDAP Authentication",
|
||||
"description": "Dynamic LDAP account/password form, headless sign-in hook, and integrated component demo.",
|
||||
"dependencies": ["@nocobase/portal-sdk@^1.0.0"],
|
||||
"docs": "Requires the Starter authentication runtime and an enabled LDAP authenticator in NocoBase.",
|
||||
"source": {
|
||||
"root": "registry/nocobase-auth-ldap",
|
||||
@@ -154,6 +161,7 @@
|
||||
"type": "registry:block",
|
||||
"title": "NocoBase SMS Authentication",
|
||||
"description": "Dynamic SMS verification-code form, delivery countdown, headless hook, and integrated demo.",
|
||||
"dependencies": ["@nocobase/portal-sdk@^1.0.0"],
|
||||
"docs": "Requires the Starter authentication runtime, an enabled SMS authenticator, and a configured public SMS verifier in NocoBase.",
|
||||
"source": {
|
||||
"root": "registry/nocobase-auth-sms",
|
||||
@@ -167,6 +175,7 @@
|
||||
"title": "NocoBase WeCom Authentication",
|
||||
"description": "Dynamic WeCom sign-in adapter, browser automatic login, headless hook, and integrated demo.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"lucide-react@^0.487.0",
|
||||
"react-router@^7.0.2"
|
||||
],
|
||||
@@ -183,6 +192,7 @@
|
||||
"title": "NocoBase DingTalk Authentication",
|
||||
"description": "Dynamic DingTalk sign-in adapter, automatic login, headless hook, and integrated demo.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"[email protected]",
|
||||
"lucide-react@^0.487.0",
|
||||
"react-router@^7.0.2"
|
||||
@@ -200,6 +210,7 @@
|
||||
"title": "NocoBase AI Runtime",
|
||||
"description": "NocoBase AI services, providers, transports, controllers, and avatars.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@ai-sdk/[email protected]",
|
||||
"[email protected]",
|
||||
"@dicebear/core@^9.4.3",
|
||||
@@ -266,7 +277,11 @@
|
||||
"type": "registry:block",
|
||||
"title": "NocoBase AI Components and Demo",
|
||||
"description": "Complete NocoBase AI component runtime with application resources, routes, and component showcase pages.",
|
||||
"dependencies": ["@refinedev/core@^5.0.8", "react-router@^7.0.2"],
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@refinedev/core@^5.0.8",
|
||||
"react-router@^7.0.2"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"@nocobase/ai-chat",
|
||||
"@nocobase/ai-react-hook-form",
|
||||
@@ -293,6 +308,7 @@
|
||||
"title": "NocoBase File Upload",
|
||||
"description": "Controlled shadcn/ui file upload field for NocoBase file collections.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@tanstack/react-query@^5.81.5",
|
||||
"lucide-react@^0.487.0"
|
||||
],
|
||||
@@ -302,7 +318,7 @@
|
||||
"dialog",
|
||||
"tooltip"
|
||||
],
|
||||
"docs": "Requires the Starter NocoBase client and a field targeting a file-template collection.",
|
||||
"docs": "Requires a compatible Portal SDK client and a field targeting a file-template collection.",
|
||||
"source": {
|
||||
"root": "registry/nocobase-file-upload",
|
||||
"target": "src/extensions/nocobase-file-upload",
|
||||
@@ -315,6 +331,7 @@
|
||||
"title": "NocoBase Mail",
|
||||
"description": "Mail inbox, compose, detail, and label components backed by the NocoBase mail API.",
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@refinedev/core@^5.0.8",
|
||||
"@tiptap/extension-link@^3.29.0",
|
||||
"@tiptap/extension-placeholder@^3.29.0",
|
||||
@@ -346,7 +363,7 @@
|
||||
"textarea",
|
||||
"toggle"
|
||||
],
|
||||
"docs": "Requires a compatible NocoBase Starter with the mail plugin enabled. The mail API client uses the Starter's built-in API_URL, getNocoBaseHeaders, and NOCOBASE_TOKEN_KEY from @/providers/constants.",
|
||||
"docs": "Requires a compatible Portal SDK and a NocoBase server with the mail plugin enabled. The mail API adapter uses the authenticated SDK client.",
|
||||
"source": {
|
||||
"root": "registry/nocobase-mail",
|
||||
"target": "src/extensions/nocobase-mail",
|
||||
@@ -358,7 +375,11 @@
|
||||
"type": "registry:block",
|
||||
"title": "NocoBase ACL UI",
|
||||
"description": "Page, region, field, and role-switching components backed by the Starter's NocoBase accessControlProvider.",
|
||||
"dependencies": ["@refinedev/core@^5.0.8", "lucide-react@^0.487.0"],
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@refinedev/core@^5.0.8",
|
||||
"lucide-react@^0.487.0"
|
||||
],
|
||||
"registryDependencies": ["badge", "select"],
|
||||
"docs": "Requires a compatible NocoBase Starter with the built-in accessControlProvider and ACL store.",
|
||||
"source": {
|
||||
@@ -372,7 +393,11 @@
|
||||
"type": "registry:block",
|
||||
"title": "NocoBase ACL Components and Demo",
|
||||
"description": "Complete ACL composition demo with live role switching, action matrix, multi-collection regions, and field permissions.",
|
||||
"dependencies": ["@refinedev/core@^5.0.8", "react-router@^7.0.2"],
|
||||
"dependencies": [
|
||||
"@nocobase/portal-sdk@^1.0.0",
|
||||
"@refinedev/core@^5.0.8",
|
||||
"react-router@^7.0.2"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"@nocobase/acl-ui",
|
||||
"button",
|
||||
|
||||
@@ -30,7 +30,7 @@ effective role names. Its result follows the familiar Refine query shape with
|
||||
ACL role set, not every role assigned to the signed-in user.
|
||||
|
||||
```tsx
|
||||
import { useGetRoles } from "@/lib/nocobase/acl";
|
||||
import { useGetRoles } from "@nocobase/portal-sdk/acl";
|
||||
|
||||
function CurrentAccessContext() {
|
||||
const { data: roles, isLoading } = useGetRoles();
|
||||
@@ -41,7 +41,7 @@ function CurrentAccessContext() {
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { defineAppRoutes } from "@/app/route-runtime";
|
||||
import { defineAppRoutes } from "@nocobase/portal-sdk/routing";
|
||||
|
||||
export const appRoutes = defineAppRoutes([
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { BaseKey } from "@refinedev/core";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
import { AccessDenied } from "@/components/access-control/access-denied";
|
||||
import { useAclEvaluator } from "@/lib/nocobase/acl";
|
||||
import type { RoleConstraint } from "@/lib/nocobase/acl";
|
||||
import { useAclEvaluator } from "@nocobase/portal-sdk/acl";
|
||||
import type { RoleConstraint } from "@nocobase/portal-sdk/acl";
|
||||
|
||||
export type AclPermission = {
|
||||
resource: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Role, RoleMode } from "@/lib/nocobase/acl";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import type { Role, RoleMode } from "@nocobase/portal-sdk/acl";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
export const UNION_ROLE = "__union__";
|
||||
export const ANONYMOUS_ROLE = "anonymous";
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useAclState,
|
||||
type AclIdentity,
|
||||
type Role,
|
||||
} from "@/lib/nocobase/acl";
|
||||
} from "@nocobase/portal-sdk/acl";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getRoleOptions, resolveRoleTitle, UNION_ROLE } from "./role-options";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type AclActionParams,
|
||||
type AclState,
|
||||
type AclStore,
|
||||
} from "@/lib/nocobase/acl";
|
||||
} from "@nocobase/portal-sdk/acl";
|
||||
|
||||
type PreviewRecordPermission = {
|
||||
dataSourceKey?: string;
|
||||
|
||||
@@ -14,8 +14,8 @@ import type { TreeMenuItem } from "@refinedev/core";
|
||||
import {
|
||||
buildRouteResources,
|
||||
defineAppRoutes,
|
||||
RouteAccessGuard,
|
||||
} from "@/app/route-runtime";
|
||||
} from "@nocobase/portal-sdk/routing";
|
||||
import { RouteAccessGuard } from "@/app/route-access-guard";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CanAccess } from "@/components/access-control/can-access";
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
filterMenuItemsByAcl,
|
||||
type RoleConstraint,
|
||||
useAclState,
|
||||
} from "@/lib/nocobase/acl";
|
||||
} from "@nocobase/portal-sdk/acl";
|
||||
import { AclField, AclPage, AclRegion } from "../components/acl-boundary";
|
||||
import { AclPreviewProvider } from "./acl-preview-provider";
|
||||
import { AclBoundaryApi } from "./boundary-api";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppExtension } from "@/app/extensions";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { Blocks, PanelsTopLeft, ShieldCheck } from "lucide-react";
|
||||
import { lazy } from "react";
|
||||
import { Outlet, Route } from "react-router";
|
||||
|
||||
@@ -13,20 +13,20 @@ try {
|
||||
getEffectiveRoles,
|
||||
getPermissionsForDataSource,
|
||||
resolveActionPermission,
|
||||
} = await server.ssrLoadModule("/src/lib/nocobase/acl/evaluator.ts");
|
||||
} = await server.ssrLoadModule("/sdk/src/acl/evaluator.ts");
|
||||
const { resolveAclDataSourceKey } = await server.ssrLoadModule(
|
||||
"/src/lib/nocobase/acl/data-source.ts"
|
||||
"/sdk/src/acl/data-source.ts"
|
||||
);
|
||||
const { filterMenuItemsByAcl, findFirstAccessibleRoute } =
|
||||
await server.ssrLoadModule("/src/lib/nocobase/acl/menu.ts");
|
||||
await server.ssrLoadModule("/sdk/src/acl/menu.ts");
|
||||
const { buildRouteResources, defineAppRoutes } = await server.ssrLoadModule(
|
||||
"/src/app/route-runtime.ts"
|
||||
"/sdk/src/routing/index.ts"
|
||||
);
|
||||
const {
|
||||
clearRecordPermissions,
|
||||
getRecordActionPermission,
|
||||
updateRecordPermissions,
|
||||
} = await server.ssrLoadModule("/src/lib/nocobase/acl/record-permissions.ts");
|
||||
} = await server.ssrLoadModule("/sdk/src/acl/record-permissions.ts");
|
||||
const { RoleSwitcher } = await server.ssrLoadModule(
|
||||
"@/extensions/nocobase-acl/components/role-switcher.tsx"
|
||||
);
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
The Registry is split by capability so applications only install the code and dependencies they use.
|
||||
|
||||
These entries target the compatible NocoBase admin starter family. The host must
|
||||
provide the `@` source alias, `@/lib/nocobase/client`, the standard shadcn UI
|
||||
components, and (for `@nocobase/ai`) the `@/app/extension` discovery contract.
|
||||
provide the `@` source alias, `@nocobase/portal-sdk/client`, the standard shadcn
|
||||
UI components, and (for `@nocobase/ai`) the
|
||||
`@nocobase/portal-sdk/extensions` discovery contract.
|
||||
Applications with a different host can still reuse the providers and components,
|
||||
but should supply their own `AIService` adapter and integration entry.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NocoBaseAIExtensionProvider } from "./global-ai-chat";
|
||||
import type { AppExtension } from "@/app/extensions";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { LoadingState } from "@/components/app-shell/loading-state";
|
||||
import {
|
||||
Bot,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nocobaseClient, type NocoBaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient, type NocoBaseClient } from "@nocobase/portal-sdk/client";
|
||||
import type {
|
||||
AIChatMessage,
|
||||
AIConversation,
|
||||
|
||||
@@ -40,7 +40,7 @@ try {
|
||||
"/registry/nocobase-ai/providers/sub-agent-stream.ts"
|
||||
);
|
||||
const { NocoBaseClient } = await server.ssrLoadModule(
|
||||
"/src/lib/nocobase/client.ts"
|
||||
"@nocobase/portal-sdk/client"
|
||||
);
|
||||
|
||||
{
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { LogIn } from "lucide-react";
|
||||
|
||||
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
||||
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
import { useCasSignIn } from "./use-cas-sign-in";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { KeyRound } from "lucide-react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { AuthDemoRoute } from "@/components/auth/demo";
|
||||
|
||||
const CasSignInButton = lazy(() => import("./cas-sign-in-button"));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import type { Authenticator } from "@/components/auth/types";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { resolvePortalUrl } from "@/providers/runtime-config";
|
||||
import type { Authenticator } from "@nocobase/portal-sdk/auth";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
|
||||
|
||||
export function useCasSignIn(authenticator: Authenticator) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
import { LoadingState } from "@/components/app-shell/loading-state";
|
||||
import { clearAcl } from "@/lib/nocobase/acl";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { resolvePortalUrl } from "@/providers/runtime-config";
|
||||
import { clearAcl } from "@nocobase/portal-sdk/acl";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
|
||||
|
||||
import { isDingtalkBrowser } from "./use-dingtalk-sign-in";
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { MessageSquare } from "lucide-react";
|
||||
|
||||
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
||||
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
import { useDingtalkSignIn } from "./use-dingtalk-sign-in";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { AuthDemoRoute } from "@/components/auth/demo";
|
||||
|
||||
const DingtalkSignInButton = lazy(() => import("./dingtalk-sign-in-button"));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import type { Authenticator } from "@/components/auth/types";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { resolvePortalUrl } from "@/providers/runtime-config";
|
||||
import type { Authenticator } from "@nocobase/portal-sdk/auth";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
|
||||
|
||||
type DingtalkAuthUrlResponse = {
|
||||
url?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { ContactRound } from "lucide-react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { AuthDemoRoute } from "@/components/auth/demo";
|
||||
|
||||
const LdapSignInForm = lazy(() => import("./ldap-sign-in-form"));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { InputPassword } from "@/components/auth/input-password";
|
||||
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
||||
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAuthenticatorSignIn } from "@/components/auth";
|
||||
import { useAuthenticatorSignIn } from "@nocobase/portal-sdk/auth";
|
||||
|
||||
export function useLdapSignIn(authenticator: string) {
|
||||
return useAuthenticatorSignIn(authenticator);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { LogIn } from "lucide-react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { AuthDemoRoute } from "@/components/auth/demo";
|
||||
|
||||
const OidcSignInButton = lazy(() => import("./oidc-sign-in-button"));
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { LogIn } from "lucide-react";
|
||||
|
||||
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
||||
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
import { useOidcSignIn } from "./use-oidc-sign-in";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import type { Authenticator } from "@/components/auth/types";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { resolvePortalUrl } from "@/providers/runtime-config";
|
||||
import type { Authenticator } from "@nocobase/portal-sdk/auth";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
|
||||
|
||||
export function useOidcSignIn(authenticator: Authenticator) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { BadgeCheck } from "lucide-react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { AuthDemoRoute } from "@/components/auth/demo";
|
||||
|
||||
const SamlSignInButton = lazy(() => import("./saml-sign-in-button"));
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { LogIn } from "lucide-react";
|
||||
|
||||
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
||||
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
import { useSamlSignIn } from "./use-saml-sign-in";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import type { Authenticator } from "@/components/auth/types";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { resolvePortalUrl } from "@/providers/runtime-config";
|
||||
import type { Authenticator } from "@nocobase/portal-sdk/auth";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
|
||||
|
||||
export function useSamlSignIn(authenticator: Authenticator) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { Smartphone } from "lucide-react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import type { AppExtension } from "@/app/extensions";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { AuthDemoRoute } from "@/components/auth/demo/auth-demo-route";
|
||||
|
||||
const SmsSignInForm = lazy(() => import("./sms-sign-in-form"));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
||||
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAuthenticatorSignIn } from "@/components/auth";
|
||||
import type { Authenticator } from "@/components/auth/types";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { useAuthenticatorSignIn } from "@nocobase/portal-sdk/auth";
|
||||
import type { Authenticator } from "@nocobase/portal-sdk/auth";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
|
||||
type SmsCodeResponse = {
|
||||
expiresAt?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { Building2 } from "lucide-react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { AuthDemoRoute } from "@/components/auth/demo";
|
||||
|
||||
const WecomSignInButton = lazy(() => import("./wecom-sign-in-button"));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import type { Authenticator } from "@/components/auth/types";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { resolvePortalUrl } from "@/providers/runtime-config";
|
||||
import type { Authenticator } from "@nocobase/portal-sdk/auth";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
|
||||
|
||||
type WecomAuthUrlResponse = {
|
||||
url?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Building2 } from "lucide-react";
|
||||
|
||||
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
||||
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
import { useWecomSignIn } from "./use-wecom-sign-in";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
|
||||
import { getDataSourceHeaders } from "./storage";
|
||||
import { isNocoBaseManagedFileUrl } from "./file-url-policy";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
|
||||
import { getDataSourceHeaders } from "./storage";
|
||||
import type {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
|
||||
import { getDataSourceHeaders } from "./storage";
|
||||
import type { FileUploadOptions, NocoBaseFileRecord } from "./types";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
|
||||
import { getDataSourceHeaders } from "./storage";
|
||||
import type { FileFieldDescriptor, StorageCheckResult } from "./types";
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
Optional NocoBase server-language integration and language controls for the
|
||||
NocoBase Admin Starter.
|
||||
|
||||
The Starter owns the Refine i18n provider, local i18next runtime, application
|
||||
resources, locale selection, and the shared `systemSettings:get` bootstrap.
|
||||
This Registry reuses those foundations and adds only the NocoBase-specific
|
||||
remote capabilities:
|
||||
The Portal SDK owns the Refine i18n provider, i18next runtime, translation
|
||||
registration protocol, locale state, and shared `systemSettings:get` store.
|
||||
The application keeps its locale configuration and translation resources in
|
||||
template-owned source. This Registry adds the NocoBase-specific remote
|
||||
capabilities:
|
||||
|
||||
- load registered dynamic namespaces from `app:getLang`;
|
||||
- persist a signed-in user's selected language through `users:updateLang`;
|
||||
@@ -23,9 +24,9 @@ Application-owned React translations belong in `src/locales`, outside the
|
||||
installed Registry directory:
|
||||
|
||||
```ts
|
||||
import { registerLocaleResources } from "@/providers/i18n";
|
||||
import { registerTranslationResources } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
registerLocaleResources("my-feature", {
|
||||
registerTranslationResources("my-feature", {
|
||||
"en-US": { title: "Orders" },
|
||||
"zh-CN": { title: "订单" },
|
||||
});
|
||||
@@ -41,4 +42,4 @@ registerServerResourceNamespace("my-dynamic-namespace");
|
||||
|
||||
Namespaces registered after startup are loaded incrementally. Existing exact
|
||||
NocoBase expressions such as `{{t("Orders")}}` remain supported by the
|
||||
Starter's translation compatibility helper.
|
||||
Portal SDK's translation compatibility helper.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useGetLocale, useSetLocale, useTranslate } from "@refinedev/core";
|
||||
import { useEnabledLocales } from "@nocobase/portal-sdk/i18n";
|
||||
import { Languages, Loader2 } from "lucide-react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEnabledLocales } from "@/providers/i18n";
|
||||
|
||||
export type LanguageSwitcherProps = {
|
||||
className?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useGetLocale, useTranslate } from "@refinedev/core";
|
||||
import { useEnabledLocales } from "@nocobase/portal-sdk/i18n";
|
||||
import { Database, Languages, PanelsTopLeft } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -9,11 +10,8 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import {
|
||||
LanguageSwitcher,
|
||||
useEnabledLocales,
|
||||
} from "..";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { LanguageSwitcher } from "..";
|
||||
import { I18nPromptGenerator } from "./prompt-generator";
|
||||
|
||||
export function I18nDemoPage() {
|
||||
|
||||
@@ -28,12 +28,12 @@ Application scene
|
||||
- Translate the complete visible user flow, including empty, loading, validation, and error states.
|
||||
|
||||
Implementation contract
|
||||
- Use the Starter's @/providers/i18n runtime and the application's translation and locale hooks.
|
||||
- Put application-owned React messages in src/locales and register the app namespace through registerTranslationResources from @/lib/i18n.
|
||||
- Keep each Registry component's messages in its own namespace and register them with registerLocaleResources().
|
||||
- Use the documented @nocobase/portal-sdk/i18n runtime and the application's translation and locale hooks.
|
||||
- Put application-owned React messages in src/locales and register the app namespace through registerTranslationResources from @nocobase/portal-sdk/i18n.
|
||||
- Keep each Registry component's messages in its own namespace and register them with registerTranslationResources().
|
||||
- Do not place application messages inside src/extensions/nocobase-i18n/locales because installed Registry files may be refreshed independently.
|
||||
- Use the reusable LanguageSwitcher when the page needs an explicit language control; the signed-in user menu already includes one.
|
||||
- Reuse the Starter's shared system settings instead of requesting systemSettings:get again.
|
||||
- Reuse useSystemSettings from @nocobase/portal-sdk/system-settings instead of requesting systemSettings:get again.
|
||||
- Keep Starter and Registry UI resources local. Use app:getLang only for registered dynamic server namespaces such as lm-collections when the NocoBase i18n Registry is installed.
|
||||
- Let the shared NocoBase client send the selected locale through X-Locale.
|
||||
- Preserve compatibility when server metadata contains exact {{t("...")}} expressions.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppExtension } from "@/app/extensions";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { LoadingState } from "@/components/app-shell/loading-state";
|
||||
import { Languages } from "lucide-react";
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export * from "./components";
|
||||
export * from "./provider";
|
||||
export * from "./server-resources";
|
||||
export {
|
||||
registerLocaleResources,
|
||||
useEnabledLocales,
|
||||
} from "@/providers/i18n";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { registerLocaleResources } from "@/providers/i18n";
|
||||
import { registerTranslationResources } from "@nocobase/portal-sdk/i18n";
|
||||
import enUS from "./en-US";
|
||||
import zhCN from "./zh-CN";
|
||||
|
||||
registerLocaleResources("nocobase-i18n", {
|
||||
registerTranslationResources("nocobase-i18n", {
|
||||
"en-US": enUS,
|
||||
"zh-CN": zhCN,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState, type PropsWithChildren } from "react";
|
||||
|
||||
import { LoadingState } from "@/components/app-shell/loading-state";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { setLocalePersistence } from "@/providers/i18n";
|
||||
import { useSystemSettings } from "@/providers/system-settings";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import { setLocalePersistence } from "@nocobase/portal-sdk/i18n";
|
||||
import { useSystemSettings } from "@nocobase/portal-sdk/system-settings";
|
||||
import { loadServerLocaleResources } from "./server-resources";
|
||||
|
||||
export function NocoBaseI18nBootstrap({ children }: PropsWithChildren) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import {
|
||||
type LocaleSystemSettings,
|
||||
applyDocumentLocale,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
i18n,
|
||||
registerLocale,
|
||||
setEnabledLocales,
|
||||
} from "@/providers/i18n";
|
||||
} from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
type ServerLanguagePayload = {
|
||||
lang?: string;
|
||||
|
||||
@@ -8,16 +8,19 @@ const server = await createServer({
|
||||
});
|
||||
|
||||
try {
|
||||
const { portalI18nReady } = await server.ssrLoadModule(
|
||||
"/src/providers/i18n/runtime.ts"
|
||||
);
|
||||
await portalI18nReady;
|
||||
await server.ssrLoadModule("/src/locales/index.ts");
|
||||
const {
|
||||
applySystemLocale,
|
||||
getCurrentLocale,
|
||||
i18n,
|
||||
registerLocaleResources,
|
||||
registerTranslationResources,
|
||||
resolveTranslatableText,
|
||||
translate,
|
||||
} = await server.ssrLoadModule("/src/providers/i18n/runtime.ts");
|
||||
const { resolveTranslatableText } =
|
||||
await server.ssrLoadModule("/src/lib/i18n.ts");
|
||||
} = await server.ssrLoadModule("@nocobase/portal-sdk/i18n");
|
||||
|
||||
await i18n.changeLanguage("en-US");
|
||||
assert.equal(translate("buttons.create", { ns: "starter" }, "Create"), "Create");
|
||||
@@ -30,7 +33,7 @@ try {
|
||||
"全部权限"
|
||||
);
|
||||
|
||||
registerLocaleResources("example-feature", {
|
||||
registerTranslationResources("example-feature", {
|
||||
"en-US": { title: "Example" },
|
||||
"zh-CN": { title: "示例" },
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ After installation, import the components from `@/extensions/nocobase-mail`.
|
||||
- Message detail resolves authenticated `cid:` inline images and keeps those images out of the
|
||||
regular attachment list.
|
||||
|
||||
The mail API client (`mailApi`) uses the Starter's built-in `API_URL`, `getNocoBaseHeaders`, and `NOCOBASE_TOKEN_KEY` from `@/providers/constants`. A compatible NocoBase backend with the mail plugin enabled is required.
|
||||
The mail API client (`mailApi`) uses `nocobaseClient` from `@nocobase/portal-sdk/client`. A compatible NocoBase backend with the mail plugin enabled is required.
|
||||
|
||||
## Send modes
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient } from "@nocobase/portal-sdk/client";
|
||||
import type {
|
||||
MailAccount,
|
||||
MailIdentity,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import {
|
||||
ListFilter,
|
||||
Mail,
|
||||
|
||||
@@ -114,7 +114,7 @@ try {
|
||||
"/registry/nocobase-mail/components/mail-api.ts"
|
||||
);
|
||||
const { nocobaseClient } = await server.ssrLoadModule(
|
||||
"/src/lib/nocobase/client.ts"
|
||||
"@nocobase/portal-sdk/client"
|
||||
);
|
||||
const { default: mailExtension } = await server.ssrLoadModule(
|
||||
"/registry/nocobase-mail/extension.tsx"
|
||||
|
||||
@@ -19,8 +19,8 @@ when the child should replace the list as a full page.
|
||||
- `RouteDrawer` supports URL-backed nested drawers and push-style stacking.
|
||||
- `RouteDialog` provides the same close contract for modal routes.
|
||||
- `RoutePage` provides the close context without an overlay.
|
||||
- `useRouteSurfaceClose` lets content request a close without knowing how it is
|
||||
presented.
|
||||
- SDK `useRouteSurfaceClose` lets content request a close without knowing how it
|
||||
is presented.
|
||||
- `useRefineUnsavedChangesGuard` adapts Refine's unsaved-change state to the
|
||||
route surface close lifecycle and renders a shadcn Alert Dialog confirmation.
|
||||
|
||||
@@ -30,6 +30,9 @@ but use relative navigation and a host-specific `closeTo` URL; do not send a
|
||||
child action to a fixed top-level resource URL. Preserve the complete opening
|
||||
URL in navigation state so closing restores list filters, pagination, tabs, and
|
||||
hash state; use the resolved parent route as the direct-entry fallback.
|
||||
Use `createRouteSurfaceNavigationState` and `resolveRouteSurfaceCloseTo` from
|
||||
`@nocobase/portal-sdk/routing` for that shared navigation protocol instead of
|
||||
copying host-specific state helpers.
|
||||
|
||||
Route definitions, ACL guards, resource data fetching, and application-specific
|
||||
paths remain application concerns.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ArrowLeft, ArrowRight, Eye, Layers3, Pencil, Plus } from "lucide-react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import {
|
||||
buildRouteLocationHref,
|
||||
createRouteSurfaceNavigationState,
|
||||
resolveRouteSurfaceCloseTo,
|
||||
useRouteSurfaceClose,
|
||||
} from "@nocobase/portal-sdk/routing";
|
||||
import {
|
||||
useLocation,
|
||||
useNavigate,
|
||||
@@ -21,16 +27,10 @@ import {
|
||||
RouteDialog,
|
||||
RouteDrawer,
|
||||
RoutePage,
|
||||
useRouteSurfaceClose,
|
||||
} from "../index";
|
||||
import { RouteSurfacePromptGenerator } from "./prompt-generator";
|
||||
import { ResourceActionGuide } from "./resource-action-guide";
|
||||
import { routeSurfaceScenarios } from "./scenarios";
|
||||
import {
|
||||
buildRouteLocationHref,
|
||||
createRouteSurfaceNavigationState,
|
||||
resolveRouteSurfaceCloseTo,
|
||||
} from "./contextual-navigation";
|
||||
|
||||
const routeSurfaceRoot = "/route-surfaces";
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ Scenario:
|
||||
- ${presentation}
|
||||
|
||||
Implementation requirements:
|
||||
- Import RouteDrawer, RouteDialog, RoutePage, and useRouteSurfaceClose from @/extensions/nocobase-route-surfaces as needed.
|
||||
- Import RouteDrawer, RouteDialog, and RoutePage from @/extensions/nocobase-route-surfaces; import useRouteSurfaceClose from @nocobase/portal-sdk/routing when actions need to close the active surface.
|
||||
- Define application-owned resource routes once with defineAppRoutes. resourceAction only binds a child path to the parent Refine resource's create, edit, or show URL; it does not choose a presentation.
|
||||
- With the default automatic resource outlet, every resourceAction element must render a RouteDrawer or RouteDialog. Never place a plain full-page form or detail component there because it will render below the list.
|
||||
- When a resource action must replace the list as a full page, set outlet: "manual" on the resource route and use an application-owned layout that renders or consumes useOutlet().
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { Layers3 } from "lucide-react";
|
||||
import { lazy } from "react";
|
||||
import { Route } from "react-router";
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
export * from "./route-dialog";
|
||||
export * from "./route-drawer";
|
||||
export * from "./route-page";
|
||||
export * from "./route-surface-context";
|
||||
export * from "./use-refine-unsaved-changes";
|
||||
export * from "./use-route-surface-close";
|
||||
export type { RouteSurfaceBeforeClose } from "./use-route-surface-state";
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { type ReactNode, useCallback, useContext } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
RouteOverlayDepthContext,
|
||||
RouteSurfaceContext,
|
||||
} from "./route-surface-context";
|
||||
import {
|
||||
type RouteSurfaceBeforeClose,
|
||||
useRouteSurfaceState,
|
||||
} from "./use-route-surface-state";
|
||||
} from "@nocobase/portal-sdk/routing";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function RouteDialog({
|
||||
title,
|
||||
|
||||
@@ -6,17 +6,15 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
} from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
RouteOverlayDepthContext,
|
||||
RouteSurfaceContext,
|
||||
} from "./route-surface-context";
|
||||
import {
|
||||
type RouteSurfaceBeforeClose,
|
||||
useRouteSurfaceState,
|
||||
} from "./use-route-surface-state";
|
||||
} from "@nocobase/portal-sdk/routing";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function RouteDrawer({
|
||||
title,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
import { RouteSurfaceContext } from "./route-surface-context";
|
||||
import {
|
||||
RouteSurfaceContext,
|
||||
type RouteSurfaceBeforeClose,
|
||||
useRouteSurfaceState,
|
||||
} from "./use-route-surface-state";
|
||||
} from "@nocobase/portal-sdk/routing";
|
||||
|
||||
export function RoutePage({
|
||||
children,
|
||||
|
||||
@@ -13,9 +13,7 @@ try {
|
||||
buildRouteLocationHref,
|
||||
createRouteSurfaceNavigationState,
|
||||
resolveRouteSurfaceCloseTo,
|
||||
} = await server.ssrLoadModule(
|
||||
"/registry/nocobase-route-surfaces/demo/contextual-navigation.ts"
|
||||
);
|
||||
} = await server.ssrLoadModule("@nocobase/portal-sdk/routing");
|
||||
const location = {
|
||||
pathname: "/customers",
|
||||
search: "?status=renewal&page=2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UsersRound } from "lucide-react";
|
||||
|
||||
import { defineAppRoutes } from "@/app/route-runtime";
|
||||
import { defineAppRoutes } from "@nocobase/portal-sdk/routing";
|
||||
import { AccessDenied } from "@/components/access-control/access-denied";
|
||||
import { CanAccess } from "@/components/access-control/can-access";
|
||||
import { UserCreate } from "./create";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type HttpError, useTranslate } from "@refinedev/core";
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
import { useMemo } from "react";
|
||||
import { useRouteSurfaceClose } from "@nocobase/portal-sdk/routing";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form } from "@/components/ui/form";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
RouteDrawer,
|
||||
RouteDrawerFooter,
|
||||
useRefineUnsavedChangesGuard,
|
||||
useRouteSurfaceClose,
|
||||
} from "@/extensions/nocobase-route-surfaces";
|
||||
import {
|
||||
applyAIUserFormValues,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type HttpError, useTranslate } from "@refinedev/core";
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
import { useMemo } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useRouteSurfaceClose } from "@nocobase/portal-sdk/routing";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form } from "@/components/ui/form";
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
RouteDrawer,
|
||||
RouteDrawerFooter,
|
||||
useRefineUnsavedChangesGuard,
|
||||
useRouteSurfaceClose,
|
||||
} from "@/extensions/nocobase-route-surfaces";
|
||||
import {
|
||||
applyAIUserFormValues,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppExtension } from "@/app/extension";
|
||||
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
|
||||
import { usersExampleRoutes } from "./app-routes";
|
||||
import "./locales";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { EditButton } from "@/components/resources/buttons/edit";
|
||||
import { ShowButton } from "@/components/resources/buttons/show";
|
||||
import { ListView } from "@/components/resources/views/list-view";
|
||||
import { useAIPageElementHandle } from "./optional-ai";
|
||||
import type { Role } from "@/lib/nocobase/acl";
|
||||
import type { Role } from "@nocobase/portal-sdk/acl";
|
||||
import { RoleBadges } from "./role-badges";
|
||||
import { resolveRoleLabel } from "./role-utils";
|
||||
import { getRolePath } from "./routes";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { registerTranslationResources } from "@/lib/i18n";
|
||||
import { registerTranslationResources } from "@nocobase/portal-sdk/i18n";
|
||||
import { usersExample as enUS } from "./en-US";
|
||||
import { usersExample as zhCN } from "./zh-CN";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Role } from "@/lib/nocobase/acl";
|
||||
import type { Role } from "@nocobase/portal-sdk/acl";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { resolveRoleLabel } from "./role-utils";
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useAIPageElementHandle } from "./optional-ai";
|
||||
import { RouteDrawer } from "@/extensions/nocobase-route-surfaces";
|
||||
import type { Role } from "@/lib/nocobase/acl";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import type { Role } from "@nocobase/portal-sdk/acl";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
import { resolveRoleLabel } from "./role-utils";
|
||||
import { getUserShowPath, userRoutes } from "./routes";
|
||||
import type { RoleRecord } from "./types";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Role } from "@/lib/nocobase/acl";
|
||||
import { resolveTranslatableText } from "@/lib/i18n";
|
||||
import type { Role } from "@nocobase/portal-sdk/acl";
|
||||
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
|
||||
|
||||
export function resolveRoleLabel(role: Role) {
|
||||
return resolveTranslatableText(role.title || role.name, { ns: "starter" });
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
FileUploadFieldValue,
|
||||
SerializedFileFieldValue,
|
||||
} from "@/extensions/nocobase-file-upload";
|
||||
import type { Role } from "@/lib/nocobase/acl";
|
||||
import type { Role } from "@nocobase/portal-sdk/acl";
|
||||
|
||||
export type UserRecord = {
|
||||
id: string | number;
|
||||
|
||||
+20
-5
@@ -52,6 +52,7 @@ const config = JSON.parse(
|
||||
);
|
||||
const itemNames = new Set();
|
||||
const filesByRoot = new Map();
|
||||
const portalSdkImport = "@nocobase/portal-sdk";
|
||||
const sourceItems = config.items.map((item) => {
|
||||
if (!item.name || itemNames.has(item.name)) {
|
||||
throw new Error(`Registry item name must be unique: ${item.name}`);
|
||||
@@ -84,6 +85,25 @@ const sourceItems = config.items.map((item) => {
|
||||
);
|
||||
}
|
||||
|
||||
const includedSource = includedFiles
|
||||
.filter((file) => /\.[cm]?[jt]sx?$/.test(file))
|
||||
.map((file) => ({
|
||||
file,
|
||||
content: fs.readFileSync(path.join(sourceRoot, file), "utf8"),
|
||||
}));
|
||||
const usesPortalSdk = includedSource.some(({ content }) =>
|
||||
content.includes(portalSdkImport)
|
||||
);
|
||||
const portalSdkDependency = item.dependencies?.find((dependency) =>
|
||||
dependency.startsWith(`${portalSdkImport}@`)
|
||||
);
|
||||
|
||||
if (usesPortalSdk && !portalSdkDependency) {
|
||||
throw new Error(
|
||||
`Registry item ${item.name} imports ${portalSdkImport} without declaring a versioned dependency`
|
||||
);
|
||||
}
|
||||
|
||||
return { item, includedFiles };
|
||||
});
|
||||
|
||||
@@ -116,11 +136,6 @@ if (action === "build") {
|
||||
for (const item of items) {
|
||||
console.log(`${item.name}: ${item.files.length} files`);
|
||||
}
|
||||
} else if (action === "link" || action === "unlink") {
|
||||
for (const source of sourceMappings.values()) {
|
||||
if (action === "link") linkSource(source);
|
||||
else unlinkSource(source);
|
||||
}
|
||||
} else {
|
||||
const outputRootIndex = process.argv.indexOf("--output-root");
|
||||
if (outputRootIndex !== -1 && !process.argv[outputRootIndex + 1]) {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Portal SDK migrations
|
||||
|
||||
This guide defines how maintainers publish breaking Portal SDK changes and how
|
||||
Portal projects adopt them. The SDK version and the Default Template version
|
||||
are independent SemVer values connected by compatibility metadata.
|
||||
|
||||
## Version policy
|
||||
|
||||
- Patch SDK releases fix behavior without changing documented APIs.
|
||||
- Minor SDK releases add backward-compatible APIs.
|
||||
- Major SDK releases may remove or change documented APIs.
|
||||
- `nocobase.supportedDefaultTemplateRange` declares the base template versions
|
||||
with which that SDK release can run.
|
||||
- A derived Portal keeps its own package name and version, but preserves the
|
||||
exact base release it has incorporated in
|
||||
`nocobase.defaultTemplateVersion`.
|
||||
|
||||
Do not update `nocobase.defaultTemplateVersion` merely to satisfy the
|
||||
compatibility check. Update it only after the corresponding base template
|
||||
changes have actually been merged and verified.
|
||||
|
||||
## Choosing the next versions
|
||||
|
||||
When an SDK major release does not require template source changes, increment
|
||||
only the SDK major and declare the template range it genuinely supports.
|
||||
|
||||
When a breaking SDK release requires coordinated template source changes, use
|
||||
a new compatibility generation:
|
||||
|
||||
```text
|
||||
@nocobase/portal-sdk 1.x -> 2.0.0
|
||||
Default Portal Template 2.x -> 3.0.0
|
||||
```
|
||||
|
||||
The coordinated packages then declare:
|
||||
|
||||
```json
|
||||
// @nocobase/portal-sdk@2.0.0
|
||||
{
|
||||
"nocobase": {
|
||||
"supportedDefaultTemplateRange": ">=3.0.0 <4.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// Default Portal Template 3.0.0
|
||||
{
|
||||
"nocobase": {
|
||||
"defaultTemplateVersion": "3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/portal-sdk": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This generation rule keeps an already-published SDK range truthful. Use a
|
||||
template minor boundary only when the older SDK remains valid for later
|
||||
templates in the same template major.
|
||||
|
||||
## Maintainer release checklist
|
||||
|
||||
1. Increment the SDK major before changing or removing documented APIs.
|
||||
2. Adapt the base template, Registry source, tests, and examples.
|
||||
3. Change every affected Registry dependency to the new SDK major range.
|
||||
4. Set the SDK's minimum supported base template to the first release that
|
||||
contains all required adaptations.
|
||||
5. Add a section below that lists removed APIs, replacements, manual edits, and
|
||||
codemods.
|
||||
6. Build and test the SDK, template, Registry integrations, and packed
|
||||
artifacts.
|
||||
7. Publish the SDK and coordinated template release through the repository
|
||||
release workflow.
|
||||
|
||||
## Release workflow targets
|
||||
|
||||
The release workflow defaults to `auto`. It compares `sdk/` with the latest
|
||||
`portal-sdk-v*` tag and the rest of the repository with the latest template
|
||||
`v*` tag, then selects one of these targets:
|
||||
|
||||
- `sdk` publishes and tags only the SDK.
|
||||
- `template` publishes and tags only the template while verifying the installed
|
||||
SDK package.
|
||||
- `both` publishes coordinated SDK and template releases.
|
||||
|
||||
When no exact version is supplied, an already-tagged package receives a patch
|
||||
increment. Supply `sdk_version` or `version` for intentional minor or major
|
||||
releases. The workflow retains manual target overrides for release recovery and
|
||||
exceptional maintenance, but normal releases should use automatic detection.
|
||||
|
||||
## Portal project upgrade checklist
|
||||
|
||||
1. Commit or back up application-owned changes.
|
||||
2. Read the migration section for every skipped SDK major.
|
||||
3. Merge the required base template release without overwriting application
|
||||
pages, UI composition, translations, or installed extension customizations.
|
||||
4. Apply API replacements to application code and customized Registry source.
|
||||
5. Update `nocobase.defaultTemplateVersion` to the base release actually
|
||||
incorporated by the project.
|
||||
6. Update `@nocobase/portal-sdk` to the required major.
|
||||
7. Run `pnpm sdk:check`, install dependencies, build, and exercise direct URLs,
|
||||
authentication, ACL, locale switching, and routed surfaces.
|
||||
|
||||
The compatibility checker detects an invalid version combination; it does not
|
||||
merge template source or rewrite customized application code.
|
||||
|
||||
## Current migrations
|
||||
|
||||
There are no published major-version migrations yet. SDK 1.x is the initial
|
||||
public API generation for Default Portal Template 2.x.
|
||||
@@ -0,0 +1,49 @@
|
||||
# @nocobase/portal-sdk
|
||||
|
||||
Stable, upgradeable runtime APIs for applications based on the NocoBase
|
||||
Default Portal Template. Application composition, pages, visual components,
|
||||
themes, and Registry implementations remain owned by the template project.
|
||||
|
||||
Use documented package exports only. Imports from `src/` are not public API.
|
||||
|
||||
## Public entry points
|
||||
|
||||
- `@nocobase/portal-sdk/runtime` — Portal base, API URL, callback, and settings URL resolution.
|
||||
- `@nocobase/portal-sdk/client` — authenticated NocoBase client, session storage, and HTTP errors.
|
||||
- `@nocobase/portal-sdk/auth` — Refine authentication provider, callback capture, authenticator types, and headless hooks.
|
||||
- `@nocobase/portal-sdk/data` — NocoBase Refine data provider.
|
||||
- `@nocobase/portal-sdk/acl` — ACL store, evaluator, record permissions, hooks, and access-control provider.
|
||||
- `@nocobase/portal-sdk/routing` — application route definitions, Refine resource compilation, and route-surface lifecycle state.
|
||||
- `@nocobase/portal-sdk/extensions` — stable `AppExtension` ABI and pure contribution collection.
|
||||
- `@nocobase/portal-sdk/i18n` — translation registration, configurable i18next runtime, locale state, and Refine adapter.
|
||||
- `@nocobase/portal-sdk/system-settings` — cached System Settings access, context, and hooks.
|
||||
- `@nocobase/portal-sdk/vite` — build compatibility and raw Portal HTML plugins.
|
||||
|
||||
## Workspace development
|
||||
|
||||
The workspace manifest resolves public SDK entry points directly from `src/`,
|
||||
so the Portal Template's Vite process compiles and watches SDK changes without a
|
||||
separate SDK watcher. During `pnpm pack` and `pnpm publish`, pnpm applies
|
||||
`publishConfig.exports` and rewrites those entry points to the compiled `dist/`
|
||||
files. SDK releases must therefore use the repository's pnpm release workflow.
|
||||
|
||||
The release workflow verifies every packed export before publishing, including
|
||||
the corresponding JavaScript and declaration files.
|
||||
|
||||
## Compatibility
|
||||
|
||||
The SDK package declares `nocobase.supportedDefaultTemplateRange`. Projects
|
||||
preserve their inherited base template version as
|
||||
`nocobase.defaultTemplateVersion`, even when they use a custom package name and
|
||||
version. `portal-sdk check`, the SDK install script, and the Vite plugin enforce
|
||||
the same compatibility decision.
|
||||
|
||||
See [MIGRATION.md](./MIGRATION.md) for the breaking-change version policy and
|
||||
the coordinated template, Registry, and application upgrade process.
|
||||
|
||||
## Ownership boundary
|
||||
|
||||
The SDK intentionally excludes shadcn components, Tailwind styling, App Shell,
|
||||
branding, business routes and pages, login UI, visual Registry components,
|
||||
application locale configuration, and translation content. Those remain source
|
||||
assets owned by each Portal project.
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"name": "@nocobase/portal-sdk",
|
||||
"version": "1.0.0",
|
||||
"description": "Stable runtime and extension APIs for NocoBase Portal applications.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist",
|
||||
"scripts",
|
||||
"README.md",
|
||||
"MIGRATION.md"
|
||||
],
|
||||
"exports": {
|
||||
"./compat": {
|
||||
"types": "./src/compat/index.ts",
|
||||
"import": "./src/compat/index.ts"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./src/runtime/index.ts",
|
||||
"import": "./src/runtime/index.ts"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./src/client/index.ts",
|
||||
"import": "./src/client/index.ts"
|
||||
},
|
||||
"./auth": {
|
||||
"types": "./src/auth/index.ts",
|
||||
"import": "./src/auth/index.ts"
|
||||
},
|
||||
"./data": {
|
||||
"types": "./src/data/index.ts",
|
||||
"import": "./src/data/index.ts"
|
||||
},
|
||||
"./routing": {
|
||||
"types": "./src/routing/index.ts",
|
||||
"import": "./src/routing/index.ts"
|
||||
},
|
||||
"./extensions": {
|
||||
"types": "./src/extensions/index.ts",
|
||||
"import": "./src/extensions/index.ts"
|
||||
},
|
||||
"./i18n": {
|
||||
"types": "./src/i18n/index.ts",
|
||||
"import": "./src/i18n/index.ts"
|
||||
},
|
||||
"./system-settings": {
|
||||
"types": "./src/system-settings/index.ts",
|
||||
"import": "./src/system-settings/index.ts"
|
||||
},
|
||||
"./vite": {
|
||||
"types": "./src/vite/index.ts",
|
||||
"import": "./src/vite/index.ts"
|
||||
},
|
||||
"./acl": {
|
||||
"types": "./src/acl/index.ts",
|
||||
"import": "./src/acl/index.ts"
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"portal-sdk": "./scripts/cli.mjs"
|
||||
},
|
||||
"nocobase": {
|
||||
"supportedDefaultTemplateRange": ">=2.0.0 <3.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"exports": {
|
||||
"./compat": {
|
||||
"types": "./dist/compat/index.d.ts",
|
||||
"import": "./dist/compat/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./dist/runtime/index.d.ts",
|
||||
"import": "./dist/runtime/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./dist/client/index.d.ts",
|
||||
"import": "./dist/client/index.js"
|
||||
},
|
||||
"./auth": {
|
||||
"types": "./dist/auth/index.d.ts",
|
||||
"import": "./dist/auth/index.js"
|
||||
},
|
||||
"./data": {
|
||||
"types": "./dist/data/index.d.ts",
|
||||
"import": "./dist/data/index.js"
|
||||
},
|
||||
"./routing": {
|
||||
"types": "./dist/routing/index.d.ts",
|
||||
"import": "./dist/routing/index.js"
|
||||
},
|
||||
"./extensions": {
|
||||
"types": "./dist/extensions/index.d.ts",
|
||||
"import": "./dist/extensions/index.js"
|
||||
},
|
||||
"./i18n": {
|
||||
"types": "./dist/i18n/index.d.ts",
|
||||
"import": "./dist/i18n/index.js"
|
||||
},
|
||||
"./system-settings": {
|
||||
"types": "./dist/system-settings/index.d.ts",
|
||||
"import": "./dist/system-settings/index.js"
|
||||
},
|
||||
"./vite": {
|
||||
"types": "./dist/vite/index.d.ts",
|
||||
"import": "./dist/vite/index.js"
|
||||
},
|
||||
"./acl": {
|
||||
"types": "./dist/acl/index.d.ts",
|
||||
"import": "./dist/acl/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ./scripts/clean-dist.mjs && tsc -p tsconfig.json",
|
||||
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
||||
"preinstall": "node ./scripts/check-template-compatibility.mjs",
|
||||
"prepack": "pnpm build",
|
||||
"pretest": "pnpm build",
|
||||
"test": "node --test ./tests/*.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"ignore": "^7.0.6",
|
||||
"i18next": "^25.3.2",
|
||||
"semver": "^7.8.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@refinedev/core": "^5.0.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-router": "^7.0.0",
|
||||
"vite": ">=6.0.0 <8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/semver": "^7.7.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import semver from "semver";
|
||||
|
||||
const sdkRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
".."
|
||||
);
|
||||
|
||||
const readPackage = (directory) => {
|
||||
const packagePath = path.join(directory, "package.json");
|
||||
if (!fs.existsSync(packagePath)) return undefined;
|
||||
try {
|
||||
return {
|
||||
directory,
|
||||
path: packagePath,
|
||||
value: JSON.parse(fs.readFileSync(packagePath, "utf8")),
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectPackage = () => {
|
||||
const starts = [
|
||||
process.env.INIT_CWD,
|
||||
process.env.npm_config_local_prefix,
|
||||
process.cwd(),
|
||||
].filter(Boolean);
|
||||
const visited = new Set();
|
||||
let fallback;
|
||||
|
||||
for (const start of starts) {
|
||||
let directory = path.resolve(start);
|
||||
while (!visited.has(directory)) {
|
||||
visited.add(directory);
|
||||
const candidate = readPackage(directory);
|
||||
if (candidate && candidate.directory !== sdkRoot) {
|
||||
fallback ??= candidate;
|
||||
const pkg = candidate.value;
|
||||
if (
|
||||
pkg?.nocobase?.defaultTemplateVersion ||
|
||||
pkg?.name === "@nocobase/portal-template-default"
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
const parent = path.dirname(directory);
|
||||
if (parent === directory) break;
|
||||
directory = parent;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const checkTemplateCompatibility = ({ silent = false } = {}) => {
|
||||
const sdkPackage = readPackage(sdkRoot)?.value;
|
||||
const projectPackage = findProjectPackage();
|
||||
const supportedRange =
|
||||
sdkPackage?.nocobase?.supportedDefaultTemplateRange;
|
||||
const project = projectPackage?.value;
|
||||
const templateVersion =
|
||||
project?.nocobase?.defaultTemplateVersion ??
|
||||
(project?.name === "@nocobase/portal-template-default"
|
||||
? project.version
|
||||
: undefined);
|
||||
|
||||
if (!supportedRange) {
|
||||
throw new Error(
|
||||
"@nocobase/portal-sdk does not declare nocobase.supportedDefaultTemplateRange."
|
||||
);
|
||||
}
|
||||
if (!templateVersion) {
|
||||
throw new Error(
|
||||
[
|
||||
"Unable to determine the NocoBase Default Template version.",
|
||||
`Project: ${projectPackage?.path ?? "not found"}`,
|
||||
"Add nocobase.defaultTemplateVersion to the project package.json before installing the SDK.",
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
if (!semver.valid(templateVersion)) {
|
||||
throw new Error(
|
||||
`Invalid nocobase.defaultTemplateVersion: ${templateVersion}`
|
||||
);
|
||||
}
|
||||
if (!semver.validRange(supportedRange)) {
|
||||
throw new Error(
|
||||
`Invalid SDK supportedDefaultTemplateRange: ${supportedRange}`
|
||||
);
|
||||
}
|
||||
|
||||
const compatible = semver.satisfies(templateVersion, supportedRange);
|
||||
if (!compatible) {
|
||||
throw new Error(
|
||||
[
|
||||
"Incompatible NocoBase Portal SDK.",
|
||||
`Current Default Template: ${templateVersion}`,
|
||||
`Installing Portal SDK: ${sdkPackage.version}`,
|
||||
`Supported Default Template range: ${supportedRange}`,
|
||||
"Upgrade the base template first, or install a compatible @nocobase/portal-sdk version.",
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
process.stdout.write(
|
||||
`Portal SDK ${sdkPackage.version} supports Default Template ${templateVersion} (${supportedRange}).\n`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
compatible,
|
||||
defaultTemplateVersion: templateVersion,
|
||||
supportedDefaultTemplateRange: supportedRange,
|
||||
};
|
||||
};
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
try {
|
||||
checkTemplateCompatibility();
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const sdkRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
".."
|
||||
);
|
||||
|
||||
fs.rmSync(path.join(sdkRoot, "dist"), { recursive: true, force: true });
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { checkTemplateCompatibility } from "./check-template-compatibility.mjs";
|
||||
|
||||
const command = process.argv[2] ?? "check";
|
||||
|
||||
if (command !== "check") {
|
||||
process.stderr.write("Usage: portal-sdk check\n");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
try {
|
||||
checkTemplateCompatibility();
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,11 @@ import type { AccessControlProvider } from "@refinedev/core";
|
||||
|
||||
import {
|
||||
aclStore,
|
||||
evaluateAccess,
|
||||
getAclState,
|
||||
loadAcl,
|
||||
type AclAccessRequest,
|
||||
type RoleConstraint,
|
||||
} from "@/lib/nocobase/acl";
|
||||
} from "./store.ts";
|
||||
import { evaluateAccess } from "./evaluator.ts";
|
||||
import type { AclAccessRequest, RoleConstraint } from "./types.ts";
|
||||
|
||||
const toAclAccessRequest = ({
|
||||
resource,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createContext, useContext, useSyncExternalStore } from "react";
|
||||
|
||||
import type { RecordPermissionResolver } from "./evaluator";
|
||||
import type { AclState } from "./types";
|
||||
import type { RecordPermissionResolver } from "./evaluator.ts";
|
||||
import type { AclState } from "./types.ts";
|
||||
|
||||
export type AclStore = {
|
||||
getState: () => AclState;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ResourceAcl } from "./types";
|
||||
import type { ResourceAcl } from "./types.ts";
|
||||
|
||||
type DataSourceMeta = {
|
||||
dataSourceKey?: unknown;
|
||||
@@ -1,7 +1,7 @@
|
||||
import ignore from "ignore";
|
||||
|
||||
import { resolveAclDataSourceKey } from "./data-source";
|
||||
import { getRecordActionPermission } from "./record-permissions";
|
||||
import { resolveAclDataSourceKey } from "./data-source.ts";
|
||||
import { getRecordActionPermission } from "./record-permissions.ts";
|
||||
import type {
|
||||
AclAccessRequest,
|
||||
AclActionParams,
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
AclResourcePermissions,
|
||||
ResourceAcl,
|
||||
RoleConstraint,
|
||||
} from "./types";
|
||||
} from "./types.ts";
|
||||
|
||||
const REFINE_ACTION_MAP: Record<string, string> = {
|
||||
list: "list",
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import { useAclState, useAclStore } from "./context";
|
||||
import { evaluateAccess } from "./evaluator";
|
||||
import type { AclAccessRequest } from "./types";
|
||||
import { useAclState, useAclStore } from "./context.ts";
|
||||
import { evaluateAccess } from "./evaluator.ts";
|
||||
import type { AclAccessRequest } from "./types.ts";
|
||||
|
||||
export const useAclEvaluator = () => {
|
||||
const store = useAclStore();
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from "./access-control.ts";
|
||||
export * from "./context.ts";
|
||||
export * from "./data-source.ts";
|
||||
export * from "./evaluator.ts";
|
||||
export * from "./hooks.ts";
|
||||
export * from "./menu.ts";
|
||||
export * from "./record-permissions.ts";
|
||||
export * from "./store.ts";
|
||||
export * from "./store-provider.tsx";
|
||||
export * from "./types.ts";
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TreeMenuItem } from "@refinedev/core";
|
||||
|
||||
import { evaluateAccess, matchesRoleConstraint } from "./evaluator";
|
||||
import type { AclPermissionSet, RouteAccessConstraint } from "./types";
|
||||
import { evaluateAccess, matchesRoleConstraint } from "./evaluator.ts";
|
||||
import type { AclPermissionSet, RouteAccessConstraint } from "./types.ts";
|
||||
|
||||
const matchesRouteAccess = (
|
||||
item: TreeMenuItem,
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { AclStoreContext, type AclStore } from "./context";
|
||||
import { AclStoreContext, type AclStore } from "./context.ts";
|
||||
|
||||
export function AclStoreProvider({
|
||||
children,
|
||||
@@ -1,13 +1,12 @@
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { NocoBaseHttpError } from "@/lib/nocobase/error";
|
||||
import type { AclStore } from "./context";
|
||||
import { nocobaseClient, NocoBaseHttpError } from "../client/index.ts";
|
||||
import type { AclStore } from "./context.ts";
|
||||
import {
|
||||
clearRecordPermissions,
|
||||
getRecordActionPermission,
|
||||
getRecordPermissions,
|
||||
subscribeRecordPermissions,
|
||||
} from "./record-permissions";
|
||||
import type { AclPermissionSet, AclResponse, AclState } from "./types";
|
||||
} from "./record-permissions.ts";
|
||||
import type { AclPermissionSet, AclResponse, AclState } from "./types.ts";
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
let activeRequest:
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clearAcl } from "@/lib/nocobase/acl";
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { clearAcl } from "../acl/index.ts";
|
||||
import { nocobaseClient } from "../client/index.ts";
|
||||
|
||||
export function captureAuthenticationCallback() {
|
||||
if (typeof window === "undefined") return false;
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./callback.ts";
|
||||
export * from "./provider.ts";
|
||||
export * from "./types.ts";
|
||||
export * from "./use-authenticator-sign-in.ts";
|
||||
export * from "./use-public-authenticators.ts";
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { AuthProvider } from "@refinedev/core";
|
||||
|
||||
import { nocobaseClient } from "@/lib/nocobase/client";
|
||||
import { nocobaseClient } from "../client/index.ts";
|
||||
import {
|
||||
clearAcl,
|
||||
loadAcl,
|
||||
type Role,
|
||||
} from "@/lib/nocobase/acl";
|
||||
} from "../acl/index.ts";
|
||||
import {
|
||||
getNocoBaseErrorMessage,
|
||||
NocoBaseHttpError,
|
||||
} from "@/lib/nocobase/error";
|
||||
} from "../client/index.ts";
|
||||
|
||||
type NocoBaseUser = {
|
||||
id: number | string;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user