@nocobase/client-v2 2.1.17 → 2.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.17",
3
+ "version": "2.1.19",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.17",
31
- "@nocobase/flow-engine": "2.1.17",
32
- "@nocobase/sdk": "2.1.17",
33
- "@nocobase/shared": "2.1.17",
34
- "@nocobase/utils": "2.1.17",
30
+ "@nocobase/evaluators": "2.1.19",
31
+ "@nocobase/flow-engine": "2.1.19",
32
+ "@nocobase/sdk": "2.1.19",
33
+ "@nocobase/shared": "2.1.19",
34
+ "@nocobase/utils": "2.1.19",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -46,5 +46,5 @@
46
46
  "react-i18next": "^11.15.1",
47
47
  "react-router-dom": "^6.30.1"
48
48
  },
49
- "gitHead": "9f8515d8a236f6584ce8a52e6b86082173a02bf4"
49
+ "gitHead": "c316683666cfb478cb355b971c8f067a4f925fcb"
50
50
  }
@@ -14,7 +14,7 @@ import {
14
14
  useFlowModelById,
15
15
  useFlowViewContext,
16
16
  } from '@nocobase/flow-engine';
17
- import type { FlowModel, FlowModelRendererProps, ModelConstructor } from '@nocobase/flow-engine';
17
+ import type { FlowEngineContext, FlowModel, FlowModelRendererProps, ModelConstructor } from '@nocobase/flow-engine';
18
18
  import { useRequest } from 'ahooks';
19
19
  import React from 'react';
20
20
  import FlowRoute from './components/FlowRoute';
@@ -57,10 +57,36 @@ type FlowPageProps = {
57
57
  showFlowSettings?: FlowModelRendererProps['showFlowSettings'];
58
58
  };
59
59
 
60
+ type FlowPageViewContext = FlowEngineContext & {
61
+ view?: {
62
+ inputArgs?: {
63
+ isMobileLayout?: unknown;
64
+ };
65
+ };
66
+ };
67
+
68
+ const bindViewLayoutState = (model: FlowModel, ctx?: FlowPageViewContext | null) => {
69
+ const hasViewMobileLayout = typeof ctx?.view?.inputArgs?.isMobileLayout === 'boolean';
70
+ const hasContextMobileLayout = typeof ctx?.isMobileLayout === 'boolean';
71
+ if (!hasViewMobileLayout && !hasContextMobileLayout) {
72
+ return;
73
+ }
74
+
75
+ model.context.defineProperty('isMobileLayout', {
76
+ get: () => {
77
+ if (typeof ctx?.isMobileLayout === 'boolean') {
78
+ return ctx.isMobileLayout;
79
+ }
80
+ return !!ctx?.view?.inputArgs?.isMobileLayout;
81
+ },
82
+ cache: false,
83
+ });
84
+ };
85
+
60
86
  export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknown>) => {
61
87
  const { pageModelClass = 'ChildPageModel', parentId, onModelLoaded, defaultTabTitle, ...rest } = props;
62
88
  const flowEngine = useFlowEngine();
63
- const ctx = useFlowViewContext();
89
+ const ctx = useFlowViewContext<FlowPageViewContext>();
64
90
  const { loading, data, error } = useRequest(
65
91
  async () => {
66
92
  const ModelClass = await flowEngine.getModelClassAsync(pageModelClass);
@@ -105,6 +131,7 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
105
131
  const data = await flowEngine.loadOrCreateModel(options, { skipSave: !flowEngine.context.flowSettingsEnabled });
106
132
  if (data?.uid && onModelLoaded) {
107
133
  data.context.addDelegate(ctx);
134
+ bindViewLayoutState(data, ctx);
108
135
  data.removeParentDelegate();
109
136
  onModelLoaded(data.uid, data);
110
137
  }
@@ -7,44 +7,105 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { render } from '@testing-library/react';
10
+ import { render, waitFor } from '@testing-library/react';
11
11
  import React from 'react';
12
12
  import { afterEach, describe, expect, it, vi } from 'vitest';
13
13
  import { FlowPage } from '../FlowPage';
14
14
 
15
- const mocks = vi.hoisted(() => ({
16
- rendererProps: undefined as any,
17
- model: {
15
+ type TestContextProperty = {
16
+ value?: unknown;
17
+ get?: () => unknown;
18
+ };
19
+
20
+ type TestModelContext = {
21
+ isMobileLayout: boolean;
22
+ themeToken: {
23
+ marginBlock: number;
24
+ };
25
+ addDelegate: ReturnType<typeof vi.fn>;
26
+ defineProperty: ReturnType<typeof vi.fn>;
27
+ };
28
+
29
+ type TestPageModel = {
30
+ uid: string;
31
+ props: {
32
+ showFlowSettings: boolean;
33
+ };
34
+ context: TestModelContext;
35
+ removeParentDelegate: ReturnType<typeof vi.fn>;
36
+ };
37
+
38
+ type RendererProps = {
39
+ model?: TestPageModel;
40
+ showFlowSettings?: boolean;
41
+ } & Record<string, unknown>;
42
+
43
+ const defineContextProperty = (context: TestModelContext, key: string, property: TestContextProperty) => {
44
+ Object.defineProperty(context, key, {
45
+ configurable: true,
46
+ enumerable: true,
47
+ get: property.get || (() => property.value),
48
+ });
49
+ };
50
+
51
+ const createPageModel = (): TestPageModel => {
52
+ const context = {
53
+ isMobileLayout: false,
54
+ themeToken: {
55
+ marginBlock: 16,
56
+ },
57
+ addDelegate: vi.fn(),
58
+ defineProperty: vi.fn((key: string, property: TestContextProperty) => {
59
+ defineContextProperty(context, key, property);
60
+ }),
61
+ };
62
+
63
+ return {
18
64
  uid: 'public-form-page',
19
65
  props: {
20
66
  showFlowSettings: false,
21
67
  },
22
- context: {
23
- isMobileLayout: false,
24
- themeToken: {
25
- marginBlock: 16,
26
- },
27
- },
28
- },
68
+ context,
69
+ removeParentDelegate: vi.fn(),
70
+ };
71
+ };
72
+
73
+ const mocks = vi.hoisted(() => ({
74
+ rendererMobileStates: [] as boolean[],
75
+ rendererProps: undefined as RendererProps | undefined,
76
+ model: undefined as TestPageModel | undefined,
77
+ viewInputMobileLayout: true as boolean | undefined,
78
+ contextMobileLayout: undefined as boolean | undefined,
29
79
  }));
30
80
 
31
81
  vi.mock('@nocobase/flow-engine', async () => {
32
- const actual = await vi.importActual<any>('@nocobase/flow-engine');
82
+ const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
33
83
  const ReactModule = await import('react');
84
+ class TestPageModelClass {}
34
85
 
35
86
  return {
36
87
  ...actual,
37
- FlowModelRenderer: vi.fn((props: any) => {
88
+ FlowModelRenderer: vi.fn((props: RendererProps) => {
38
89
  mocks.rendererProps = props;
90
+ if (typeof props.model?.context.isMobileLayout === 'boolean') {
91
+ mocks.rendererMobileStates.push(props.model.context.isMobileLayout);
92
+ }
39
93
  return ReactModule.createElement('div', { 'data-testid': 'flow-model-renderer' });
40
94
  }),
41
95
  useFlowEngine: vi.fn(() => ({
42
- getModelClassAsync: vi.fn(),
43
- loadOrCreateModel: vi.fn(),
96
+ getModelClassAsync: vi.fn(async () => TestPageModelClass),
97
+ loadOrCreateModel: vi.fn(async () => mocks.model),
44
98
  context: {},
99
+ translate: vi.fn((value: string) => value),
45
100
  })),
46
101
  useFlowModelById: vi.fn(() => mocks.model),
47
102
  useFlowViewContext: vi.fn(() => ({
103
+ view: {
104
+ inputArgs: {
105
+ ...(typeof mocks.viewInputMobileLayout === 'boolean' ? { isMobileLayout: mocks.viewInputMobileLayout } : {}),
106
+ },
107
+ },
108
+ ...(typeof mocks.contextMobileLayout === 'boolean' ? { isMobileLayout: mocks.contextMobileLayout } : {}),
48
109
  themeToken: {
49
110
  marginBlock: 16,
50
111
  },
@@ -53,27 +114,93 @@ vi.mock('@nocobase/flow-engine', async () => {
53
114
  });
54
115
 
55
116
  vi.mock('ahooks', async () => {
56
- const actual = await vi.importActual<any>('ahooks');
117
+ const actual = await vi.importActual<typeof import('ahooks')>('ahooks');
118
+ const ReactModule = await import('react');
57
119
  return {
58
120
  ...actual,
59
- useRequest: vi.fn(() => ({
60
- loading: false,
61
- data: {
62
- uid: 'public-form-page',
63
- },
64
- error: null,
65
- })),
121
+ useRequest: vi.fn((service: () => Promise<unknown>) => {
122
+ const serviceRef = ReactModule.useRef(service);
123
+ const [state, setState] = ReactModule.useState<{
124
+ loading: boolean;
125
+ data?: unknown;
126
+ error?: unknown;
127
+ }>({
128
+ loading: true,
129
+ });
130
+
131
+ ReactModule.useEffect(() => {
132
+ let active = true;
133
+ serviceRef
134
+ .current()
135
+ .then((data) => {
136
+ if (active) {
137
+ setState({ loading: false, data, error: null });
138
+ }
139
+ })
140
+ .catch((error) => {
141
+ if (active) {
142
+ setState({ loading: false, data: undefined, error });
143
+ }
144
+ });
145
+
146
+ return () => {
147
+ active = false;
148
+ };
149
+ }, []);
150
+
151
+ return state;
152
+ }),
66
153
  };
67
154
  });
68
155
 
69
156
  describe('FlowPage', () => {
70
157
  afterEach(() => {
158
+ mocks.rendererMobileStates = [];
71
159
  mocks.rendererProps = undefined;
160
+ mocks.model = undefined;
161
+ mocks.viewInputMobileLayout = true;
162
+ mocks.contextMobileLayout = undefined;
72
163
  });
73
164
 
74
- it('uses showFlowSettings from page model props', () => {
165
+ it('uses showFlowSettings from page model props', async () => {
166
+ mocks.model = createPageModel();
75
167
  render(<FlowPage />);
76
168
 
77
- expect(mocks.rendererProps?.showFlowSettings).toBe(false);
169
+ await waitFor(() => {
170
+ expect((mocks.rendererProps as { showFlowSettings?: boolean } | undefined)?.showFlowSettings).toBe(false);
171
+ });
172
+ });
173
+
174
+ it('exposes mobile layout state from route-managed view inputArgs before first render', async () => {
175
+ mocks.model = createPageModel();
176
+ render(<FlowPage onModelLoaded={vi.fn()} />);
177
+
178
+ await waitFor(() => {
179
+ expect(mocks.rendererMobileStates.length).toBeGreaterThan(0);
180
+ });
181
+ expect(mocks.rendererMobileStates[0]).toBe(true);
78
182
  });
183
+
184
+ it.each([
185
+ { viewInputMobileLayout: true, contextMobileLayout: false, expected: false },
186
+ { viewInputMobileLayout: false, contextMobileLayout: true, expected: true },
187
+ { viewInputMobileLayout: undefined, contextMobileLayout: true, expected: true },
188
+ { viewInputMobileLayout: undefined, contextMobileLayout: false, expected: false },
189
+ { viewInputMobileLayout: true, contextMobileLayout: undefined, expected: true },
190
+ { viewInputMobileLayout: false, contextMobileLayout: undefined, expected: false },
191
+ ])(
192
+ 'resolves mobile layout state from view input args and layout context %#',
193
+ async ({ viewInputMobileLayout, contextMobileLayout, expected }) => {
194
+ mocks.viewInputMobileLayout = viewInputMobileLayout;
195
+ mocks.contextMobileLayout = contextMobileLayout;
196
+ mocks.model = createPageModel();
197
+
198
+ render(<FlowPage onModelLoaded={vi.fn()} />);
199
+
200
+ await waitFor(() => {
201
+ expect(mocks.rendererMobileStates.length).toBeGreaterThan(0);
202
+ });
203
+ expect(mocks.rendererMobileStates[0]).toBe(expected);
204
+ },
205
+ );
79
206
  });
@@ -64,6 +64,40 @@ describe('normalizeDataScopeFilter', () => {
64
64
  });
65
65
  });
66
66
 
67
+ it('prunes a missing URL search param variable', () => {
68
+ const rawFilter = {
69
+ logic: '$and',
70
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' }],
71
+ };
72
+ const resolvedFilter = {
73
+ logic: '$and',
74
+ items: [{ path: 'departmentId', operator: '$eq', value: undefined }],
75
+ };
76
+
77
+ expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toBeUndefined();
78
+ });
79
+
80
+ it('only prunes the missing URL search param condition from mixed filters', () => {
81
+ const rawFilter = {
82
+ logic: '$and',
83
+ items: [
84
+ { path: 'status', operator: '$eq', value: 'active' },
85
+ { path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' },
86
+ ],
87
+ };
88
+ const resolvedFilter = {
89
+ logic: '$and',
90
+ items: [
91
+ { path: 'status', operator: '$eq', value: 'active' },
92
+ { path: 'departmentId', operator: '$eq', value: undefined },
93
+ ],
94
+ };
95
+
96
+ expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toEqual({
97
+ $and: [{ status: { $eq: 'active' } }],
98
+ });
99
+ });
100
+
67
101
  it('still prunes empty constant values', () => {
68
102
  const filter = {
69
103
  logic: '$and',
@@ -139,6 +173,34 @@ describe('normalizeDataScopeFilter', () => {
139
173
  expect(resource.removeFilterGroup).not.toHaveBeenCalled();
140
174
  });
141
175
 
176
+ it('dataScope handler removes data scope when a URL search param is missing', async () => {
177
+ const resource = {
178
+ addFilterGroup: vi.fn(),
179
+ removeFilterGroup: vi.fn(),
180
+ };
181
+ const ctx = {
182
+ model: {
183
+ uid: 'field-1',
184
+ resource,
185
+ },
186
+ resolveJsonTemplate: vi.fn(async (template) => ({
187
+ ...template,
188
+ items: [{ ...template.items[0], value: undefined }],
189
+ })),
190
+ };
191
+ const params = {
192
+ filter: {
193
+ logic: '$and',
194
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' }],
195
+ },
196
+ };
197
+
198
+ await (dataScope as any).handler(ctx, params);
199
+
200
+ expect(resource.removeFilterGroup).toHaveBeenCalledWith('field-1');
201
+ expect(resource.addFilterGroup).not.toHaveBeenCalled();
202
+ });
203
+
142
204
  it('dataScope handler preserves current role as server-side variable', async () => {
143
205
  const engine = new FlowEngine();
144
206
  const resource = {
@@ -12,6 +12,7 @@ import { describe, it, expect, vi } from 'vitest';
12
12
  import { RUNJS_OPEN_VIEW_ROUTE_STATE } from '@nocobase/flow-engine';
13
13
  import { openView } from '../openView';
14
14
  import { FlowPage } from '../../FlowPage';
15
+ import { ROUTE_TRANSIENT_INPUT_ARGS_KEY } from '../../routeTransientInputArgs';
15
16
 
16
17
  describe('openView action - route mode defineProperties/defineMethods', () => {
17
18
  const createRouteManagedCtx = () => {
@@ -172,6 +173,56 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
172
173
  expect(defineMethodCalls).toContain('pong');
173
174
  });
174
175
 
176
+ it('passes formData through route navigation state', async () => {
177
+ const navigateTo = vi.fn();
178
+ const ctx: any = {
179
+ inputArgs: {
180
+ formData: {
181
+ start: '2026-06-24 08:00:00',
182
+ end: '2026-06-24 09:00:00',
183
+ },
184
+ },
185
+ engine: {
186
+ context: {},
187
+ },
188
+ model: {
189
+ uid: 'popup-uid',
190
+ context: {
191
+ defineProperty: vi.fn(),
192
+ },
193
+ },
194
+ view: {
195
+ navigation: {
196
+ navigateTo,
197
+ },
198
+ },
199
+ isNavigationEnabled: true,
200
+ };
201
+
202
+ await openView.handler(ctx, { navigation: true });
203
+
204
+ expect(navigateTo).toHaveBeenCalledWith(
205
+ {
206
+ viewUid: 'popup-uid',
207
+ filterByTk: undefined,
208
+ sourceId: undefined,
209
+ tabUid: undefined,
210
+ },
211
+ {
212
+ state: {
213
+ [ROUTE_TRANSIENT_INPUT_ARGS_KEY]: {
214
+ 'popup-uid': {
215
+ formData: {
216
+ start: '2026-06-24 08:00:00',
217
+ end: '2026-06-24 09:00:00',
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ );
224
+ });
225
+
175
226
  it('adds route state to first-stage navigation only for RunJS ctx.openView calls', async () => {
176
227
  const { ctx, navigateTo } = createFirstStageCtx({
177
228
  mode: 'dialog',
@@ -181,13 +232,16 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
181
232
 
182
233
  await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
183
234
 
184
- expect(navigateTo).toHaveBeenCalledWith({
185
- viewUid: 'popup-uid',
186
- filterByTk: undefined,
187
- sourceId: undefined,
188
- tabUid: undefined,
189
- openViewRouteState: { mode: 'dialog', size: 'large' },
190
- });
235
+ expect(navigateTo).toHaveBeenCalledWith(
236
+ {
237
+ viewUid: 'popup-uid',
238
+ filterByTk: undefined,
239
+ sourceId: undefined,
240
+ tabUid: undefined,
241
+ openViewRouteState: { mode: 'dialog', size: 'large' },
242
+ },
243
+ undefined,
244
+ );
191
245
  });
192
246
 
193
247
  it('normalizes first-stage RunJS route state mode on mobile layout', async () => {
@@ -200,13 +254,16 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
200
254
 
201
255
  await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
202
256
 
203
- expect(navigateTo).toHaveBeenCalledWith({
204
- viewUid: 'popup-uid',
205
- filterByTk: undefined,
206
- sourceId: undefined,
207
- tabUid: undefined,
208
- openViewRouteState: { mode: 'embed', size: 'large' },
209
- });
257
+ expect(navigateTo).toHaveBeenCalledWith(
258
+ {
259
+ viewUid: 'popup-uid',
260
+ filterByTk: undefined,
261
+ sourceId: undefined,
262
+ tabUid: undefined,
263
+ openViewRouteState: { mode: 'embed', size: 'large' },
264
+ },
265
+ undefined,
266
+ );
210
267
  });
211
268
 
212
269
  it('uses route replay mode and size before persisted openView defaults', async () => {
@@ -246,11 +303,14 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
246
303
 
247
304
  await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
248
305
 
249
- expect(navigateTo).toHaveBeenCalledWith({
250
- viewUid: 'popup-uid',
251
- filterByTk: undefined,
252
- sourceId: undefined,
253
- tabUid: undefined,
254
- });
306
+ expect(navigateTo).toHaveBeenCalledWith(
307
+ {
308
+ viewUid: 'popup-uid',
309
+ filterByTk: undefined,
310
+ sourceId: undefined,
311
+ tabUid: undefined,
312
+ },
313
+ undefined,
314
+ );
255
315
  });
256
316
  });
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
10
+ import { extractPropertyPath, isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
11
11
  import { transformFilter } from '@nocobase/utils/client';
12
12
  import _ from 'lodash';
13
13
 
@@ -45,6 +45,14 @@ function restorePreservedNull(value: any): any {
45
45
  return value;
46
46
  }
47
47
 
48
+ function isUrlSearchParamsExpression(value: any) {
49
+ if (!isVariableExpression(value)) {
50
+ return false;
51
+ }
52
+
53
+ return extractPropertyPath(value)?.[0] === 'urlSearchParams';
54
+ }
55
+
48
56
  function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
49
57
  if (!rawNode || !resolvedNode || typeof rawNode !== 'object' || typeof resolvedNode !== 'object') {
50
58
  return;
@@ -57,6 +65,7 @@ function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
57
65
  }
58
66
  if (
59
67
  isVariableExpression(rawNode.value) &&
68
+ !isUrlSearchParamsExpression(rawNode.value) &&
60
69
  (resolvedNode.value === undefined || resolvedNode.value === null || resolvedNode.value === '')
61
70
  ) {
62
71
  resolvedNode.value = PRESERVE_NULL;
@@ -20,6 +20,7 @@ import React from 'react';
20
20
  import { FlowPage } from '../FlowPage';
21
21
  import { PageModel, RootPageModel } from '../models';
22
22
  import _ from 'lodash';
23
+ import { ROUTE_TRANSIENT_INPUT_ARGS_KEY } from '../routeTransientInputArgs';
23
24
 
24
25
  type DirtyAwareFlowModel = FlowModel & {
25
26
  getUserModifiedFields?: () => Set<string> | undefined;
@@ -31,6 +32,15 @@ type BeforeCloseDirtyState = {
31
32
  formModelUids: string[];
32
33
  };
33
34
 
35
+ const pickRouteTransientInputArgs = (inputArgs: Record<string, unknown>) => {
36
+ return _.pickBy(
37
+ {
38
+ formData: inputArgs.formData,
39
+ },
40
+ (value) => typeof value !== 'undefined',
41
+ );
42
+ };
43
+
34
44
  function collectDirtyFormModelUids(model?: FlowModel | null, ignoredDirtyFormModelUids: string[] = []): string[] {
35
45
  if (!model) {
36
46
  return [];
@@ -363,7 +373,17 @@ export const openView = defineAction({
363
373
  tabUid: mergedTabUid,
364
374
  ...(effectiveRunJSOpenViewRouteState ? { openViewRouteState: effectiveRunJSOpenViewRouteState } : {}),
365
375
  };
366
- ctx.view.navigation.navigateTo(nextView);
376
+ const transientInputArgs = pickRouteTransientInputArgs(inputArgs);
377
+ const navigationOptions = Object.keys(transientInputArgs).length
378
+ ? {
379
+ state: {
380
+ [ROUTE_TRANSIENT_INPUT_ARGS_KEY]: {
381
+ [nextView.viewUid]: transientInputArgs,
382
+ },
383
+ },
384
+ }
385
+ : undefined;
386
+ ctx.view.navigation.navigateTo(nextView, navigationOptions);
367
387
  return;
368
388
  }
369
389
  }