@truedat/dd 4.39.1 → 4.40.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.40.1] 2022-03-08
4
+
5
+ ### Changed
6
+
7
+ - [TD-4604] Use actions from `/api/data_structures/search` instead of fetching
8
+ permissions
9
+
3
10
  ## [4.39.1] 2022-03-04
4
11
 
5
12
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "4.39.1",
3
+ "version": "4.40.1",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -84,9 +84,9 @@
84
84
  ]
85
85
  },
86
86
  "dependencies": {
87
- "@truedat/auth": "4.39.0",
88
- "@truedat/core": "4.39.0",
89
- "@truedat/df": "4.39.0",
87
+ "@truedat/auth": "4.40.1",
88
+ "@truedat/core": "4.40.1",
89
+ "@truedat/df": "4.40.1",
90
90
  "lodash": "^4.17.21",
91
91
  "path-to-regexp": "^1.7.0",
92
92
  "prop-types": "^15.7.2",
@@ -107,5 +107,5 @@
107
107
  "react-dom": ">= 16.8.6 < 17",
108
108
  "semantic-ui-react": ">= 0.88.2 < 2.1"
109
109
  },
110
- "gitHead": "834628cb0737cb7c874fce2bce891d23fe80e1d7"
110
+ "gitHead": "58b35b48833fbed1602d1070853e6c805bcc48f1"
111
111
  }
@@ -35,7 +35,7 @@ const isValid = _.conforms({
35
35
  domain_id: _.isFinite,
36
36
  });
37
37
 
38
- const actions = (handleSubmit, canAutoPublish) => {
38
+ const actions = (handleSubmit, autoPublish) => {
39
39
  const standardActions = [
40
40
  {
41
41
  key: "no",
@@ -52,7 +52,7 @@ const actions = (handleSubmit, canAutoPublish) => {
52
52
  },
53
53
  ];
54
54
 
55
- return canAutoPublish
55
+ return autoPublish
56
56
  ? [
57
57
  ...standardActions,
58
58
  {
@@ -74,6 +74,7 @@ const isEmptyNumberOrObject = (value) =>
74
74
  export class StructuresBulkUpdate extends React.Component {
75
75
  static propTypes = {
76
76
  applyTemplate: PropTypes.func,
77
+ autoPublish: PropTypes.object,
77
78
  bulkUpdateStructures: PropTypes.func,
78
79
  loading: PropTypes.bool,
79
80
  selectTemplate: PropTypes.func,
@@ -81,7 +82,7 @@ export class StructuresBulkUpdate extends React.Component {
81
82
  template: PropTypes.object,
82
83
  templateId: PropTypes.number,
83
84
  templatesLoaded: PropTypes.bool,
84
- canAutoPublish: PropTypes.bool,
85
+ enabled: PropTypes.bool,
85
86
  };
86
87
 
87
88
  state = initialState;
@@ -123,13 +124,13 @@ export class StructuresBulkUpdate extends React.Component {
123
124
 
124
125
  render() {
125
126
  const {
127
+ autoPublish,
128
+ enabled,
126
129
  loading,
130
+ structureCount,
127
131
  template,
128
132
  templateId,
129
- structureCount,
130
133
  templatesLoaded,
131
- canAutoPublish,
132
- structureActiveFilters,
133
134
  } = this.props;
134
135
  const { content } = this.state;
135
136
  const disableAction = !template || _.isEmpty(template);
@@ -137,9 +138,7 @@ export class StructuresBulkUpdate extends React.Component {
137
138
  return templatesLoaded ? (
138
139
  <Container as={Segment} text>
139
140
  <TemplateLoader />
140
- {!_.isEmpty(template) &&
141
- structureCount != 0 &&
142
- _.has("type.raw")(structureActiveFilters) ? (
141
+ {enabled ? (
143
142
  <>
144
143
  <Header as="h2">
145
144
  <Icon name="book" />
@@ -169,7 +168,7 @@ export class StructuresBulkUpdate extends React.Component {
169
168
  />
170
169
  )}
171
170
  <ConfirmModal
172
- actions={actions(this.handleSubmit, canAutoPublish)}
171
+ actions={actions(this.handleSubmit, autoPublish)}
173
172
  trigger={
174
173
  <Button
175
174
  primary
@@ -236,8 +235,7 @@ const mapStateToProps = ({
236
235
  structures,
237
236
  structureTypes,
238
237
  structureTypesLoading,
239
- userDomains,
240
- structureActiveFilters,
238
+ structuresActions,
241
239
  }) => {
242
240
  const structure = _.head(structures);
243
241
  const templateId = findTemplate(structureTypes, templates, structure);
@@ -249,20 +247,12 @@ const mapStateToProps = ({
249
247
  structureCount,
250
248
  structureTypesLoaded: !structureTypesLoading,
251
249
  templatesLoaded: !templatesLoading && !_.isEmpty(templates),
252
- userDomains,
253
- canAutoPublish: _.any(
254
- ({ domains, permission }) =>
255
- !_.isEmpty(domains) &&
256
- ["publish_structure_note_from_draft"].includes(permission)
257
- )(userDomains),
258
- structureActiveFilters,
250
+ autoPublish: structuresActions?.autoPublish,
251
+ enabled: !!templateId && !!structuresActions?.bulkUpdate,
259
252
  };
260
253
  };
261
254
 
262
255
  export default compose(
263
256
  withRouter,
264
- connect(mapStateToProps, {
265
- bulkUpdateStructures,
266
- selectTemplate,
267
- })
257
+ connect(mapStateToProps, { bulkUpdateStructures, selectTemplate })
268
258
  )(StructuresBulkUpdate);
@@ -8,9 +8,9 @@ import StructuresUpdateOption from "./StructuresUpdateOption";
8
8
  import StructuresUploadOption from "./StructuresUploadOption";
9
9
 
10
10
  export const StructuresOptions = ({
11
+ autoPublish,
12
+ bulkUpload,
11
13
  hasFilterApplied,
12
- canBulkUpload,
13
- canAutoPublish,
14
14
  }) => (
15
15
  <Dropdown
16
16
  icon="ellipsis vertical"
@@ -23,9 +23,9 @@ export const StructuresOptions = ({
23
23
  <Dropdown.Divider />
24
24
  <StructuresDownloadOption hasFilterApplied={hasFilterApplied} />
25
25
  <StructuresEditableDownloadOption hasFilterApplied={hasFilterApplied} />
26
- {canBulkUpload && (
27
- <StructuresUploadOption canAutoPublish={canAutoPublish} />
28
- )}
26
+ {bulkUpload ? (
27
+ <StructuresUploadOption canAutoPublish={!!autoPublish} />
28
+ ) : null}
29
29
  <Dropdown.Divider />
30
30
  <StructuresUpdateOption />
31
31
  </Dropdown.Menu>
@@ -33,9 +33,9 @@ export const StructuresOptions = ({
33
33
  );
34
34
 
35
35
  StructuresOptions.propTypes = {
36
+ autoPublish: PropTypes.object,
37
+ bulkUpload: PropTypes.object,
36
38
  hasFilterApplied: PropTypes.bool,
37
- canBulkUpload: PropTypes.bool,
38
- canAutoPublish: PropTypes.bool,
39
39
  };
40
40
 
41
41
  export default StructuresOptions;
@@ -31,10 +31,6 @@ const RuleImplementationsLoader = React.lazy(() =>
31
31
  import("@truedat/dq/components/RuleImplementationsLoader")
32
32
  );
33
33
 
34
- const UserDomainsLoader = React.lazy(() =>
35
- import("@truedat/auth/users/components/UserDomainsLoader")
36
- );
37
-
38
34
  const ConceptSelectorConceptsLoader = () => (
39
35
  <ConceptsLoader
40
36
  pageSize={7}
@@ -72,13 +68,6 @@ const AuthorizedRoutes = () => {
72
68
  render={() => (
73
69
  <>
74
70
  <UserSearchFiltersLoader scope="data_structure" />
75
- <UserDomainsLoader
76
- permissions={[
77
- "publish_structure_note_from_draft",
78
- "edit_structure_note",
79
- "create_structure_note",
80
- ]}
81
- />
82
71
  <StructuresView />
83
72
  </>
84
73
  )}
@@ -88,13 +77,6 @@ const AuthorizedRoutes = () => {
88
77
  path={STRUCTURES_BULK_UPDATE}
89
78
  render={() => (
90
79
  <>
91
- <UserDomainsLoader
92
- permissions={[
93
- "publish_structure_note_from_draft",
94
- "edit_structure_note",
95
- "create_structure_note",
96
- ]}
97
- />
98
80
  <StructureCrumbs
99
81
  name={formatMessage({ id: "structures.bulkUpdate.crumbs.top" })}
100
82
  />
@@ -27,32 +27,18 @@ export const StructuresHeader = () => (
27
27
  );
28
28
 
29
29
  export const StructuresView = ({
30
+ actions = {},
30
31
  loading,
31
32
  structures,
32
33
  hasFilterApplied,
33
34
  systemsLoading,
34
- userDomains,
35
35
  }) => {
36
36
  const history = useHistory();
37
- const canBulkUpload = _.any(
38
- ({ domains, permission }) =>
39
- !_.isEmpty(domains) &&
40
- ["edit_structure_note", "create_structure_note"].includes(permission)
41
- )(userDomains);
42
- const canAutoPublish = _.any(
43
- ({ domains, permission }) =>
44
- !_.isEmpty(domains) &&
45
- ["publish_structure_note_from_draft"].includes(permission)
46
- )(userDomains);
47
37
  return (
48
38
  <Segment loading={systemsLoading}>
49
39
  <StructuresHeader />
50
40
  <Divider hidden />
51
- <StructuresOptions
52
- hasFilterApplied={hasFilterApplied}
53
- canBulkUpload={canBulkUpload}
54
- canAutoPublish={canAutoPublish}
55
- />
41
+ <StructuresOptions hasFilterApplied={hasFilterApplied} {...actions} />
56
42
  <StructuresSearch />
57
43
  <StructureSelectedFilters />
58
44
  <StructureDateFilter />
@@ -70,11 +56,11 @@ export const StructuresView = ({
70
56
  };
71
57
 
72
58
  StructuresView.propTypes = {
59
+ actions: PropTypes.object,
73
60
  structures: PropTypes.array,
74
61
  loading: PropTypes.bool,
75
62
  systemsLoading: PropTypes.bool,
76
63
  hasFilterApplied: PropTypes.bool,
77
- userDomains: PropTypes.array,
78
64
  };
79
65
 
80
66
  const selectedFilters = ({ structureActiveFilters, structureDateFilter }) =>
@@ -87,11 +73,11 @@ const isEmptyPath = (state, path) => _.flow(_.path(path), _.isEmpty)(state);
87
73
 
88
74
  const mapStateToProps = (state) => ({
89
75
  structures: structureRowsSelector(state),
76
+ actions: state.structuresActions,
90
77
  loading: state.structuresLoading,
91
78
  systemsLoading: state.systemsLoading,
92
79
  hasFilterApplied:
93
80
  !isEmptyPath(state, "structureQuery.query") || selectedFilters(state),
94
- userDomains: state.userDomains,
95
81
  });
96
82
 
97
83
  export default connect(mapStateToProps)(StructuresView);
@@ -17,7 +17,7 @@ describe("<StructuresBulkUpdate />", () => {
17
17
  templatesLoaded: true,
18
18
  structureTypes: [{ name: "doc", template_id: 1 }],
19
19
  structureTypesLoaded: true,
20
- structureActiveFilters: { "type.raw": "booo" },
20
+ enabled: true,
21
21
  };
22
22
 
23
23
  it("matches the latest snapshot", () => {
@@ -137,7 +137,7 @@ describe("<StructuresBulkUpdate />", () => {
137
137
  selectedDomain: { id: 1 },
138
138
  templatesLoaded: true,
139
139
  bulkUpdateStructures: jest.fn(),
140
- structureActiveFilters: { "type.raw": "booo" },
140
+ enabled: true,
141
141
  };
142
142
  const wrapper = shallow(<StructuresBulkUpdate {...props} />);
143
143
  wrapper.setState({
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { waitFor } from "@testing-library/react";
3
+ import { render } from "@truedat/test/render";
4
+ import coreEn from "@truedat/core/messages/en";
5
+ import StructuresOptions from "../StructuresOptions";
6
+ import en from "../../messages/en";
7
+
8
+ const renderOpts = {
9
+ messages: { en: { ...coreEn, ...en } },
10
+ };
11
+
12
+ describe("<StructuresOptions />", () => {
13
+ const props = { bulkUpload: {}, bulkUpdate: {}, autoPublish: {} };
14
+
15
+ it("matches the latest snapshot", async () => {
16
+ const { container } = render(<StructuresOptions {...props} />, renderOpts);
17
+ await waitFor(() => expect(container).toMatchSnapshot());
18
+ });
19
+ });
@@ -31,13 +31,11 @@ describe("<StructuresView />", () => {
31
31
  { name: "group" },
32
32
  ];
33
33
 
34
- const userDomains = [
35
- {
36
- domains: [1, 2],
37
- permission: "edit_structure_note",
38
- },
39
- ];
40
- const props = { structures, columns, userDomains };
34
+ const actions = {
35
+ bulkUpdate: {},
36
+ };
37
+
38
+ const props = { structures, columns, actions };
41
39
  const wrapper = shallow(<StructuresView {...props} />);
42
40
  expect(wrapper).toMatchSnapshot();
43
41
 
@@ -0,0 +1,77 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<StructuresOptions /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ aria-expanded="false"
7
+ class="ui floating dropdown button icon group-actions button-update"
8
+ role="listbox"
9
+ tabindex="0"
10
+ >
11
+ <i
12
+ aria-hidden="true"
13
+ class="ellipsis vertical icon"
14
+ />
15
+ <div
16
+ class="menu transition left"
17
+ >
18
+ <div
19
+ class="divider"
20
+ />
21
+ <div
22
+ aria-disabled="true"
23
+ class="disabled item"
24
+ role="option"
25
+ >
26
+ <i
27
+ aria-hidden="true"
28
+ class="download icon"
29
+ />
30
+ <span>
31
+ Download structures metadata
32
+ </span>
33
+ <p
34
+ class="menu-item-description"
35
+ >
36
+ You must search or filter to download structures
37
+ </p>
38
+ </div>
39
+ <div
40
+ aria-disabled="true"
41
+ class="disabled item"
42
+ role="option"
43
+ >
44
+ <i
45
+ aria-hidden="true"
46
+ class="download icon"
47
+ />
48
+ <span>
49
+ Download editable structures metadata
50
+ </span>
51
+ <p
52
+ class="menu-item-description"
53
+ >
54
+ You must search or filter to download structures
55
+ </p>
56
+ </div>
57
+ <div
58
+ class="item"
59
+ role="option"
60
+ >
61
+ <i
62
+ aria-hidden="true"
63
+ class="upload icon"
64
+ />
65
+ <span
66
+ class="text"
67
+ >
68
+ Upload structure notes
69
+ </span>
70
+ </div>
71
+ <div
72
+ class="divider"
73
+ />
74
+ </div>
75
+ </div>
76
+ </div>
77
+ `;
@@ -7,8 +7,7 @@ exports[`<StructuresView /> matches the latest snapshot 1`] = `
7
7
  hidden={true}
8
8
  />
9
9
  <StructuresOptions
10
- canAutoPublish={false}
11
- canBulkUpload={true}
10
+ bulkUpdate={Object {}}
12
11
  />
13
12
  <Connect(StructuresSearch) />
14
13
  <injectIntl(Connect(SelectedFilters)) />
@@ -353,7 +353,7 @@ export default {
353
353
  "structures.actions.editableDownload.tooltip":
354
354
  "Download editable structures metadata",
355
355
  "structures.actions.download.popup":
356
- "You should select some filter or doing a search",
356
+ "You must search or filter to download structures",
357
357
  "structures.actions.download.tooltip": "Download structures metadata",
358
358
  "structures.actions.edit_system": "Edit system",
359
359
  "structures.actions.new_system": "New system",
@@ -0,0 +1,38 @@
1
+ import { clearStructures, fetchStructures } from "../../routines";
2
+ import { structuresActions, initialState } from "../structuresActions";
3
+
4
+ const fooState = { foo: "bar" };
5
+
6
+ describe("reducers: structuresActions", () => {
7
+ it("should provide the initial state", () => {
8
+ expect(structuresActions(undefined, {})).toBe(initialState);
9
+ });
10
+
11
+ it("should handle the clearStructures.TRIGGER action", () => {
12
+ expect(structuresActions(fooState, { type: clearStructures.TRIGGER })).toBe(
13
+ initialState
14
+ );
15
+ });
16
+
17
+ it("should handle the fetchStructures.TRIGGER action", () => {
18
+ expect(structuresActions(fooState, { type: fetchStructures.TRIGGER })).toBe(
19
+ initialState
20
+ );
21
+ });
22
+
23
+ it("should handle the fetchStructures.SUCCESS action", () => {
24
+ const _actions = { bulkUpdate: {} };
25
+ const data = { _actions };
26
+
27
+ expect(
28
+ structuresActions(fooState, {
29
+ type: fetchStructures.SUCCESS,
30
+ payload: { data },
31
+ })
32
+ ).toMatchObject(_actions);
33
+ });
34
+
35
+ it("should ignore unknown actions", () => {
36
+ expect(structuresActions(fooState, { type: "FOO" })).toBe(fooState);
37
+ });
38
+ });
@@ -13,9 +13,11 @@ import { grantLoading } from "./grantLoading";
13
13
  import { grantQuery } from "./grantQuery";
14
14
  import { grantRequest } from "./grantRequest";
15
15
  import { grantRequests } from "./grantRequests";
16
+ import { grantRequestsCart } from "./grantRequestsCart";
16
17
  import { grantRequestsLoading } from "./grantRequestsLoading";
17
- import { grants } from "./grants";
18
+ import { grantRequestsQuery } from "./grantRequestsQuery";
18
19
  import { grantSelectedFilter } from "./grantSelectedFilter";
20
+ import { grants } from "./grants";
19
21
  import { grantsPageSize } from "./grantsPageSize";
20
22
  import { graph } from "./graph";
21
23
  import { graphLoading } from "./graphLoading";
@@ -37,6 +39,7 @@ import { profileGroupLoading } from "./profileGroupLoading";
37
39
  import { selectedGroups } from "./selectedGroups";
38
40
  import { selectedNode } from "./selectedNode";
39
41
  import { selectedResources } from "./selectedResources";
42
+ import { selectedUserSearchFilter } from "./selectedUserSearchFilter";
40
43
  import { structure } from "./structure";
41
44
  import { structureActions } from "./structureActions";
42
45
  import { structureActiveFilters } from "./structureActiveFilters";
@@ -60,9 +63,7 @@ import { structureProfile } from "./structureProfile";
60
63
  import { structureQuery } from "./structureQuery";
61
64
  import { structureRedirect } from "./structureRedirect";
62
65
  import { structureSelectedFilter } from "./structureSelectedFilter";
63
- import { selectedUserSearchFilter } from "./selectedUserSearchFilter";
64
66
  import { structureSiblings } from "./structureSiblings";
65
- import { structuresSiblings } from "./structureSiblings";
66
67
  import { structureSystem } from "./structureSystem";
67
68
  import { structureTagCreating, structureTag } from "./structureTag";
68
69
  import { structureTagLoading } from "./structureTagLoading";
@@ -73,25 +74,25 @@ import { structureTypeLoading } from "./structureTypeLoading";
73
74
  import { structureTypes } from "./structureTypes";
74
75
  import { structureTypesLoading } from "./structureTypesLoading";
75
76
  import { structureUpdating } from "./structureUpdating";
76
- import { userSearchFilters } from "./userSearchFilters";
77
77
  import { structureVersion } from "./structureVersion";
78
78
  import { structureVersions } from "./structureVersions";
79
79
  import { structures } from "./structures";
80
- import { structuresEditableDownloading } from "./structuresEditableDownloading";
80
+ import { structuresActions } from "./structuresActions";
81
81
  import { structuresDownloading } from "./structuresDownloading";
82
+ import { structuresEditableDownloading } from "./structuresEditableDownloading";
82
83
  import { structuresLoading } from "./structuresLoading";
83
84
  import { structuresPageSize } from "./structuresPageSize";
85
+ import { structuresSiblings } from "./structureSiblings";
84
86
  import { system } from "./system";
85
87
  import { systemId } from "./systemId";
86
88
  import { systemStructures } from "./systemStructures";
87
89
  import { systemStructuresLoading } from "./systemStructuresLoading";
88
90
  import { systems } from "./systems";
89
91
  import { systemsLoading } from "./systemsLoading";
90
- import { uploadingStructuresFile } from "./uploadingStructuresFile";
91
92
  import { unitDomains } from "./unitDomains";
93
+ import { uploadingStructuresFile } from "./uploadingStructuresFile";
92
94
  import { userPermissions } from "./userPermissions";
93
- import { grantRequestsCart } from "./grantRequestsCart";
94
- import { grantRequestsQuery } from "./grantRequestsQuery";
95
+ import { userSearchFilters } from "./userSearchFilters";
95
96
 
96
97
  export {
97
98
  bulkUpdateStructuresLoading,
@@ -107,11 +108,13 @@ export {
107
108
  grantFiltersLoading,
108
109
  grantLoading,
109
110
  grantQuery,
110
- grants,
111
111
  grantRequest,
112
112
  grantRequests,
113
+ grantRequestsCart,
113
114
  grantRequestsLoading,
115
+ grantRequestsQuery,
114
116
  grantSelectedFilter,
117
+ grants,
115
118
  grantsPageSize,
116
119
  graph,
117
120
  graphLoading,
@@ -134,6 +137,7 @@ export {
134
137
  selectedGroups,
135
138
  selectedNode,
136
139
  selectedResources,
140
+ selectedUserSearchFilter,
137
141
  structure,
138
142
  structureActions,
139
143
  structureActiveFilters,
@@ -157,10 +161,8 @@ export {
157
161
  structureQuery,
158
162
  structureRedirect,
159
163
  structureSelectedFilter,
160
- selectedUserSearchFilter,
161
164
  structureSiblings,
162
165
  structureSystem,
163
- structuresSiblings,
164
166
  structureTag,
165
167
  structureTagCreating,
166
168
  structureTagLoading,
@@ -171,24 +173,24 @@ export {
171
173
  structureTypes,
172
174
  structureTypesLoading,
173
175
  structureUpdating,
174
- userSearchFilters,
175
176
  structureVersion,
176
177
  structureVersions,
177
178
  structures,
178
- structuresEditableDownloading,
179
+ structuresActions,
179
180
  structuresDownloading,
181
+ structuresEditableDownloading,
180
182
  structuresFields,
181
183
  structuresLoading,
182
184
  structuresPageSize,
185
+ structuresSiblings,
183
186
  system,
184
187
  systemId,
185
188
  systemStructures,
186
189
  systemStructuresLoading,
187
190
  systems,
188
191
  systemsLoading,
189
- uploadingStructuresFile,
190
192
  unitDomains,
193
+ uploadingStructuresFile,
191
194
  userPermissions,
192
- grantRequestsCart,
193
- grantRequestsQuery,
195
+ userSearchFilters,
194
196
  };
@@ -0,0 +1,18 @@
1
+ import { clearStructures, fetchStructures } from "../routines";
2
+
3
+ export const initialState = null;
4
+
5
+ const structuresActions = (state = initialState, { type, payload }) => {
6
+ switch (type) {
7
+ case clearStructures.TRIGGER:
8
+ return initialState;
9
+ case fetchStructures.TRIGGER:
10
+ return initialState;
11
+ case fetchStructures.SUCCESS:
12
+ return payload?.data?._actions || initialState;
13
+ default:
14
+ return state;
15
+ }
16
+ };
17
+
18
+ export { structuresActions };
@@ -34,7 +34,6 @@ export const defaultStructureColumns = [
34
34
  name: "updated_at",
35
35
  sort: { name: "updated_at" },
36
36
  fieldDecorator: dateDecorator,
37
- textAlign: "center",
38
37
  width: 1,
39
38
  },
40
39
  ];