feat(plugin-environment-variables): expose $env on the flow-engine context in v1 too (#9728)

Extract the v2 plugin's `defineProperty('$env')` registration into a
framework-agnostic `registerEnvProperty(context, apiClient, t)` helper, and call
it from the v1 client entry as well (via the allowed v1 -> v2 import). This puts
`$env` on the v1 flow-engine context — the same mechanism v2 already uses — so v2
UI rendered in the v1 runtime (which mounts detached from the v1 React tree and
can't reach the React-context `addGlobalVar('$env')`) can resolve `$env` from
`flowEngine.context.getPropertyMetaTree()`. The list is fetched once and cached by
the flow-engine context. The legacy v1 `addGlobalVar` + provider path is kept
untouched.
This commit is contained in:
PiEgg
2026-06-10 12:03:43 +08:00
committed by GitHub
parent 9d54552771
commit 7be4397cda
3 changed files with 98 additions and 44 deletions
@@ -9,28 +9,7 @@
import type { Application } from '@nocobase/client-v2';
import { Plugin } from '@nocobase/client-v2';
import type { PropertyMeta } from '@nocobase/flow-engine';
type EnvironmentVariable = {
name: string;
value?: string;
type?: 'default' | 'secret';
};
const ENV_VARS_LIST_URL = 'environmentVariables?paginate=false';
async function fetchEnvironmentVariables(apiClient: Application['apiClient']): Promise<EnvironmentVariable[]> {
try {
const response = await apiClient.request({
url: ENV_VARS_LIST_URL,
skipNotify: true,
} as any);
const list = (response as any)?.data?.data;
return Array.isArray(list) ? list : [];
} catch {
return [];
}
}
import { registerEnvProperty } from './registerEnvProperty';
export class PluginEnvironmentVariablesClientV2 extends Plugin<Record<string, never>, Application> {
async load() {
@@ -52,28 +31,9 @@ export class PluginEnvironmentVariablesClientV2 extends Plugin<Record<string, ne
// Expose `$env` to all v2 plugins via FlowContext, replacing v1's
// `addGlobalVar('$env', ...)` + Provider chain. Lazy-loaded — the API is
// only hit on first read, then cached by FlowContext.
this.flowEngine.context.defineProperty('$env', {
get: async () => {
const list = await fetchEnvironmentVariables(this.app.apiClient);
return Object.fromEntries(list.map((item) => [item.name, item.value]));
},
meta: {
type: 'object',
title: this.t('Variables and secrets') as unknown as string,
properties: async (): Promise<Record<string, PropertyMeta>> => {
const list = await fetchEnvironmentVariables(this.app.apiClient);
const out: Record<string, PropertyMeta> = {};
for (const item of list) {
out[item.name] = {
type: item.type === 'secret' ? 'string' : item.type || 'string',
title: item.name,
};
}
return out;
},
},
});
// only hit on first read, then cached by FlowContext. Shared with the v1
// runtime via `registerEnvProperty` (see `./registerEnvProperty`).
registerEnvProperty(this.flowEngine.context, this.app.apiClient, (key) => this.t(key));
}
}
@@ -0,0 +1,84 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
/**
* Shared registration of the `$env` property on a flow-engine context — the
* single source of truth used by BOTH client runtimes:
* - v2 (`client-v2/plugin.tsx`) registers it on its own flow-engine context;
* - v1 (`client/index.tsx`) imports this same helper via the allowed v1 → v2
* direction and registers it on v1's flow-engine context, so workflow's v2
* config drawer (which renders detached from the React tree and therefore
* can't reach v1's React-context-based `addGlobalVar('$env')`) can still read
* `$env` from `flowEngine.context.getPropertyMetaTree()`.
*
* Framework-agnostic: dependencies (`apiClient`, `t`) are injected as parameters,
* so it carries no React hooks and no `@nocobase/client` import — usable from
* either runtime. The list is fetched lazily (first read only) and cached by the
* flow-engine context, so the picker never re-requests on every expand.
*/
import type { FlowContext, PropertyMeta } from '@nocobase/flow-engine';
type EnvironmentVariable = {
name: string;
value?: string;
type?: 'default' | 'secret';
};
/** Minimal structural slice of the API client — only the one call this needs.
* Avoids importing the concrete `APIClient` type across the package boundary. */
type EnvApiClient = {
request(options: { url: string; skipNotify?: boolean }): Promise<unknown>;
};
const ENV_VARS_LIST_URL = 'environmentVariables?paginate=false';
const ENV_ROOT = '$env';
export async function fetchEnvironmentVariables(apiClient: EnvApiClient): Promise<EnvironmentVariable[]> {
try {
const response = await apiClient.request({ url: ENV_VARS_LIST_URL, skipNotify: true });
const list = (response as { data?: { data?: unknown } })?.data?.data;
return Array.isArray(list) ? (list as EnvironmentVariable[]) : [];
} catch {
return [];
}
}
/** A translator for the scope label. Accepts the i18next `t` from either runtime
* (its return type isn't a bare `string` — it's a `TFunctionDetailedResult` /
* union), so the result is coerced to a string by the helper. */
type Translate = (key: string) => unknown;
/**
* Define `$env` on the given flow-engine context. `t` translates the scope label
* ("Variables and secrets"); `apiClient` fetches the variable list lazily.
*/
export function registerEnvProperty(context: FlowContext, apiClient: EnvApiClient, t: Translate): void {
context.defineProperty(ENV_ROOT, {
get: async () => {
const list = await fetchEnvironmentVariables(apiClient);
return Object.fromEntries(list.map((item) => [item.name, item.value]));
},
meta: {
type: 'object',
title: String(t('Variables and secrets')),
properties: async (): Promise<Record<string, PropertyMeta>> => {
const list = await fetchEnvironmentVariables(apiClient);
const out: Record<string, PropertyMeta> = {};
for (const item of list) {
out[item.name] = {
type: item.type === 'secret' ? 'string' : item.type || 'string',
title: item.name,
};
}
return out;
},
},
});
}
@@ -11,6 +11,9 @@ import { Plugin } from '@nocobase/client';
import { EnvironmentVariablesAndSecretsProvider } from './EnvironmentVariablesAndSecretsProvider';
import EnvironmentPage from './components/EnvironmentPage';
import { useGetEnvironmentVariables, useGetEnvironmentVariablesCtx } from './utils';
// Shared `$env` registration, defined once in client-v2 and imported back here
// via the allowed v1 → v2 direction (see the helper's doc comment).
import { registerEnvProperty } from '../client-v2/registerEnvProperty';
export class PluginEnvironmentVariablesClient extends Plugin {
async load() {
@@ -19,8 +22,15 @@ export class PluginEnvironmentVariablesClient extends Plugin {
icon: 'TableOutlined',
Component: EnvironmentPage,
});
// Legacy v1 path: `$env` as a React-context global var, consumed by the v1
// Formily `Variable.Input` scope (`useGlobalVariable('$env')`). Kept as-is.
this.app.addGlobalVar('$env', useGetEnvironmentVariables, useGetEnvironmentVariablesCtx);
this.app.use(EnvironmentVariablesAndSecretsProvider);
// Also expose `$env` on the flow-engine context (the v2 mechanism), so v2 UI
// rendered in the v1 runtime — e.g. workflow's config drawer, which mounts
// detached from the React tree and can't read the React-context global var —
// can still resolve `$env` via `flowEngine.context.getPropertyMetaTree()`.
registerEnvProperty(this.app.flowEngine.context, this.app.apiClient, (key) => this.t(key));
}
}