@truedat/lm 8.1.5 → 8.2.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.
Files changed (26) hide show
  1. package/package.json +4 -4
  2. package/src/api.js +4 -0
  3. package/src/components/ConceptLinkForm.js +111 -0
  4. package/src/components/LinksPagination.js +15 -0
  5. package/src/components/LinksSearch.js +139 -0
  6. package/src/components/QualityControlConcepts.js +156 -0
  7. package/src/components/QualityControlStructures.js +183 -0
  8. package/src/components/RelationTagForm.js +4 -0
  9. package/src/components/StructureLinkForm.js +105 -0
  10. package/src/components/__tests__/ConceptLinkForm.spec.js +252 -0
  11. package/src/components/__tests__/LinksSearch.spec.js +299 -0
  12. package/src/components/__tests__/QualityControlConcepts.spec.js +262 -0
  13. package/src/components/__tests__/QualityControlStructures.spec.js +213 -0
  14. package/src/components/__tests__/StructureLinkForm.spec.js +284 -0
  15. package/src/components/__tests__/__snapshots__/ConceptLinkForm.spec.js.snap +48 -0
  16. package/src/components/__tests__/__snapshots__/LinksSearch.spec.js.snap +29 -0
  17. package/src/components/__tests__/__snapshots__/NewRelationTag.spec.js.snap +13 -0
  18. package/src/components/__tests__/__snapshots__/QualityControlConcepts.spec.js.snap +49 -0
  19. package/src/components/__tests__/__snapshots__/QualityControlStructures.spec.js.snap +49 -0
  20. package/src/components/__tests__/__snapshots__/RelationTagForm.spec.js.snap +13 -0
  21. package/src/components/__tests__/__snapshots__/StructureLinkForm.spec.js.snap +48 -0
  22. package/src/hooks/useRelations.js +53 -0
  23. package/src/messages/en.js +1 -0
  24. package/src/messages/es.js +1 -0
  25. package/src/selectors/index.js +1 -0
  26. package/src/selectors/messages.js +48 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/lm",
3
- "version": "8.1.5",
3
+ "version": "8.2.1",
4
4
  "description": "Truedat Link Manager",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -48,14 +48,14 @@
48
48
  "@testing-library/jest-dom": "^6.6.3",
49
49
  "@testing-library/react": "^16.3.0",
50
50
  "@testing-library/user-event": "^14.6.1",
51
- "@truedat/test": "8.1.5",
51
+ "@truedat/test": "8.2.1",
52
52
  "identity-obj-proxy": "^3.0.0",
53
53
  "jest": "^29.7.0",
54
54
  "redux-saga-test-plan": "^4.0.6"
55
55
  },
56
56
  "dependencies": {
57
57
  "@apollo/client": "^3.13.8",
58
- "axios": "^1.12.0",
58
+ "axios": "^1.13.5",
59
59
  "graphql": "^16.11.0",
60
60
  "is-hotkey": "^0.2.0",
61
61
  "is-url": "^1.2.4",
@@ -82,5 +82,5 @@
82
82
  "semantic-ui-react": "^3.0.0-beta.2",
83
83
  "swr": "^2.3.3"
84
84
  },
85
- "gitHead": "a9bc192d725eff352d0088d29d7b9b7ebea29254"
85
+ "gitHead": "3008f4a5a734dc1f12e9f25a790542b56aed61d2"
86
86
  }
package/src/api.js CHANGED
@@ -2,6 +2,8 @@ const API_RELATION = "/api/relations/:id";
2
2
  const API_RELATIONS = "/api/relations";
3
3
  const API_RELATIONS_GRAPH = "/api/relations/:id/graph";
4
4
  const API_RELATIONS_SEARCH = "/api/relations/search";
5
+ const API_RELATIONS_INDEX_SEARCH = "/api/relations/index_search";
6
+ const API_RELATIONS_FILTERS = "/api/relations/filters";
5
7
  const API_TAG = "/api/tags/:id";
6
8
  const API_TAGS = "/api/tags";
7
9
  const API_TAGS_SEARCH = "/api/tags/search";
@@ -11,6 +13,8 @@ export {
11
13
  API_RELATIONS,
12
14
  API_RELATIONS_GRAPH,
13
15
  API_RELATIONS_SEARCH,
16
+ API_RELATIONS_INDEX_SEARCH,
17
+ API_RELATIONS_FILTERS,
14
18
  API_TAG,
15
19
  API_TAGS,
16
20
  API_TAGS_SEARCH,
@@ -0,0 +1,111 @@
1
+ import _ from "lodash/fp";
2
+ import { lazy, useState, useEffect } from "react";
3
+ import { useSelector, useDispatch } from "react-redux";
4
+ import { useIntl } from "react-intl";
5
+ import { clearSelectedRelationTags } from "../routines";
6
+ import { Divider, Button } from "semantic-ui-react";
7
+ import { HistoryBackButton } from "@truedat/core/components";
8
+ import { useWebContext } from "@truedat/core/webContext";
9
+ import RelationTagsLoader from "./RelationTagsLoader";
10
+ import { useCreateRelation } from "../hooks/useRelations";
11
+ import { useNavigate } from "react-router";
12
+ import { getRelationErrorAlertMessage } from "../selectors/messages";
13
+
14
+ const TagTypeDropdownSelector = lazy(
15
+ () => import("@truedat/lm/components/TagTypeDropdownSelector")
16
+ );
17
+
18
+ const ConceptSelector = lazy(
19
+ () => import("@truedat/bg/concepts/relations/components/ConceptSelector")
20
+ );
21
+
22
+ const filters = {
23
+ current: [true],
24
+ status: ["pending_approval", "draft", "published"],
25
+ };
26
+
27
+ export const ConceptLinkForm = ({
28
+ targetId,
29
+ targetType,
30
+ redirectUrl,
31
+ selectTagOptions,
32
+ }) => {
33
+ const navigate = useNavigate();
34
+ const selectedRelationTags = useSelector(
35
+ (state) => state?.selectedRelationTags
36
+ );
37
+ const tagOptions = useSelector(selectTagOptions);
38
+ const { trigger: createRelation, isMutating: creatingRelation } =
39
+ useCreateRelation();
40
+ const dispatch = useDispatch();
41
+ const [selectedConcept, setSelectedConcept] = useState(null);
42
+ const { formatMessage } = useIntl();
43
+ const { setAlertMessage } = useWebContext();
44
+
45
+ useEffect(() => {
46
+ return () => {
47
+ dispatch(clearSelectedRelationTags.trigger());
48
+ };
49
+ }, [clearSelectedRelationTags, dispatch]);
50
+
51
+ const handleSubmit = () => {
52
+ const link = {
53
+ source_id: selectedConcept?.business_concept_id,
54
+ source_type: "business_concept",
55
+ target_id: targetId,
56
+ target_type: targetType,
57
+ tag_ids: selectedRelationTags ? selectedRelationTags : [],
58
+ };
59
+ createRelation({ relation: link })
60
+ .then(({ data }) => {
61
+ navigate(redirectUrl, {
62
+ state: {
63
+ createdRelation: {
64
+ ...data?.data,
65
+ source_name: selectedConcept?.name,
66
+ source_data: selectedConcept,
67
+ },
68
+ },
69
+ });
70
+ })
71
+ .catch((error) => {
72
+ const alertMessage = getRelationErrorAlertMessage(error, formatMessage);
73
+ if (alertMessage) {
74
+ setAlertMessage(alertMessage);
75
+ }
76
+ });
77
+ };
78
+
79
+ const disabled = !(selectedConcept && selectedRelationTags);
80
+
81
+ return (
82
+ <>
83
+ <Divider hidden />
84
+ <RelationTagsLoader />
85
+ {!_.isEmpty(tagOptions) && (
86
+ <>
87
+ <TagTypeDropdownSelector options={tagOptions} />
88
+ <Divider hidden />
89
+ </>
90
+ )}
91
+ <ConceptSelector
92
+ selectedConcept={selectedConcept}
93
+ handleConceptSelected={setSelectedConcept}
94
+ defaultFilters={filters}
95
+ />
96
+ <Divider hidden />
97
+ <Button.Group>
98
+ <Button
99
+ primary
100
+ loading={creatingRelation}
101
+ content={formatMessage({ id: "actions.create" })}
102
+ disabled={disabled}
103
+ onClick={handleSubmit}
104
+ />
105
+ <HistoryBackButton content={formatMessage({ id: "actions.cancel" })} />
106
+ </Button.Group>
107
+ </>
108
+ );
109
+ };
110
+
111
+ export default ConceptLinkForm;
@@ -0,0 +1,15 @@
1
+ import { Pagination } from "@truedat/core/components";
2
+ import { useSearchContext } from "@truedat/core/search/SearchContext";
3
+
4
+ export default function LinksPagination() {
5
+ const { count, size, page, selectPage } = useSearchContext();
6
+ const totalPages = Math.ceil(count / size);
7
+
8
+ return (
9
+ <Pagination
10
+ totalPages={totalPages}
11
+ activePage={page}
12
+ selectPage={selectPage}
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,139 @@
1
+ import _ from "lodash/fp";
2
+ import {
3
+ SearchContextProvider,
4
+ useSearchContext,
5
+ } from "@truedat/core/search/SearchContext";
6
+ import { useEffect, useRef } from "react";
7
+ import { useNavigate, useLocation } from "react-router";
8
+ import SearchWidget from "@truedat/core/search/SearchWidget";
9
+ import { useRelations, useRelationFilters } from "../hooks/useRelations";
10
+ import { Table, Segment, Header, Icon } from "semantic-ui-react";
11
+ import { FormattedMessage } from "react-intl";
12
+ import { columnDecorator } from "@truedat/core/services";
13
+ import LinksPagination from "./LinksPagination";
14
+
15
+ const LinkRow = ({ link, columns, highlight }) => (
16
+ <Table.Row positive={highlight}>
17
+ {columns.map((column, i) => (
18
+ <Table.Cell
19
+ key={i}
20
+ textAlign={_.prop("textAlign")(column)}
21
+ content={columnDecorator(column)(link)}
22
+ />
23
+ ))}
24
+ </Table.Row>
25
+ );
26
+
27
+ const LinksTable = ({ relations, columns, extraRow }) => {
28
+ const headerRow = columns.map(({ header }, key) => (
29
+ <Table.HeaderCell
30
+ key={key}
31
+ content={header ? <FormattedMessage id={header} /> : null}
32
+ />
33
+ ));
34
+
35
+ const renderBodyRow = (link, i) => (
36
+ <LinkRow
37
+ key={i}
38
+ link={link}
39
+ columns={columns}
40
+ highlight={link.id === extraRow?.id}
41
+ />
42
+ );
43
+
44
+ return (
45
+ <Table
46
+ headerRow={headerRow}
47
+ renderBodyRow={renderBodyRow}
48
+ tableData={relations}
49
+ />
50
+ );
51
+ };
52
+ const LinksSearchContent = ({
53
+ columns,
54
+ extraRow,
55
+ deletedLinkId,
56
+ refetchSearch,
57
+ setRefetchSearch,
58
+ targetType = "business_concept",
59
+ }) => {
60
+ const { searchData, loading, filterParams } = useSearchContext();
61
+ const { trigger: searchRelations } = useRelations();
62
+ const { trigger: searchRelationsFilters } = useRelationFilters();
63
+ const previousParamsRef = useRef(filterParams);
64
+ const navigate = useNavigate();
65
+ const location = useLocation();
66
+ const emptyMessage =
67
+ targetType === "business_concept"
68
+ ? "linked_concepts.empty"
69
+ : "linked_structures.empty";
70
+
71
+ useEffect(() => {
72
+ if (refetchSearch) {
73
+ setRefetchSearch(false);
74
+ searchRelations(filterParams);
75
+ searchRelationsFilters(filterParams);
76
+ }
77
+ }, [refetchSearch, filterParams, searchRelations, searchRelationsFilters]);
78
+
79
+ useEffect(() => {
80
+ if (_.isEmpty(previousParamsRef.current)) {
81
+ previousParamsRef.current = filterParams;
82
+ return;
83
+ }
84
+
85
+ if (extraRow && !_.isEqual(previousParamsRef, filterParams)) {
86
+ navigate(location.pathname, {
87
+ replace: true,
88
+ state: null,
89
+ });
90
+ }
91
+ }, [extraRow, previousParamsRef, location.pathname, filterParams]);
92
+ const data = _.reject(_.propEq("id", deletedLinkId))(searchData?.data || []);
93
+ const relations =
94
+ extraRow && !_.some(_.propEq("id", extraRow.id))(data)
95
+ ? [extraRow, ...data]
96
+ : data;
97
+ return (
98
+ <Segment loading={loading}>
99
+ <SearchWidget />
100
+ {!_.isEmpty(relations) ? (
101
+ <LinksTable
102
+ relations={relations}
103
+ columns={columns}
104
+ extraRow={extraRow}
105
+ />
106
+ ) : (
107
+ <Header as="h4">
108
+ <Icon name="search" />
109
+ <Header.Content>
110
+ <FormattedMessage id={emptyMessage} />
111
+ </Header.Content>
112
+ </Header>
113
+ )}
114
+ <LinksPagination size="small" />
115
+ </Segment>
116
+ );
117
+ };
118
+
119
+ export function LinksSearch(props) {
120
+ const defaultFilters = _.propOr({}, "defaultFilters")(props);
121
+ const useSearch = _.propOr(useRelations, "useSearch")(props);
122
+ const useFilters = _.propOr(useRelationFilters, "useFilters")(props);
123
+ const searchProps = {
124
+ initialSortColumn: _.propOr("source_name.raw", "initialSortColumn")(props),
125
+ initialSortDirection: "ascending",
126
+ useSearch: useSearch,
127
+ useFilters: useFilters,
128
+ pageSize: 10,
129
+ omitFilters: ["current", "domain_ids"],
130
+ };
131
+
132
+ return (
133
+ <SearchContextProvider {...searchProps} defaultFilters={defaultFilters}>
134
+ <LinksSearchContent {...props} />
135
+ </SearchContextProvider>
136
+ );
137
+ }
138
+
139
+ export default LinksSearch;
@@ -0,0 +1,156 @@
1
+ import _ from "lodash/fp";
2
+ import { useState } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { Button, Grid, Label, Segment } from "semantic-ui-react";
5
+ import { FormattedMessage } from "react-intl";
6
+ import { useDeleteRelation } from "../hooks/useRelations";
7
+ import { Link, useLocation } from "react-router";
8
+ import { linkTo } from "@truedat/core/routes";
9
+ import { ConfirmDeleteRelation } from "./ConfirmDeleteRelation";
10
+ import LinksSearch from "./LinksSearch";
11
+
12
+ const mapStatusColor = {
13
+ deprecated: "grey",
14
+ draft: "olive",
15
+ pending_approval: "teal",
16
+ published: "green",
17
+ rejected: "red",
18
+ versioned: "yellow",
19
+ };
20
+
21
+ const ConceptLink = ({ source_id: business_concept_id, source_name: name }) => (
22
+ <Link
23
+ to={linkTo.CONCEPT_LINKS_QUALITY_CONTROLS({
24
+ business_concept_id,
25
+ id: "current",
26
+ })}
27
+ >
28
+ {name}
29
+ </Link>
30
+ );
31
+
32
+ const RelationTag = ({ tag_type }) => {
33
+ if (!tag_type) return null;
34
+ return (
35
+ <FormattedMessage
36
+ id={`relation.tag.${tag_type}`}
37
+ defaultMessage={tag_type}
38
+ />
39
+ );
40
+ };
41
+
42
+ const ConfirmDeleteQualityControlRelation = ({ id, deleteRelation }) => (
43
+ <ConfirmDeleteRelation
44
+ id={id}
45
+ resourceType="quality_control_concept"
46
+ deleteRelation={deleteRelation}
47
+ />
48
+ );
49
+
50
+ const StatusDecorator = ({ status }) => (
51
+ <Label color={mapStatusColor[status]}>
52
+ <FormattedMessage id={`concepts.status.${status}`} />
53
+ </Label>
54
+ );
55
+
56
+ export const QualityControlConcepts = ({ qualityControl, actions }) => {
57
+ const target_id = qualityControl?.id;
58
+ const target_type = "quality_control";
59
+ const source_type = "business_concept";
60
+ const defaultFilters = {
61
+ target_id,
62
+ target_type,
63
+ source_type,
64
+ };
65
+ const [refetchSearch, setRefetchSearch] = useState(false);
66
+ const [deletedRelationId, setDeletedRelationId] = useState(null);
67
+ const { trigger: deleteRelation, isMutating: deleting } = useDeleteRelation();
68
+ const canCreateLink = _.includes("link_to_business_concept")(actions);
69
+ const { state } = useLocation();
70
+ const extraRow = state?.createdRelation;
71
+
72
+ const onDeleteRelation = ({ id }) => {
73
+ deleteRelation(id).then(({ status }) => {
74
+ if (status === 204) {
75
+ setRefetchSearch(true);
76
+ setDeletedRelationId(id);
77
+ }
78
+ });
79
+ };
80
+
81
+ const deleteColumn = canCreateLink
82
+ ? [
83
+ {
84
+ fieldSelector: _.identity,
85
+ fieldDecorator: ({ id }) => (
86
+ <ConfirmDeleteQualityControlRelation
87
+ id={id}
88
+ deleteRelation={onDeleteRelation}
89
+ />
90
+ ),
91
+ },
92
+ ]
93
+ : [];
94
+
95
+ const columns = [
96
+ {
97
+ header: "concepts.props.name",
98
+ fieldSelector: _.identity,
99
+ fieldDecorator: ConceptLink,
100
+ },
101
+ {
102
+ header: "concepts.props.domain",
103
+ fieldSelector: _.path("source_data.domain.name"),
104
+ },
105
+ {
106
+ header: "concepts.props.tags",
107
+ fieldSelector: _.identity,
108
+ fieldDecorator: RelationTag,
109
+ },
110
+ {
111
+ header: "concepts.props.status",
112
+ fieldSelector: _.path("source_data.status"),
113
+ fieldDecorator: (status) => <StatusDecorator status={status} />,
114
+ },
115
+ ...deleteColumn,
116
+ ];
117
+
118
+ return (
119
+ <Segment basic loading={deleting}>
120
+ <Grid>
121
+ <Grid.Column width={16}>
122
+ {canCreateLink && (
123
+ <Button
124
+ floated="right"
125
+ primary
126
+ as={Link}
127
+ to={linkTo.QUALITY_CONTROL_CONCEPT_LINKS_NEW({
128
+ id: qualityControl?.id,
129
+ version: qualityControl?.version,
130
+ })}
131
+ state={null}
132
+ content={<FormattedMessage id="links.actions.create" />}
133
+ />
134
+ )}
135
+ </Grid.Column>
136
+ <Grid.Column width={16}>
137
+ <LinksSearch
138
+ columns={columns}
139
+ extraRow={extraRow}
140
+ deletedLinkId={deletedRelationId}
141
+ defaultFilters={defaultFilters}
142
+ refetchSearch={refetchSearch}
143
+ setRefetchSearch={setRefetchSearch}
144
+ />
145
+ </Grid.Column>
146
+ </Grid>
147
+ </Segment>
148
+ );
149
+ };
150
+
151
+ QualityControlConcepts.propTypes = {
152
+ qualityControl: PropTypes.object,
153
+ actions: PropTypes.array,
154
+ };
155
+
156
+ export default QualityControlConcepts;
@@ -0,0 +1,183 @@
1
+ import _ from "lodash/fp";
2
+ import { useState } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { Button, Grid, Icon, Label, Segment } from "semantic-ui-react";
5
+ import { FormattedMessage, useIntl } from "react-intl";
6
+ import { useDeleteRelation } from "../hooks/useRelations";
7
+ import { Link, useLocation } from "react-router";
8
+ import { linkTo } from "@truedat/core/routes";
9
+ import { useRelations, useRelationFilters } from "../hooks/useRelations";
10
+ import { ConfirmDeleteRelation } from "./ConfirmDeleteRelation";
11
+ import LinksSearch from "./LinksSearch";
12
+ import Moment from "react-moment";
13
+
14
+ const StructureLink = ({ target_id: id, target_name: name }) => (
15
+ <Link to={linkTo.STRUCTURE({ id })} title={name}>
16
+ {name}
17
+ </Link>
18
+ );
19
+
20
+ const ConfirmDeleteQualityControlRelation = ({ id, deleteRelation }) => (
21
+ <ConfirmDeleteRelation
22
+ id={id}
23
+ resourceType="quality_control_structure"
24
+ deleteRelation={deleteRelation}
25
+ />
26
+ );
27
+
28
+ const PathDecorator = (path) => (
29
+ <span title={_.join(" › ")(path)}>
30
+ {_.flow(_.join(" › "), _.truncate({ length: 90 }))(path)}{" "}
31
+ </span>
32
+ );
33
+
34
+ const DateDecorator = (date) => {
35
+ const { locale } = useIntl();
36
+ return (
37
+ date && <Moment locale={locale} date={date} format="YYYY-MM-DD HH:mm" />
38
+ );
39
+ };
40
+
41
+ const DeletedDateDecorator = (date) => {
42
+ const { locale } = useIntl();
43
+ return (
44
+ date && (
45
+ <Label className="alert warning">
46
+ <Icon name="warning circle" color="red" />
47
+ <Moment locale={locale} date={date} format="YYYY-MM-DD HH:mm" />
48
+ </Label>
49
+ )
50
+ );
51
+ };
52
+
53
+ const RelationTag = ({ tag_type }) => {
54
+ if (!tag_type) return null;
55
+ return (
56
+ <FormattedMessage
57
+ id={`relation.tag.${tag_type}`}
58
+ defaultMessage={tag_type}
59
+ />
60
+ );
61
+ };
62
+
63
+ export const QualityControlStructures = ({ qualityControl, actions }) => {
64
+ const source_id = qualityControl?.id;
65
+ const source_type = "quality_control";
66
+ const target_type = "data_structure";
67
+ const defaultFilters = {
68
+ source_id,
69
+ target_type,
70
+ source_type,
71
+ };
72
+ const [refetchSearch, setRefetchSearch] = useState(false);
73
+ const [deletedRelationId, setDeletedRelationId] = useState(null);
74
+ const { trigger: deleteRelation, isMutating: deleting } = useDeleteRelation();
75
+ const canCreateLink = _.includes("link_to_data_structure")(actions);
76
+ const { state } = useLocation();
77
+ const extraRow = state?.createdRelation;
78
+
79
+ const onDeleteRelation = ({ id }) => {
80
+ deleteRelation(id).then(({ status }) => {
81
+ if (status === 204) {
82
+ setRefetchSearch(true);
83
+ setDeletedRelationId(id);
84
+ }
85
+ });
86
+ };
87
+
88
+ const deleteColumn = canCreateLink
89
+ ? [
90
+ {
91
+ fieldSelector: _.identity,
92
+ fieldDecorator: ({ id }) => (
93
+ <ConfirmDeleteQualityControlRelation
94
+ id={id}
95
+ deleteRelation={onDeleteRelation}
96
+ />
97
+ ),
98
+ },
99
+ ]
100
+ : [];
101
+
102
+ const columns = [
103
+ {
104
+ header: "structures.props.name",
105
+ fieldSelector: _.identity,
106
+ fieldDecorator: StructureLink,
107
+ },
108
+ {
109
+ header: "structures.props.domain",
110
+ fieldSelector: _.path("target_data.domain.name"),
111
+ },
112
+ {
113
+ header: "structures.props.system",
114
+ fieldSelector: _.path("target_data.system.name"),
115
+ },
116
+ {
117
+ header: "structures.props.path",
118
+ fieldSelector: _.path("target_data.path"),
119
+ fieldDecorator: PathDecorator,
120
+ },
121
+ {
122
+ header: "structures.props.last_change_at",
123
+ fieldSelector: _.path("target_data.last_change_at"),
124
+ fieldDecorator: DateDecorator,
125
+ width: 1,
126
+ },
127
+ {
128
+ header: "structures.props.relation_tags",
129
+ fieldSelector: _.identity,
130
+ fieldDecorator: RelationTag,
131
+ },
132
+ {
133
+ header: "structures.props.deleted_at",
134
+ fieldSelector: _.path("target_data.deleted_at"),
135
+ fieldDecorator: DeletedDateDecorator,
136
+ width: 1,
137
+ },
138
+ ...deleteColumn,
139
+ ];
140
+
141
+ return (
142
+ <Segment basic loading={deleting}>
143
+ <Grid>
144
+ <Grid.Column width={16}>
145
+ {canCreateLink && (
146
+ <Button
147
+ floated="right"
148
+ primary
149
+ as={Link}
150
+ to={linkTo.QUALITY_CONTROL_STRUCTURE_LINKS_NEW({
151
+ id: qualityControl?.id,
152
+ version: qualityControl?.version,
153
+ })}
154
+ state={null}
155
+ content={<FormattedMessage id="links.actions.create" />}
156
+ />
157
+ )}
158
+ </Grid.Column>
159
+ <Grid.Column width={16}>
160
+ <LinksSearch
161
+ columns={columns}
162
+ extraRow={extraRow}
163
+ deletedLinkId={deletedRelationId}
164
+ defaultFilters={defaultFilters}
165
+ refetchSearch={refetchSearch}
166
+ setRefetchSearch={setRefetchSearch}
167
+ targetType={target_type}
168
+ initialSortColumn="target_name.raw"
169
+ useSearch={() => useRelations({ document: "all" })}
170
+ useFilters={() => useRelationFilters({ document: "all" })}
171
+ />
172
+ </Grid.Column>
173
+ </Grid>
174
+ </Segment>
175
+ );
176
+ };
177
+
178
+ QualityControlStructures.propTypes = {
179
+ qualityControl: PropTypes.object,
180
+ actions: PropTypes.array,
181
+ };
182
+
183
+ export default QualityControlStructures;
@@ -31,6 +31,10 @@ export const RelationTagForm = ({ relationTag, isSubmitting, onSubmit }) => {
31
31
  value: "data_field",
32
32
  text: formatMessage({ id: "target_type.data_field" }),
33
33
  },
34
+ {
35
+ value: "quality_control",
36
+ text: formatMessage({ id: "target_type.quality_control" }),
37
+ },
34
38
  ];
35
39
  const currentTargetType = watch("target_type");
36
40
  return (