@byline/admin 3.16.0 → 3.17.0

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.
@@ -6,11 +6,18 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { ArrayField as ArrayFieldType } from '@byline/core';
9
- export declare const ArrayField: ({ field, defaultValue, path, disableSorting, contentLocale, }: {
9
+ export declare const ArrayField: ({ field, defaultValue, path, disableSorting, collectionPath, contentLocale, }: {
10
10
  field: ArrayFieldType;
11
11
  defaultValue: any;
12
12
  path: string;
13
13
  disableSorting?: boolean;
14
+ /**
15
+ * Collection path forwarded to upload-capable fields (`file` / `image`)
16
+ * nested inside an array item, which need it to reach the `/upload`
17
+ * endpoint. Without it those fields fall back to their empty placeholder
18
+ * and never render an upload widget.
19
+ */
20
+ collectionPath?: string;
14
21
  /**
15
22
  * Active content locale, forwarded to each array item's fields so
16
23
  * localized widgets nested inside an array (e.g. a `localized` richText)
@@ -8,7 +8,7 @@ import { FieldRenderer } from "../field-renderer.js";
8
8
  import { SortableItem } from "../sortable-item.js";
9
9
  import { useFormContext } from "../../forms/form-context.js";
10
10
  import array_field_module from "./array-field.module.js";
11
- const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale })=>{
11
+ const ArrayField = ({ field, defaultValue, path, disableSorting = false, collectionPath, contentLocale })=>{
12
12
  const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext();
13
13
  const { t } = useTranslation('byline-admin');
14
14
  const [items, setItems] = useState([]);
@@ -118,6 +118,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
118
118
  defaultValue: groupData[innerField.name],
119
119
  basePath: `${arrayElementPath}.${childField.name}`,
120
120
  disableSorting: true,
121
+ collectionPath: collectionPath,
121
122
  contentLocale: contentLocale
122
123
  }, innerField.name))
123
124
  ]
@@ -128,6 +129,7 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
128
129
  defaultValue: initial,
129
130
  basePath: arrayElementPath,
130
131
  disableSorting: true,
132
+ collectionPath: collectionPath,
131
133
  contentLocale: contentLocale
132
134
  }, childField.name);
133
135
  });
@@ -33,5 +33,5 @@ interface FieldRendererProps {
33
33
  */
34
34
  editor?: RichTextEditorComponent;
35
35
  }
36
- export declare const FieldRenderer: ({ field, defaultValue, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, }: FieldRendererProps) => import("react").JSX.Element | null;
36
+ export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, }: FieldRendererProps) => import("react").JSX.Element | null;
37
37
  export {};
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { getClientConfig } from "@byline/core";
3
3
  import classnames from "classnames";
4
+ import { useFormContext } from "../forms/form-context.js";
4
5
  import { ArrayField } from "./array/array-field.js";
5
6
  import { BlocksField } from "./blocks/blocks-field.js";
6
7
  import { CheckboxField } from "./checkbox/checkbox-field.js";
@@ -17,14 +18,20 @@ import { SelectField } from "./select/select-field.js";
17
18
  import { TextField } from "./text/text-field.js";
18
19
  import { TextAreaField } from "./text-area/text-area-field.js";
19
20
  import { useFieldChangeHandler } from "./use-field-change-handler.js";
20
- const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor })=>{
21
+ import { useFieldCondition } from "./use-field-condition.js";
22
+ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor })=>{
21
23
  const path = basePath ? `${basePath}.${field.name}` : field.name;
22
24
  const htmlId = path.replace(/[[\].]/g, '-');
23
25
  const handleChange = useFieldChangeHandler(field, path);
26
+ const { getFieldValue } = useFormContext();
27
+ const visible = useFieldCondition(field, basePath);
28
+ const storedValue = field.condition ? getFieldValue(path) : void 0;
29
+ const defaultValue = void 0 !== storedValue ? storedValue : initialDefault;
24
30
  const isLocalised = true === field.localized;
25
31
  const badge = isLocalised && contentLocale && !hideLabel ? /*#__PURE__*/ jsx(LocaleBadge, {
26
32
  locale: contentLocale
27
33
  }) : null;
34
+ if (!visible) return null;
28
35
  const renderField = ()=>{
29
36
  switch(field.type){
30
37
  case 'text':
@@ -180,6 +187,7 @@ const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabe
180
187
  } : field,
181
188
  defaultValue: defaultValue,
182
189
  path: path,
190
+ collectionPath: collectionPath,
183
191
  contentLocale: contentLocale
184
192
  });
185
193
  case 'blocks':
@@ -197,6 +205,7 @@ const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabe
197
205
  defaultValue: defaultValue,
198
206
  path: path,
199
207
  disableSorting: disableSorting,
208
+ collectionPath: collectionPath,
200
209
  contentLocale: contentLocale
201
210
  });
202
211
  default:
@@ -10,6 +10,12 @@ interface GroupFieldProps {
10
10
  field: GroupFieldType;
11
11
  defaultValue: any;
12
12
  path: string;
13
+ /**
14
+ * Collection path forwarded to upload-capable child fields (`file` / `image`),
15
+ * which need it to reach the `/upload` endpoint. Without it those fields fall
16
+ * back to their empty placeholder and never render an upload widget.
17
+ */
18
+ collectionPath?: string;
13
19
  /**
14
20
  * Active content locale, forwarded to child fields so localized widgets
15
21
  * nested inside the group (e.g. a `localized` richText) can render their
@@ -17,5 +23,5 @@ interface GroupFieldProps {
17
23
  */
18
24
  contentLocale?: string;
19
25
  }
20
- export declare const GroupField: ({ field, defaultValue, path, contentLocale }: GroupFieldProps) => import("react").JSX.Element;
26
+ export declare const GroupField: ({ field, defaultValue, path, collectionPath, contentLocale, }: GroupFieldProps) => import("react").JSX.Element;
21
27
  export {};
@@ -6,7 +6,7 @@ import { placeholderForField } from "../field-helpers.js";
6
6
  import { FieldRenderer } from "../field-renderer.js";
7
7
  import { useFieldError } from "../../forms/form-context.js";
8
8
  import group_field_module from "./group-field.module.js";
9
- const GroupField = ({ field, defaultValue, path, contentLocale })=>{
9
+ const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale })=>{
10
10
  const fieldError = useFieldError(field.name);
11
11
  const groupData = useMemo(()=>{
12
12
  if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
@@ -47,6 +47,7 @@ const GroupField = ({ field, defaultValue, path, contentLocale })=>{
47
47
  defaultValue: groupData[innerField.name],
48
48
  basePath: path,
49
49
  disableSorting: true,
50
+ collectionPath: collectionPath,
50
51
  contentLocale: contentLocale
51
52
  }, innerField.name))
52
53
  }),
@@ -15,7 +15,8 @@ function useFieldChangeHandler(field, path) {
15
15
  data: getFieldValues(),
16
16
  path,
17
17
  field,
18
- operation: 'change'
18
+ operation: 'change',
19
+ setFieldValue
19
20
  };
20
21
  clearFieldError(path);
21
22
  if (0 === changeFns.length) setFieldValue(path, value);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { Field } from '@byline/core';
9
+ /**
10
+ * Evaluate a field's visibility `condition` against live form data.
11
+ *
12
+ * Subscribes to the form's meta listeners — fired on every field commit, the
13
+ * same loop `TabDefinition.condition` rides — and recomputes the predicate.
14
+ * The consumer only re-renders when the boolean actually flips, so a stable
15
+ * condition costs one function call per form edit, no reconciliation.
16
+ *
17
+ * `basePath` is the field's sibling scope: the enclosing group / array item
18
+ * (e.g. `files[0].filesGroup` for `files[0].filesGroup.thumbnailPage`).
19
+ * Root-level fields pass no basePath and receive the full form data as their
20
+ * sibling scope, mirroring `FieldCondition`'s contract.
21
+ *
22
+ * Fields without a condition are always visible and no subscription is made.
23
+ */
24
+ export declare const useFieldCondition: (field: Field, basePath?: string) => boolean;
@@ -0,0 +1,28 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { useFormContext } from "../forms/form-context.js";
3
+ import { get } from "../forms/nested-path.js";
4
+ const useFieldCondition = (field, basePath)=>{
5
+ const { getFieldValues, subscribeMeta } = useFormContext();
6
+ const evaluate = useCallback(()=>{
7
+ if (!field.condition) return true;
8
+ const data = getFieldValues();
9
+ const siblingData = basePath ? get(data, basePath) ?? {} : data;
10
+ return Boolean(field.condition(data, siblingData));
11
+ }, [
12
+ field,
13
+ basePath,
14
+ getFieldValues
15
+ ]);
16
+ const [visible, setVisible] = useState(evaluate);
17
+ useEffect(()=>{
18
+ if (!field.condition) return;
19
+ setVisible(evaluate());
20
+ return subscribeMeta(()=>setVisible(evaluate()));
21
+ }, [
22
+ field.condition,
23
+ subscribeMeta,
24
+ evaluate
25
+ ]);
26
+ return visible;
27
+ };
28
+ export { useFieldCondition };
@@ -144,10 +144,6 @@ const FormProvider = ({ children, initialData = {} })=>{
144
144
  ];
145
145
  dirtyFields.current.add('__patch__');
146
146
  notifyMetaListeners();
147
- if ('production' !== process.env.NODE_ENV) console.debug('FormContext.appendPatch', {
148
- patch,
149
- dirtyCount: dirtyFields.current.size
150
- });
151
147
  }, [
152
148
  notifyMetaListeners
153
149
  ]);
@@ -248,6 +244,7 @@ const FormProvider = ({ children, initialData = {} })=>{
248
244
  const formErrors = [];
249
245
  const data = getFieldValues();
250
246
  for (const field of fields){
247
+ if (field.condition && !field.condition(data, data)) continue;
251
248
  const value = getFieldValue(field.name);
252
249
  if (!field.optional && (null == value || '' === value)) formErrors.push({
253
250
  field: field.name,
@@ -331,6 +328,7 @@ const FormProvider = ({ children, initialData = {} })=>{
331
328
  for (const field of fields){
332
329
  const fns = normalizeHooks(field.hooks?.beforeValidate);
333
330
  if (0 === fns.length) continue;
331
+ if (field.condition && !field.condition(data, data)) continue;
334
332
  const path = field.name;
335
333
  const value = getFieldValue(path);
336
334
  const ctx = {
@@ -339,7 +337,8 @@ const FormProvider = ({ children, initialData = {} })=>{
339
337
  data,
340
338
  path,
341
339
  field,
342
- operation: 'submit'
340
+ operation: 'submit',
341
+ setFieldValue
343
342
  };
344
343
  try {
345
344
  for (const fn of fns){
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.16.0",
5
+ "version": "3.17.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -155,10 +155,10 @@
155
155
  "react-diff-viewer-continued": "^4.2.2",
156
156
  "uuid": "^14.0.0",
157
157
  "zod": "^4.4.3",
158
- "@byline/auth": "3.16.0",
159
- "@byline/ui": "3.16.0",
160
- "@byline/core": "3.16.0",
161
- "@byline/i18n": "3.16.0"
158
+ "@byline/auth": "3.17.0",
159
+ "@byline/i18n": "3.17.0",
160
+ "@byline/core": "3.17.0",
161
+ "@byline/ui": "3.17.0"
162
162
  },
163
163
  "peerDependencies": {
164
164
  "react": "^19.0.0",
@@ -29,12 +29,20 @@ export const ArrayField = ({
29
29
  defaultValue,
30
30
  path,
31
31
  disableSorting = false,
32
+ collectionPath,
32
33
  contentLocale,
33
34
  }: {
34
35
  field: ArrayFieldType
35
36
  defaultValue: any
36
37
  path: string
37
38
  disableSorting?: boolean
39
+ /**
40
+ * Collection path forwarded to upload-capable fields (`file` / `image`)
41
+ * nested inside an array item, which need it to reach the `/upload`
42
+ * endpoint. Without it those fields fall back to their empty placeholder
43
+ * and never render an upload widget.
44
+ */
45
+ collectionPath?: string
38
46
  /**
39
47
  * Active content locale, forwarded to each array item's fields so
40
48
  * localized widgets nested inside an array (e.g. a `localized` richText)
@@ -205,6 +213,7 @@ export const ArrayField = ({
205
213
  defaultValue={groupData[innerField.name]}
206
214
  basePath={`${arrayElementPath}.${childField.name}`}
207
215
  disableSorting={true}
216
+ collectionPath={collectionPath}
208
217
  contentLocale={contentLocale}
209
218
  />
210
219
  ))}
@@ -219,6 +228,7 @@ export const ArrayField = ({
219
228
  defaultValue={initial}
220
229
  basePath={arrayElementPath}
221
230
  disableSorting={true}
231
+ collectionPath={collectionPath}
222
232
  contentLocale={contentLocale}
223
233
  />
224
234
  )
@@ -17,6 +17,7 @@ import type {
17
17
  import { getClientConfig } from '@byline/core'
18
18
  import cx from 'classnames'
19
19
 
20
+ import { useFormContext } from '../forms/form-context'
20
21
  import { ArrayField } from './array/array-field'
21
22
  import { BlocksField } from './blocks/blocks-field'
22
23
  import { CheckboxField } from './checkbox/checkbox-field'
@@ -33,6 +34,7 @@ import { SelectField } from './select/select-field'
33
34
  import { TextField } from './text/text-field'
34
35
  import { TextAreaField } from './text-area/text-area-field'
35
36
  import { useFieldChangeHandler } from './use-field-change-handler'
37
+ import { useFieldCondition } from './use-field-condition'
36
38
 
37
39
  // ---------------------------------------------------------------------------
38
40
  // FieldRenderer — the main field type switch. Delegates to the appropriate
@@ -69,7 +71,7 @@ interface FieldRendererProps {
69
71
 
70
72
  export const FieldRenderer = ({
71
73
  field,
72
- defaultValue,
74
+ defaultValue: initialDefault,
73
75
  basePath,
74
76
  disableSorting,
75
77
  hideLabel,
@@ -82,6 +84,20 @@ export const FieldRenderer = ({
82
84
  const htmlId = path.replace(/[[\].]/g, '-')
83
85
 
84
86
  const handleChange = useFieldChangeHandler(field, path)
87
+ const { getFieldValue } = useFormContext()
88
+
89
+ // Conditional visibility (BaseField.condition) — re-evaluated on every form
90
+ // edit; the field unmounts while its condition is false.
91
+ const visible = useFieldCondition(field, basePath)
92
+
93
+ // Conditional fields unmount while hidden, so on re-show the uncontrolled
94
+ // widget must be re-seeded from the live form store (which survives the
95
+ // unmount) rather than the initial document data — otherwise an edit made
96
+ // before hiding would be visually reverted while the store (and the patch
97
+ // stream) still carried it. `undefined` means the path was never written,
98
+ // in which case the initial default stands.
99
+ const storedValue = field.condition ? getFieldValue(path) : undefined
100
+ const defaultValue = storedValue !== undefined ? storedValue : initialDefault
85
101
 
86
102
  // When a locale is active and the field is localised, inject a badge into
87
103
  // the field label so the editor knows they are editing locale-specific content.
@@ -90,6 +106,10 @@ export const FieldRenderer = ({
90
106
  const badge =
91
107
  isLocalised && contentLocale && !hideLabel ? <LocaleBadge locale={contentLocale} /> : null
92
108
 
109
+ // All hooks have run by this point, so a conditional bail-out is safe.
110
+ // Hiding retains the field's stored value — no clearing patch is emitted.
111
+ if (!visible) return null
112
+
93
113
  /**
94
114
  * Render the underlying field widget. If the field is localised, we wrap it
95
115
  * so we can append the locale badge after the label.
@@ -254,6 +274,7 @@ export const FieldRenderer = ({
254
274
  }
255
275
  defaultValue={defaultValue}
256
276
  path={path}
277
+ collectionPath={collectionPath}
257
278
  contentLocale={contentLocale}
258
279
  />
259
280
  )
@@ -275,6 +296,7 @@ export const FieldRenderer = ({
275
296
  defaultValue={defaultValue}
276
297
  path={path}
277
298
  disableSorting={disableSorting}
299
+ collectionPath={collectionPath}
278
300
  contentLocale={contentLocale}
279
301
  />
280
302
  )
@@ -32,6 +32,12 @@ interface GroupFieldProps {
32
32
  field: GroupFieldType
33
33
  defaultValue: any
34
34
  path: string
35
+ /**
36
+ * Collection path forwarded to upload-capable child fields (`file` / `image`),
37
+ * which need it to reach the `/upload` endpoint. Without it those fields fall
38
+ * back to their empty placeholder and never render an upload widget.
39
+ */
40
+ collectionPath?: string
35
41
  /**
36
42
  * Active content locale, forwarded to child fields so localized widgets
37
43
  * nested inside the group (e.g. a `localized` richText) can render their
@@ -40,7 +46,13 @@ interface GroupFieldProps {
40
46
  contentLocale?: string
41
47
  }
42
48
 
43
- export const GroupField = ({ field, defaultValue, path, contentLocale }: GroupFieldProps) => {
49
+ export const GroupField = ({
50
+ field,
51
+ defaultValue,
52
+ path,
53
+ collectionPath,
54
+ contentLocale,
55
+ }: GroupFieldProps) => {
44
56
  const fieldError = useFieldError(field.name)
45
57
  // Default value for a group field is a plain object: { rating: 5, comment: '...' }
46
58
  // Normalize to a plain object if not already one.
@@ -80,6 +92,7 @@ export const GroupField = ({ field, defaultValue, path, contentLocale }: GroupFi
80
92
  defaultValue={groupData[innerField.name]}
81
93
  basePath={path}
82
94
  disableSorting={true}
95
+ collectionPath={collectionPath}
83
96
  contentLocale={contentLocale}
84
97
  />
85
98
  )
@@ -53,6 +53,10 @@ export function useFieldChangeHandler(field: Field, path: string) {
53
53
  path,
54
54
  field,
55
55
  operation: 'change',
56
+ // Raw store write for cross-field behaviour (e.g. mutual exclusivity
57
+ // across array items). Deliberately does not run the target field's
58
+ // own hooks — see FieldHookContext.setFieldValue.
59
+ setFieldValue,
56
60
  }
57
61
 
58
62
  clearFieldError(path)
@@ -0,0 +1,52 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { useCallback, useEffect, useState } from 'react'
10
+
11
+ import type { Field } from '@byline/core'
12
+
13
+ import { useFormContext } from '../forms/form-context'
14
+ import { get as getNestedValue } from '../forms/nested-path'
15
+
16
+ /**
17
+ * Evaluate a field's visibility `condition` against live form data.
18
+ *
19
+ * Subscribes to the form's meta listeners — fired on every field commit, the
20
+ * same loop `TabDefinition.condition` rides — and recomputes the predicate.
21
+ * The consumer only re-renders when the boolean actually flips, so a stable
22
+ * condition costs one function call per form edit, no reconciliation.
23
+ *
24
+ * `basePath` is the field's sibling scope: the enclosing group / array item
25
+ * (e.g. `files[0].filesGroup` for `files[0].filesGroup.thumbnailPage`).
26
+ * Root-level fields pass no basePath and receive the full form data as their
27
+ * sibling scope, mirroring `FieldCondition`'s contract.
28
+ *
29
+ * Fields without a condition are always visible and no subscription is made.
30
+ */
31
+ export const useFieldCondition = (field: Field, basePath?: string): boolean => {
32
+ const { getFieldValues, subscribeMeta } = useFormContext()
33
+
34
+ const evaluate = useCallback((): boolean => {
35
+ if (!field.condition) return true
36
+ const data = getFieldValues()
37
+ const siblingData = basePath ? (getNestedValue(data, basePath) ?? {}) : data
38
+ return Boolean(field.condition(data, siblingData))
39
+ }, [field, basePath, getFieldValues])
40
+
41
+ const [visible, setVisible] = useState<boolean>(evaluate)
42
+
43
+ useEffect(() => {
44
+ if (!field.condition) return undefined
45
+ // Re-evaluate on (re)subscribe as well as on every subsequent form edit —
46
+ // the store may have changed between the initial render and this effect.
47
+ setVisible(evaluate())
48
+ return subscribeMeta(() => setVisible(evaluate()))
49
+ }, [field.condition, subscribeMeta, evaluate])
50
+
51
+ return visible
52
+ }
@@ -308,10 +308,11 @@ export const FormProvider = ({
308
308
  // for patches that don't correspond to a specific field.set.
309
309
  dirtyFields.current.add('__patch__')
310
310
  notifyMetaListeners()
311
- if (process.env.NODE_ENV !== 'production') {
312
- // eslint-disable-next-line no-console
313
- console.debug('FormContext.appendPatch', { patch, dirtyCount: dirtyFields.current.size })
314
- }
311
+ // Dev-time patch tracing — uncomment when debugging the patch stream.
312
+ // if (process.env.NODE_ENV !== 'production') {
313
+ // // eslint-disable-next-line no-console
314
+ // console.debug('FormContext.appendPatch', { patch, dirtyCount: dirtyFields.current.size })
315
+ // }
315
316
  },
316
317
  [notifyMetaListeners]
317
318
  )
@@ -476,6 +477,12 @@ export const FormProvider = ({
476
477
  const data = getFieldValues()
477
478
 
478
479
  for (const field of fields) {
480
+ // Condition-hidden fields are exempt from client-side validation — a
481
+ // field the editor cannot currently see must not block submit. Only
482
+ // top-level fields flow through this walk, and a root-level field's
483
+ // sibling scope is the form data itself (see FieldCondition).
484
+ if (field.condition && !field.condition(data, data)) continue
485
+
479
486
  const value = getFieldValue(field.name)
480
487
 
481
488
  // Required field validation
@@ -583,6 +590,10 @@ export const FormProvider = ({
583
590
  const fns = normalizeHooks(field.hooks?.beforeValidate)
584
591
  if (fns.length === 0) continue
585
592
 
593
+ // Condition-hidden fields skip submit-time hooks, mirroring their
594
+ // exemption from validateForm below.
595
+ if (field.condition && !field.condition(data, data)) continue
596
+
586
597
  const path = field.name
587
598
  const value = getFieldValue(path)
588
599
 
@@ -593,6 +604,7 @@ export const FormProvider = ({
593
604
  path,
594
605
  field,
595
606
  operation: 'submit',
607
+ setFieldValue,
596
608
  }
597
609
 
598
610
  try {