@k-int/stripes-kint-components 5.10.0 → 5.12.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/es/index.js +12 -0
  3. package/es/lib/EditableRefdataList/EditableRefdataList.js +1 -1
  4. package/es/lib/SASQLookupComponent/SASQLookupComponent.js +38 -30
  5. package/es/lib/Tags/Tags.js +143 -0
  6. package/es/lib/Tags/Tags.test.js +80 -0
  7. package/es/lib/Tags/hooks/index.js +28 -0
  8. package/es/lib/Tags/hooks/useTags.js +15 -0
  9. package/es/lib/Tags/hooks/useTagsEnabled.js +25 -0
  10. package/es/lib/Tags/index.js +40 -0
  11. package/es/lib/Tags/tagsConfig.js +10 -0
  12. package/es/lib/constants/pagination.js +15 -0
  13. package/es/lib/hooks/index.js +7 -0
  14. package/es/lib/hooks/useHelperApp.js +18 -15
  15. package/es/lib/hooks/useLocalPageStore.js +25 -0
  16. package/es/lib/hooks/usePrevNextPagination.js +168 -0
  17. package/package.json +11 -10
  18. package/src/index.js +2 -0
  19. package/src/lib/EditableRefdataList/EditableRefdataList.js +5 -5
  20. package/src/lib/SASQLookupComponent/SASQLookupComponent.js +60 -49
  21. package/src/lib/Tags/Tags.js +145 -0
  22. package/src/lib/Tags/Tags.test.js +77 -0
  23. package/src/lib/Tags/hooks/index.js +2 -0
  24. package/src/lib/Tags/hooks/useTags.js +16 -0
  25. package/src/lib/Tags/hooks/useTagsEnabled.js +19 -0
  26. package/src/lib/Tags/index.js +4 -0
  27. package/src/lib/Tags/tagsConfig.js +16 -0
  28. package/src/lib/constants/pagination.js +10 -0
  29. package/src/lib/hooks/index.js +2 -0
  30. package/src/lib/hooks/useHelperApp.js +21 -13
  31. package/src/lib/hooks/useLocalPageStore.js +18 -0
  32. package/src/lib/hooks/usePrevNextPagination.js +203 -0
  33. package/test/helpers/test-implementor-translations.json +4 -1
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ var _reactRouterDom = require("react-router-dom");
9
+ var _queryString = _interopRequireDefault(require("query-string"));
10
+ var _pagination = require("../constants/pagination");
11
+ var _useLocalPageStore = _interopRequireDefault(require("./useLocalPageStore"));
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
+ // Currently there are several places in which this hook is used twice within the same component
14
+ // Once in order to get the current page, and again in order to handle changes to paginations
15
+ // This hook should be refactored in order to resolve these issue - @EthanFreestone
16
+ const usePrevNextPagination = function () {
17
+ let {
18
+ count = 0,
19
+ // Only needed for reading back MCL props
20
+ defaultToPageOne = true,
21
+ // A prop to allow the implementor to turn off the defaulting to page=1
22
+ pageSize = _pagination.DEFAULT_PAGINATION_SIZE,
23
+ // Only needed for reading back MCL props
24
+ id = _pagination.DEFAULT_PAGE_KEY,
25
+ // This id is ONLY used for syncToLocation: false cases, as a key to the zustand store
26
+ syncToLocation = true,
27
+ // Used to turn on/off location syncing, so can be used as standalone state if required,
28
+ hasNextPage = null // Override for canGoNext, used in the case in which resources are fetched with no stats
29
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
30
+ /* ------ ZUSTAND STORE ------ */
31
+ // For NON-SYNC-TO-LOCATION use cases, store the currentPage in a keyed store
32
+ const pageStore = (0, _useLocalPageStore.default)(state => state.pageStore);
33
+ const setPage = (0, _useLocalPageStore.default)(state => state.setPage);
34
+
35
+ /* ------ CURRENTPAGE STATE ------ */
36
+ // Set up initialValue
37
+ const getInitialCurrentPage = (0, _react.useCallback)(() => {
38
+ let initialCurrentPage;
39
+ if (!syncToLocation) {
40
+ if (pageStore[id]) {
41
+ initialCurrentPage = pageStore[id];
42
+ } else {
43
+ // Initialise store state
44
+ setPage(id, 1);
45
+ initialCurrentPage = 1;
46
+ }
47
+ }
48
+ return initialCurrentPage;
49
+ }, [id, pageStore, setPage, syncToLocation]);
50
+ // State itself
51
+ const [currentPage, setCurrentPage] = (0, _react.useState)(getInitialCurrentPage());
52
+ const location = (0, _reactRouterDom.useLocation)();
53
+ const history = (0, _reactRouterDom.useHistory)();
54
+
55
+ /* ------ HANDLEPAGECHANGE ------ */
56
+ // Takes in a direction "prev" or "next" and performs the requisite logic to move
57
+ // currentPage state and/or zustand store state
58
+ const handlePageChange = (0, _react.useCallback)(direction => {
59
+ const urlQuery = _queryString.default.parse(location.search);
60
+ let newPage;
61
+ if (direction === _pagination.NEXT) {
62
+ newPage = currentPage + 1;
63
+ } else if (direction === _pagination.PREV) {
64
+ newPage = currentPage - 1;
65
+ }
66
+ if (!syncToLocation) {
67
+ // We're manipulating the state directly in this case
68
+ // We're dealing with the zustand store in this case,
69
+ // change the store and the currentPage will update below
70
+ setPage(id, newPage);
71
+ setCurrentPage(newPage);
72
+ } else if (newPage !== urlQuery?.page) {
73
+ const newQuery = {
74
+ ...urlQuery,
75
+ page: newPage
76
+ };
77
+ history.push({
78
+ pathname: location.pathname,
79
+ search: `?${_queryString.default.stringify(newQuery)}`
80
+ });
81
+ }
82
+ }, [currentPage, history, id, location.pathname, location.search, setPage, syncToLocation]);
83
+ const [resetPageState, setResetPageState] = (0, _react.useState)(false);
84
+ const resetPage = (0, _react.useCallback)(() => {
85
+ if (syncToLocation) {
86
+ setResetPageState(true);
87
+ } else {
88
+ setPage(id, 1);
89
+ setCurrentPage(1);
90
+ }
91
+ }, [id, setPage, syncToLocation]);
92
+ (0, _react.useEffect)(() => {
93
+ if (syncToLocation) {
94
+ const urlQuery = _queryString.default.parse(location.search);
95
+ if (urlQuery?.page && currentPage !== urlQuery?.page) {
96
+ setCurrentPage(Number(urlQuery?.page));
97
+ } else if (!urlQuery?.page && defaultToPageOne) {
98
+ // If url query "page" is not yet set, set it to 1
99
+ setCurrentPage(1);
100
+ const newQuery = {
101
+ ...urlQuery,
102
+ page: 1
103
+ };
104
+ history.push({
105
+ pathname: location.pathname,
106
+ search: `?${_queryString.default.stringify(newQuery)}`
107
+ });
108
+ }
109
+ if (resetPageState) {
110
+ const newQuery = {
111
+ ...urlQuery,
112
+ page: 1
113
+ };
114
+ history.push({
115
+ pathname: location.pathname,
116
+ search: `?${_queryString.default.stringify(newQuery)}`
117
+ });
118
+ setResetPageState(false);
119
+ }
120
+ } else if (currentPage !== pageStore[id]) {
121
+ // Only do this when not syncing to location...
122
+ // If current page state is not what we have in the store, set current page state
123
+ setCurrentPage(pageStore[id]);
124
+ }
125
+ }, [currentPage, defaultToPageOne, history, id, location, pageStore, resetPageState, syncToLocation]);
126
+
127
+ // Set up MCL specific props based on page
128
+ const pagingCanGoNext = hasNextPage ?? (currentPage && currentPage < Number(count) / pageSize);
129
+ const pagingCanGoPrevious = currentPage && Number(currentPage) > 1;
130
+ const pagingOffset = currentPage ? (currentPage - 1) * pageSize : 0;
131
+ const onNeedMoreData = function () {
132
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
133
+ args[_key] = arguments[_key];
134
+ }
135
+ if (args[_pagination.MCL_NEED_MORE_DATA_PREV_NEXT_ARG_INDEX]) {
136
+ handlePageChange(args[_pagination.MCL_NEED_MORE_DATA_PREV_NEXT_ARG_INDEX]);
137
+ }
138
+ };
139
+
140
+ // Set up SASQ callback handling
141
+ // If extras are needed, these can be set up
142
+ // manually using resetPage per SASQ
143
+ const queryStateReducer = (0, _react.useCallback)((_currState, nextState) => {
144
+ const resetPageEvents = ['clear.all', 'reset.all', 'filter.state', 'filter.clearGroup', 'sort.change', 'search.reset', 'search.submit'];
145
+ if (resetPageEvents.includes(nextState.changeType)) {
146
+ resetPage();
147
+ }
148
+ return nextState;
149
+ }, [resetPage]);
150
+ const paginationSASQProps = (0, _react.useMemo)(() => ({
151
+ queryStateReducer
152
+ }), [queryStateReducer]);
153
+ return {
154
+ currentPage,
155
+ handlePageChange,
156
+ paginationMCLProps: {
157
+ onNeedMoreData,
158
+ pagingCanGoNext,
159
+ pagingCanGoPrevious,
160
+ pagingOffset,
161
+ pagingType: 'prev-next'
162
+ },
163
+ paginationSASQProps,
164
+ queryStateReducer,
165
+ resetPage
166
+ };
167
+ };
168
+ var _default = exports.default = usePrevNextPagination;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-int/stripes-kint-components",
3
- "version": "5.10.0",
3
+ "version": "5.12.0",
4
4
  "description": "Stripes Component library for K-Int specific applications",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -13,7 +13,8 @@
13
13
  "build:es": "rm -rf ./es && babel --extensions .js --ignore ./src/**/tests/**/* ./src --out-dir es",
14
14
  "build": "yarn build:es",
15
15
  "clean": "rm -rf ./node_modules ./*/node_modules ./yarn.lock && yarn install --ignore-scripts",
16
- "clean-build": "yarn clean && yarn build",
16
+ "clean-install": "yarn clean && yarn install --ignore-scripts",
17
+ "clean-build": "yarn clean-install && yarn build",
17
18
  "prepare": "yarn build",
18
19
  "semantic-release": "semantic-release"
19
20
  },
@@ -39,12 +40,12 @@
39
40
  "@babel/preset-flow": "^7.18.6",
40
41
  "@babel/preset-react": "^7.18.6",
41
42
  "@babel/preset-typescript": "^7.18.6",
42
- "@folio/eslint-config-stripes": "^7.0.0",
43
- "@folio/jest-config-stripes": "^2.0.0",
44
- "@folio/stripes": "^9.0.0",
45
- "@folio/stripes-cli": "^3.0.0",
43
+ "@folio/eslint-config-stripes": "^7.0.0 || ^8.0.0",
44
+ "@folio/jest-config-stripes": "^2.0.0 || ^3.0.0",
45
+ "@folio/stripes": "^9.0.0 || ^10.0.0",
46
+ "@folio/stripes-cli": "^3.0.0 || ^4.0.0",
46
47
  "@folio/stripes-erm-testing": "^2.0.0",
47
- "@formatjs/cli": "^6.1.3",
48
+ "@formatjs/cli": "^6.6.0",
48
49
  "@semantic-release/changelog": "^6.0.3",
49
50
  "@semantic-release/git": "^10.0.1",
50
51
  "@semantic-release/gitlab": "^12.0.6",
@@ -63,7 +64,7 @@
63
64
  "prop-types-extra": ">=1.1.0",
64
65
  "react": "^18.2.0",
65
66
  "react-dom": "^18.2.0",
66
- "react-intl": "^6.4.4",
67
+ "react-intl": "^6.4.4 || ^7.1.5",
67
68
  "react-query": "^3.6.0",
68
69
  "react-redux": "^9.0.0",
69
70
  "react-router": "^5.2.0",
@@ -76,7 +77,7 @@
76
77
  "sinon": "^18.0.0"
77
78
  },
78
79
  "peerDependencies": {
79
- "@folio/stripes": ">=9.0.0",
80
+ "@folio/stripes": "^9.0.0 || ^10.0.0",
80
81
  "final-form": ">=4.18.4",
81
82
  "final-form-arrays": ">=3.0.1",
82
83
  "lodash": ">=4.17.0",
@@ -85,7 +86,7 @@
85
86
  "react": "*",
86
87
  "react-final-form": ">=6.3.0",
87
88
  "react-final-form-arrays": ">=3.1.0",
88
- "react-intl": ">=6.4.4",
89
+ "react-intl": "^6.4.4 || ^7.1.5",
89
90
  "react-query": ">=3.9.0",
90
91
  "react-router-dom": ">=5.2.0"
91
92
  },
package/src/index.js CHANGED
@@ -113,3 +113,5 @@ export { default as SettingsFormContainer } from './lib/SettingsFormContainer';
113
113
  export { default as ComboButton } from './lib/ComboButton';
114
114
 
115
115
  export { default as NumberField } from './lib/NumberField';
116
+
117
+ export * from './lib/Tags';
@@ -106,16 +106,16 @@ const EditableRefdataList = ({
106
106
  // or one provided in labelOverrides, which is passed the error message and refdata in question
107
107
  delete: async (err) => {
108
108
  const errorResp = await parseErrorResponse(err.response);
109
- console.log("ERRORRESP: %o", errorResp);
109
+ // console.log('ERRORRESP: %o', errorResp);
110
110
  callout.sendCallout({
111
111
  message: kintIntl.formatKintMessage({
112
112
  id: 'refdata.deleteRefdataValue.errorMessage',
113
113
  overrideValue: labelOverrides?.deleteError
114
114
  },
115
- {
116
- label: deleteModal?.refdata?.label,
117
- error: errorResp?.message
118
- }),
115
+ {
116
+ label: deleteModal?.refdata?.label,
117
+ error: errorResp?.message
118
+ }),
119
119
  type: 'error',
120
120
  });
121
121
  },
@@ -1,8 +1,8 @@
1
- import { forwardRef, useImperativeHandle } from 'react';
1
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
2
2
 
3
3
  import PropTypes from 'prop-types';
4
4
  import { FormattedMessage } from 'react-intl';
5
- import { useInfiniteQuery } from 'react-query';
5
+ import { useQuery } from 'react-query';
6
6
 
7
7
  import {
8
8
  useNamespace,
@@ -25,7 +25,7 @@ import {
25
25
  } from '@folio/stripes/components';
26
26
 
27
27
  import { generateKiwtQuery } from '../utils';
28
- import { useKintIntl, useKiwtSASQuery, useLocalStorageState } from '../hooks';
28
+ import { useKintIntl, useKiwtSASQuery, useLocalStorageState, usePrevNextPagination } from '../hooks';
29
29
 
30
30
  import TableBody from './TableBody';
31
31
 
@@ -41,6 +41,7 @@ const SASQLookupComponent = forwardRef((props, ref) => {
41
41
  intlNS: passedIntlNS,
42
42
  labelOverrides = {},
43
43
  mainPaneProps,
44
+ mclProps = {},
44
45
  noSearchField,
45
46
  persistedPanesetProps = {},
46
47
  RenderBody,
@@ -50,6 +51,12 @@ const SASQLookupComponent = forwardRef((props, ref) => {
50
51
  searchFieldProps
51
52
  } = props;
52
53
  const kintIntl = useKintIntl(passedIntlKey, passedIntlNS);
54
+ const [count, setCount] = useState(0);
55
+
56
+ const { currentPage, paginationMCLProps, paginationSASQProps } = usePrevNextPagination({
57
+ count,
58
+ pageSize: fetchParameters.SASQ_MAP?.perPage
59
+ });
53
60
 
54
61
  const { query, queryGetter, querySetter } = useKiwtSASQuery();
55
62
  const { 0: namespace } = useNamespace();
@@ -57,11 +64,14 @@ const SASQLookupComponent = forwardRef((props, ref) => {
57
64
 
58
65
  const filterPaneVisibileKey = `${namespace}-${id}-filterPaneVisibility`;
59
66
 
60
- const fetchPageData = ({ pageParam = 0 }) => {
61
- const queryMap = fetchParameters.SASQ_MAP;
62
- queryMap.offset = pageParam;
63
- return ky(`${fetchParameters.endpoint}${generateKiwtQuery(queryMap, query)}`).json();
64
- };
67
+ const queryParams = useMemo(() => (
68
+ generateKiwtQuery(
69
+ {
70
+ ...fetchParameters.SASQ_MAP,
71
+ page: currentPage,
72
+ }, (query ?? {})
73
+ )
74
+ ), [currentPage, fetchParameters.SASQ_MAP, query]);
65
75
 
66
76
  const [filterPaneVisible, setFilterPaneVisible] = useLocalStorageState(filterPaneVisibileKey, true);
67
77
  const toggleFilterPane = () => setFilterPaneVisible(!filterPaneVisible);
@@ -72,65 +82,57 @@ const SASQLookupComponent = forwardRef((props, ref) => {
72
82
  }
73
83
  queryNamespace.push('viewAll');
74
84
  queryNamespace.push(query);
85
+ queryNamespace.push(currentPage);
75
86
 
76
87
  const {
77
- data: totalData = {},
78
- ...restOfInfiniteQueryProps
79
- } = useInfiniteQuery(
88
+ data = {},
89
+ ...restOfQueryProps
90
+ } = useQuery(
80
91
  queryNamespace,
81
- fetchPageData
92
+ () => {
93
+ return ky.get(`${fetchParameters.endpoint}${queryParams}`).json();
94
+ },
95
+ {
96
+ enabled: !!currentPage,
97
+ }
82
98
  );
83
99
 
100
+ useEffect(() => {
101
+ if (count !== data?.totalRecords) {
102
+ setCount(data?.totalRecords);
103
+ }
104
+ }, [count, data.totalRecords]);
105
+
84
106
  useImperativeHandle(ref, () => (
85
107
  {
86
108
  lookupQueryProps: {
87
- data: totalData,
88
- ...restOfInfiniteQueryProps
89
- }
109
+ data,
110
+ ...restOfQueryProps
111
+ },
112
+ queryParams
90
113
  }
91
114
  ));
92
115
 
93
- const data = totalData.pages?.reduce(
94
- (acc, curr) => {
95
- const newAcc = { ...acc };
96
- for (const [key, value] of Object.entries(curr)) {
97
- if (
98
- key !== 'page' &&
99
- key !== 'result' &&
100
- acc[key] !== value
101
- ) {
102
- newAcc[key] = value;
103
- }
104
- }
105
-
106
- const newResults = [...(acc.results ?? [])];
107
- newResults.push(...(curr.results ?? []));
108
- newAcc.results = newResults;
109
-
110
- return newAcc;
111
- },
112
- {}
113
- ) ?? {};
114
-
115
116
  return (
116
117
  <SearchAndSortQuery
117
118
  initialSearchState={{ query: '' }}
118
119
  queryGetter={queryGetter}
119
120
  querySetter={querySetter}
120
121
  {...sasqProps}
122
+ {...paginationSASQProps}
121
123
  >
122
124
  {
123
125
  (sasqRenderProps) => {
124
126
  const {
125
- activeFilters,
126
- filterChanged,
127
- getFilterHandlers,
128
- getSearchHandlers,
129
- onSubmitSearch,
130
- resetAll,
131
- searchChanged,
132
- searchValue
133
- } = sasqRenderProps;
127
+ activeFilters,
128
+ filterChanged,
129
+ getFilterHandlers,
130
+ getSearchHandlers,
131
+ onSubmitSearch,
132
+ resetAll,
133
+ searchChanged,
134
+ searchValue
135
+ } = sasqRenderProps;
134
136
 
135
137
  const searchHandlers = getSearchHandlers();
136
138
  const disableReset = !filterChanged && !searchChanged;
@@ -143,7 +145,7 @@ const SASQLookupComponent = forwardRef((props, ref) => {
143
145
  filterPaneFirstMenu,
144
146
  filterPaneLastMenu,
145
147
  ...restOfFilterPaneProps
146
- } = filterPaneProps;
148
+ } = filterPaneProps;
147
149
  const {
148
150
  mainPaneFirstMenu,
149
151
  mainPaneLastMenu,
@@ -269,7 +271,7 @@ const SASQLookupComponent = forwardRef((props, ref) => {
269
271
  query={query}
270
272
  rowNavigation={rowNavigation}
271
273
  toggleFilterPane={toggleFilterPane}
272
- {...restOfInfiniteQueryProps}
274
+ {...restOfQueryProps}
273
275
  {...sasqRenderProps}
274
276
  /*
275
277
  * This is insane, it looks like SASQProps `initialSortState`
@@ -277,7 +279,16 @@ const SASQLookupComponent = forwardRef((props, ref) => {
277
279
  * sort handler. Passing through SASQProps.
278
280
  */
279
281
  {...sasqProps}
280
- {...props}
282
+ // pass down all props handed to us except mclProps (pass those down below with our extra prev/next goodies)
283
+ {
284
+ ...{
285
+ ...props,
286
+ mclProps: {
287
+ ...paginationMCLProps,
288
+ ...mclProps,
289
+ }
290
+ }
291
+ }
281
292
  />
282
293
  </Pane>
283
294
  {children}
@@ -0,0 +1,145 @@
1
+ import { useContext } from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import { useQuery, useMutation, useQueryClient } from 'react-query';
5
+ import { CalloutContext, useOkapiKy } from '@folio/stripes/core';
6
+
7
+ import { uniqBy, sortBy, difference } from 'lodash';
8
+ import { Pane } from '@folio/stripes/components';
9
+
10
+ import { TagsForm } from '@folio/stripes/smart-components';
11
+
12
+ import { tagNamespaceArray } from './tagsConfig';
13
+ import { useTags } from './hooks';
14
+ import { useKintIntl } from '../hooks';
15
+
16
+ const Tags = ({
17
+ invalidateLinks = [], // If there are other queries that need invalidating, pass those here
18
+ labelOverrides = {},
19
+ link,
20
+ onToggle,
21
+ intlKey: passedIntlKey,
22
+ intlNS: passedIntlNS,
23
+ }) => {
24
+ const kintIntl = useKintIntl(passedIntlKey, passedIntlNS);
25
+
26
+ const ky = useOkapiKy();
27
+ const callout = useContext(CalloutContext);
28
+ const queryClient = useQueryClient();
29
+
30
+ // TAG GET/POST
31
+ const { data: { tags = [] } = {} } = useTags(
32
+ [...tagNamespaceArray, link]
33
+ );
34
+
35
+ // istanbul ignore next
36
+ const { mutateAsync: postTags } = useMutation(
37
+ ['tags', 'stripes-erm-components', 'Tags', 'postTags'],
38
+ (data) => ky.post('tags', { json: data }).then(() => {
39
+ queryClient.invalidateQueries('tags');
40
+ })
41
+ );
42
+
43
+ // ENTITY GET/PUT
44
+ const { data: entity } = useQuery(
45
+ [link, 'stripes-erm-components', 'Tags'],
46
+ () => ky.get(link).json()
47
+ );
48
+
49
+ // istanbul ignore next
50
+ const { mutateAsync: putEntity } = useMutation(
51
+ [link, 'stripes-erm-components', 'Tags', 'putEntity'],
52
+ (data) => ky.put(link, { json: data }).then(() => {
53
+ queryClient.invalidateQueries(link);
54
+ if (invalidateLinks?.length) {
55
+ invalidateLinks.forEach(il => queryClient.invalidateQueries(il));
56
+ }
57
+ })
58
+ );
59
+
60
+ // add tags to global list of tags
61
+ // istanbul ignore next
62
+ const saveTags = (tagsToSave) => {
63
+ const newTag = difference(tagsToSave.map(t => (t.value || t)), tags.map(t => t.label.toLowerCase()));
64
+ if (!newTag || !newTag.length) return;
65
+
66
+ postTags({
67
+ label: newTag[0],
68
+ description: newTag[0]
69
+ });
70
+
71
+ callout.sendCallout({
72
+ message: kintIntl.formatKintMessage({
73
+ id: 'newTagCreated',
74
+ overrideValue: labelOverrides.newTagCreated
75
+ })
76
+ });
77
+ };
78
+
79
+ // add tag to the list of entity tags
80
+ // istanbul ignore next
81
+ const saveEntityTags = (tagsToSave) => {
82
+ const tagListMap = (entity?.tags ?? []).map(tag => ({ 'value': tag.value }));
83
+ const tagsMap = tagsToSave.map(tag => ({ 'value': tag.value || tag }));
84
+
85
+ const newTags = sortBy(uniqBy([...tagListMap, ...tagsMap], 'value'));
86
+
87
+ putEntity({
88
+ tags: newTags
89
+ });
90
+ };
91
+
92
+ const onAdd = (addTags) => {
93
+ saveEntityTags(addTags);
94
+ saveTags(addTags);
95
+ };
96
+
97
+ const onRemove = (tag) => {
98
+ const tagToDelete = (entity?.tags ?? []).filter(t => t.value.toLowerCase() === tag.toLowerCase());
99
+
100
+ putEntity({
101
+ tags: [{ id: tagToDelete[0].id, _delete:true }]
102
+ });
103
+ };
104
+
105
+ const entityTags = (entity?.tags ?? []).map(tag => tag.value.toLowerCase());
106
+ return (
107
+ <Pane
108
+ defaultWidth="20%"
109
+ dismissible
110
+ id="tags-helper-pane"
111
+ onClose={onToggle}
112
+ paneSub={kintIntl.formatKintMessage(
113
+ {
114
+ id: 'numberOfTags',
115
+ overrideValue: labelOverrides.numberOfTags
116
+ },
117
+ { count: entity?.tags?.length ?? 0 }
118
+ )}
119
+ paneTitle={kintIntl.formatKintMessage(
120
+ {
121
+ id: 'tags',
122
+ overrideValue: labelOverrides.tags
123
+ }
124
+ )}
125
+ >
126
+ <TagsForm
127
+ entityTags={entityTags}
128
+ onAdd={onAdd}
129
+ onRemove={onRemove}
130
+ tags={tags}
131
+ />
132
+ </Pane>
133
+ );
134
+ };
135
+
136
+ Tags.propTypes = {
137
+ intlKey: PropTypes.string,
138
+ intlNS: PropTypes.string,
139
+ invalidateLinks: PropTypes.arrayOf(PropTypes.string),
140
+ labelOverrides: PropTypes.object,
141
+ link: PropTypes.string,
142
+ onToggle: PropTypes.func
143
+ };
144
+
145
+ export default Tags;
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+
4
+ import {
5
+ IconButton,
6
+ Pane,
7
+ PaneHeader
8
+ } from '@folio/stripes-erm-testing';
9
+
10
+ import Tags from './Tags';
11
+ import { renderWithKintHarness } from '../../../test/jest';
12
+
13
+ const onToggle = jest.fn();
14
+ const onAdd = jest.fn();
15
+ const link = 'erm/sas/14c16fc4-f986-4e60-aa59-4e627fcf160b';
16
+
17
+ describe('Tags', () => {
18
+ let renderComponent;
19
+ beforeEach(() => {
20
+ renderComponent = renderWithKintHarness(
21
+ <MemoryRouter>
22
+ <Tags
23
+ invalidateLinks={[]}
24
+ link={link}
25
+ onAdd={onAdd}
26
+ onToggle={onToggle}
27
+ />
28
+ </MemoryRouter>
29
+ );
30
+ });
31
+
32
+ test('renders the expected label', () => {
33
+ const { getByText } = renderComponent;
34
+ expect(getByText('0 Tags')).toBeInTheDocument();
35
+ });
36
+
37
+ test('renders expected pane dismiss button ', async () => {
38
+ await IconButton('Close Tags').exists();
39
+ });
40
+
41
+ test('renders expected open menu button ', () => {
42
+ const { getByRole } = renderComponent;
43
+ expect(
44
+ getByRole('button', {
45
+ name: 'stripes-components.multiSelection.dropdownTriggerLabel',
46
+ })
47
+ ).toBeInTheDocument();
48
+ });
49
+
50
+ test('renders tags heading ', () => {
51
+ const { getByRole } = renderComponent;
52
+ expect(getByRole('heading', { name: 'Tags' })).toBeInTheDocument();
53
+ });
54
+
55
+ test('renders expected region with zero tags', () => {
56
+ const { getByRole } = renderComponent;
57
+ expect(getByRole('region', { name: 'Tags 0 Tags' })).toBeInTheDocument();
58
+ });
59
+
60
+ test('renders the expected multiSelectDescription', () => {
61
+ const { getByText } = renderComponent;
62
+ expect(getByText('Contains a list of any selected values, followed by an autocomplete textfield for selecting additional values.')).toBeInTheDocument();
63
+ });
64
+
65
+ test('renders the expected label', () => {
66
+ const { getByText } = renderComponent;
67
+ expect(getByText('0 items selected')).toBeInTheDocument();
68
+ });
69
+
70
+ test('displays the tags pane', async () => {
71
+ await Pane('Tags').is({ visible: true });
72
+ });
73
+
74
+ test('displays the tags pane header', async () => {
75
+ await PaneHeader('Tags').is({ visible: true });
76
+ });
77
+ });
@@ -0,0 +1,2 @@
1
+ export { default as useTagsEnabled, tagsEnabledQueryKey } from './useTagsEnabled';
2
+ export { default as useTags } from './useTags';