@internetarchive/ia-book-actions 0.0.6 → 0.0.7-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.
package/demo/index.html CHANGED
@@ -71,6 +71,18 @@
71
71
  float: left;
72
72
  margin: 2px;
73
73
  }
74
+
75
+ body.modal-manager-open {
76
+ overflow: hidden;
77
+ }
78
+
79
+ modal-manager {
80
+ display: none;
81
+ }
82
+
83
+ modal-manager[mode='open'] {
84
+ display: block;
85
+ }
74
86
  </style>
75
87
  </head>
76
88
 
@@ -85,6 +97,7 @@
85
97
  defaultLendingStatus,
86
98
  canBrowseAndBorrow,
87
99
  } from '../src/assets/data/lending-get-status-service.js';
100
+ import { ModalConfig } from '@internetarchive/modal-manager';
88
101
 
89
102
  const userid = '@neeraj-archive';
90
103
  const identifier = 'naturalhistoryof00unse_4111';
@@ -92,13 +105,17 @@
92
105
 
93
106
  let iaBookActions = document.querySelector('ia-book-actions');
94
107
 
108
+ const modalConfig = new ModalConfig();
109
+ modalConfig.headerColor = '#d9534f';
110
+
95
111
  // set defaultLendingStatus for unavailable (without borrowables) book
96
- iaBookActions.userid = '';
112
+ iaBookActions.userid = userid;
97
113
  iaBookActions.identifier = identifier;
98
114
  iaBookActions.bookTitle = 'Contemporary Black biography. Volume 39 : profiles from the interContemporary Black biography. Volume 39';
99
- iaBookActions.lendingStatus = defaultLendingStatus;
115
+ iaBookActions.lendingStatus = canBrowseAndBorrow;
100
116
  iaBookActions.bwbPurchaseUrl = '';
101
117
  iaBookActions.barType = '';
118
+ iaBookActions.modalConfig = modalConfig;
102
119
 
103
120
  let lendingStatus1 = iaBookActions.lendingStatus;
104
121
 
@@ -112,6 +129,29 @@
112
129
  });
113
130
  });
114
131
 
132
+ document.querySelectorAll('.errorEnable input[type=checkbox]').forEach((item) => {
133
+ if (window.location.href.indexOf('?error=true') !== -1) {
134
+ item.checked = true;
135
+ }
136
+ item.addEventListener('click', (e) => {
137
+ var searchParams = new URLSearchParams(window.location.search);
138
+ var separate = '';
139
+ if (e.target.checked) {
140
+ separate = '?';
141
+ searchParams.set('error', true);
142
+ } else {
143
+ separate = '';
144
+ searchParams.delete('error');
145
+ }
146
+
147
+ if (history.pushState) {
148
+ var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + separate + searchParams;
149
+ window.history.pushState({path:newurl},'',newurl);
150
+ }
151
+ });
152
+ });
153
+
154
+
115
155
  document.querySelectorAll('.userState input[type=checkbox]').forEach((item) => {
116
156
  item.addEventListener('click', (e) => {
117
157
  if (e.target.checked) {
@@ -219,6 +259,9 @@
219
259
  <fieldset>
220
260
  <legend>Dev testing for book action bar</legend>
221
261
  <table class="initial">
262
+ <tr colspan='5'>
263
+ <td class="errorEnable"><label><input type="checkbox" name="errorEnable" value="errorEnable" />Do you want to see error response?</label></td>
264
+ </tr>
222
265
  <tr>
223
266
  <td class="userState">
224
267
  <label><input type="checkbox" name="user" value="isAdmin" />isAdmin</label>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.0.6",
3
+ "version": "0.0.7-0",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -20,7 +20,9 @@
20
20
  "dependencies": {
21
21
  "@internetarchive/ia-icons": "^1.2.2",
22
22
  "@internetarchive/icon-bookmark": "^1.1.3",
23
+ "@internetarchive/icon-close": "^1.1.3",
23
24
  "@internetarchive/icon-info": "^1.2.6",
25
+ "@internetarchive/modal-manager": "^0.1.0",
24
26
  "@internetarchive/shared-resize-observer": "^0.0.1",
25
27
  "lit-element": "^2.4.0",
26
28
  "lit-html": "^1.3.0"
@@ -6,6 +6,9 @@ export default css`
6
6
  height: 3.5rem;
7
7
  padding: 1rem 0;
8
8
  }
9
+ .actiongroup {
10
+ display: flex;
11
+ }
9
12
  .action-buttons {
10
13
  display: inline-flex;
11
14
  align-items: center;
@@ -80,7 +83,7 @@ export default css`
80
83
  padding: 0 0.6rem;
81
84
  margin-left: 0;
82
85
  }
83
- .action-loader {
86
+ .actionloader {
84
87
  vertical-align: middle;
85
88
  visibility: hidden;
86
89
  padding: 0.9rem 0.2rem;
@@ -129,9 +132,14 @@ export default css`
129
132
  display: initial;
130
133
  font-size: 1.4rem;
131
134
  }
132
-
133
135
  .unavailable {
134
136
  opacity: 0.7;
135
137
  pointer-events: none;
136
138
  }
139
+
140
+ .disabled {
141
+ opacity: 0.8;
142
+ pointer-events: none;
143
+ visibility: visible;
144
+ }
137
145
  `;
@@ -1,5 +1,6 @@
1
1
  import { html } from 'lit-element';
2
2
  import { nothing } from 'lit-html';
3
+ import { classMap } from 'lit-html/directives/class-map';
3
4
 
4
5
  import ActionsHandler from '../core/services/actions-handler/actions-handler.js';
5
6
 
@@ -25,6 +26,8 @@ export class CollapsibleActionGroup extends ActionsHandler {
25
26
  width: { type: Number },
26
27
  hasAdminAccess: { type: Boolean },
27
28
  dropdownArrow: { type: String },
29
+ disabled: { type: Boolean },
30
+ borrowType: { type: String },
28
31
  };
29
32
  }
30
33
 
@@ -42,6 +45,9 @@ export class CollapsibleActionGroup extends ActionsHandler {
42
45
  this.initialButton = false;
43
46
  this.title = '';
44
47
  this.loaderIcon = 'https://archive.org/upload/images/tree/loading.gif';
48
+ this.disabled = false;
49
+ this.borrowType = ''; // (browsed|borrowed)
50
+ this.consecutiveLoanCounts = 1; // consecutive loan count
45
51
  }
46
52
 
47
53
  updated(changed) {
@@ -50,6 +56,35 @@ export class CollapsibleActionGroup extends ActionsHandler {
50
56
  this.resetActions();
51
57
  }
52
58
  }
59
+
60
+ // this is execute to fetch loan token
61
+ if (changed.has('borrowType')) {
62
+ this.emitEnableBookAccess();
63
+ }
64
+ }
65
+
66
+ /* emit custom event to fetch loan token */
67
+ emitEnableBookAccess() {
68
+ // send consecutiveLoanCounts for browsed books only.
69
+ // for borrowed books, we just send [Counts-1]
70
+ if (this.borrowType === 'browsed') {
71
+ this.consecutiveLoanCounts =
72
+ localStorage.getItem('consecutive-loan-count') ?? 1;
73
+ }
74
+
75
+ this.dispatchEvent(
76
+ new CustomEvent('enableBookAccess', {
77
+ detail: {
78
+ event: {
79
+ category: `${this.borrowType}BookAccess`, // browsedBookAccess | borrowedBookAccess
80
+ action: `${
81
+ this.borrowType === 'browsed' ? 'BrowseCounts-' : 'Counts-'
82
+ }${this.consecutiveLoanCounts}`, // (BrowseCounts-1|Counts-X)
83
+ },
84
+ borrowType: this.borrowType,
85
+ },
86
+ })
87
+ );
53
88
  }
54
89
 
55
90
  /**
@@ -94,13 +129,20 @@ export class CollapsibleActionGroup extends ActionsHandler {
94
129
 
95
130
  render() {
96
131
  return html`
97
- ${this.getLoaderIcon}
98
- <section class="action-buttons primary">
99
- ${this.renderPrimaryActions}
100
- </section>
101
- <section class="action-buttons secondary">
102
- ${this.renderSecondaryActions}
103
- </section>
132
+ <div
133
+ class="${classMap({
134
+ actiongroup: true,
135
+ disabled: this.disabled,
136
+ })}"
137
+ >
138
+ ${this.getLoaderIcon}
139
+ <section class="action-buttons primary">
140
+ ${this.renderPrimaryActions}
141
+ </section>
142
+ <section class="action-buttons secondary">
143
+ ${this.renderSecondaryActions}
144
+ </section>
145
+ </div>
104
146
  `;
105
147
  }
106
148
 
@@ -225,7 +267,14 @@ export class CollapsibleActionGroup extends ActionsHandler {
225
267
  * @returns { HTMLElement }
226
268
  */
227
269
  get getLoaderIcon() {
228
- return html`<img class="action-loader" alt="" src="${this.loaderIcon}" />`;
270
+ return html`<img
271
+ class="${classMap({
272
+ actionloader: true,
273
+ disabled: this.disabled,
274
+ })}"
275
+ alt=""
276
+ src="${this.loaderIcon}"
277
+ />`;
229
278
  }
230
279
 
231
280
  /**
@@ -8,18 +8,11 @@ export default function ActionsHandlerService(options) {
8
8
  const option = {
9
9
  action: null,
10
10
  identifier: '',
11
- data: {},
12
11
  success() {},
13
- error: null,
14
- loader: true,
15
- type: 'POST',
12
+ error() {},
16
13
  ...options,
17
14
  };
18
15
 
19
- if (option.loader) {
20
- showActionButtonLoader();
21
- }
22
-
23
16
  let baseHost = '';
24
17
  if (window.location.pathname === '/demo/') {
25
18
  baseHost = `/demo/`;
@@ -36,23 +29,39 @@ export default function ActionsHandlerService(options) {
36
29
  body: formData,
37
30
  })
38
31
  .then(response => {
39
- if (response.status === 200) {
40
- option.success.call();
32
+ let shouldReturnError =
33
+ window.location.href.indexOf('?error=true') !== -1;
34
+ const tokenError = 'loan token not found. please try again later.';
35
+ const borrowError =
36
+ 'This book is not available to borrow at this time. Please try again later.';
37
+
38
+ // return error reponse if query param has ?error=true param...
39
+ const erroneousActions = ['browse_book', 'borrow_book', 'create_token'];
40
+ if (shouldReturnError && erroneousActions.includes(option.action)) {
41
+ return {
42
+ success: false,
43
+ error: option.action === 'create_token' ? tokenError : borrowError,
44
+ };
41
45
  }
42
- })
43
- .catch(error => {
44
- console.log(error);
45
- });
46
- }
47
46
 
48
- function showActionButtonLoader() {
49
- const collapsibleElement = document
50
- .querySelector('ia-book-actions')
51
- .shadowRoot.querySelector('collapsible-action-group');
52
- collapsibleElement.setAttribute('style', 'opacity:0.8; pointer-events:none');
47
+ // return success response for /demo/ server...
48
+ if (baseHost == '/demo/1' || baseHost == '/demo/') {
49
+ return {
50
+ success: true,
51
+ message: 'operation executed successfully!',
52
+ };
53
+ }
53
54
 
54
- const actionLoader = collapsibleElement.shadowRoot.querySelector(
55
- '.action-loader'
56
- );
57
- actionLoader.setAttribute('style', 'visibility: visible');
55
+ // The response is a Response instance.
56
+ // You parse the data into a useable format using `.json()`
57
+ return response.json();
58
+ })
59
+ .then(data => {
60
+ // `data` is the parsed version of the JSON returned from the above endpoint.
61
+ if (!data.error) {
62
+ option?.success(data);
63
+ } else {
64
+ option?.error(data);
65
+ }
66
+ });
58
67
  }
@@ -7,10 +7,7 @@ import ActionsHandlerService from './actions-handler-service.js';
7
7
  *
8
8
  * ActionsHandlerService is a function being used to execute
9
9
  * APIs based of the request made by user. It consist some parameters are as follows:-
10
- * 1. action: action name like browse_book, return_book, borrow_book etc...
11
- * 2. identifier: book name.
12
- * 3. success: This is callback function which will be executed after the API request
13
- * return reponse as a success.
10
+ * 1. identifier: book name.
14
11
  */
15
12
 
16
13
  export default class ActionsHandler extends LitElement {
@@ -18,22 +15,37 @@ export default class ActionsHandler extends LitElement {
18
15
  super();
19
16
  this.identifier = identifier;
20
17
  this.ajaxTimeout = 6000;
18
+ this.loanTokenPollingDelay =
19
+ window.location.pathname === '/demo/' ? 2000 : 120000; // 120000 ms = 2 min
20
+ this.loanTokenInterval = null;
21
21
  this.bindEvents();
22
22
  }
23
23
 
24
+ disconnectedCallback() {
25
+ this.loanTokenInterval = undefined;
26
+ }
27
+
24
28
  sendEvent(eventCategory, eventAction) {
25
29
  // eslint-disable-next-line no-console
26
30
  console?.log('Book action: ', { eventCategory, eventAction });
27
- window?.archive_analytics?.send_event(
31
+ window?.archive_analytics?.send_event_no_sampling(
28
32
  eventCategory,
29
33
  eventAction,
30
- window.location.pathname
34
+ `identifier=${this.identifier}`
31
35
  );
32
36
  }
33
37
 
34
38
  bindEvents() {
35
39
  this.addEventListener('browseBook', ({ detail }) => {
36
40
  this.handleBrowseIt();
41
+ this.setConsecutiveLoanCounts();
42
+ const { category, action } = detail.event;
43
+ this.sendEvent(category, action);
44
+ });
45
+
46
+ this.addEventListener('browseBookAgain', ({ detail }) => {
47
+ this.handleBrowseIt();
48
+ this.setConsecutiveLoanCounts('browseAgain');
37
49
  const { category, action } = detail.event;
38
50
  this.sendEvent(category, action);
39
51
  });
@@ -87,59 +99,128 @@ export default class ActionsHandler extends LitElement {
87
99
  const { category, action } = detail.event;
88
100
  this.sendEvent(category, action);
89
101
  });
102
+
103
+ this.addEventListener('enableBookAccess', ({ detail }) => {
104
+ const borrowType = detail?.borrowType;
105
+
106
+ // fetch loan token for browsed/borrowed book and set an interval
107
+ if (borrowType) {
108
+ console.log('token poll started...');
109
+
110
+ this.loanTokenInterval = setInterval(() => {
111
+ this.handleLoanTokenPoller();
112
+ }, this.loanTokenPollingDelay);
113
+
114
+ const { category, action } = detail?.event;
115
+ this.sendEvent(category, action);
116
+ } else {
117
+ // if book is not browsed, just clear token polling interval
118
+ clearInterval(this.loanTokenInterval);
119
+ }
120
+ });
90
121
  }
91
122
 
92
123
  handleBrowseIt() {
124
+ const context = 'browse_book';
125
+ this.ActionError(context);
126
+
93
127
  ActionsHandlerService({
94
- action: 'browse_book',
128
+ action: context,
95
129
  identifier: this.identifier,
96
130
  success: () => {
97
131
  this.handleReadItNow();
98
132
  },
133
+ error: data => {
134
+ this.ActionError(context, data);
135
+ },
99
136
  });
100
137
  }
101
138
 
102
139
  handleReturnIt() {
140
+ const context = 'return_loan';
141
+ this.ActionError(context);
142
+
103
143
  ActionsHandlerService({
104
- action: 'return_loan',
144
+ action: context,
105
145
  identifier: this.identifier,
106
146
  success: () => {
107
147
  this.deleteLoanCookies();
108
148
  URLHelper.goToUrl(`/details/${this.identifier}`, true);
109
149
  },
150
+ error: data => {
151
+ this.ActionError(context, data);
152
+ },
110
153
  });
111
154
  }
112
155
 
113
156
  handleBorrowIt() {
157
+ const context = 'borrow_book';
158
+ this.ActionError(context);
159
+
114
160
  ActionsHandlerService({
115
- action: 'borrow_book',
161
+ action: context,
116
162
  identifier: this.identifier,
117
163
  success: () => {
118
164
  this.handleReadItNow();
119
165
  },
166
+ error: data => {
167
+ this.ActionError(context, data);
168
+ },
120
169
  });
121
170
  }
122
171
 
123
172
  handleReserveIt() {
173
+ const context = 'join_waitlist';
174
+ this.ActionError(context);
175
+
124
176
  ActionsHandlerService({
125
- action: 'join_waitlist',
177
+ action: context,
126
178
  identifier: this.identifier,
127
179
  success: () => {
128
180
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
129
181
  },
182
+ error: data => {
183
+ this.ActionError(context, data);
184
+ },
130
185
  });
131
186
  }
132
187
 
133
188
  handleRemoveFromWaitingList() {
189
+ const context = 'leave_waitlist';
190
+ this.ActionError(context);
191
+
134
192
  ActionsHandlerService({
135
- action: 'leave_waitlist',
193
+ action: context,
136
194
  identifier: this.identifier,
137
195
  success: () => {
138
196
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
139
197
  },
198
+ error: data => {
199
+ this.ActionError(context, data);
200
+ },
201
+ });
202
+ }
203
+
204
+ handleLoanTokenPoller() {
205
+ const context = 'create_token';
206
+ ActionsHandlerService({
207
+ identifier: this.identifier,
208
+ action: context,
209
+ error: data => {
210
+ this.ActionError(context, data);
211
+ clearInterval(this.loanTokenInterval); // stop token fetch api
212
+ },
140
213
  });
141
214
  }
142
215
 
216
+ ActionError(context, data = {}) {
217
+ this.dispatchEvent(
218
+ new CustomEvent('lendingActionError', {
219
+ detail: { context, data },
220
+ })
221
+ );
222
+ }
223
+
143
224
  handleLoginOk() {
144
225
  /* eslint class-methods-use-this: "off" */
145
226
  const target = `/account/login?referer=${encodeURIComponent(
@@ -172,6 +253,23 @@ export default class ActionsHandler extends LitElement {
172
253
  }, this.ajaxTimeout);
173
254
  }
174
255
 
256
+ // save consecutive loan count for borrow
257
+ setConsecutiveLoanCounts(action = '') {
258
+ const { localStorage } = window;
259
+ if (localStorage) {
260
+ let newCount = 1;
261
+ const storageKey = `consecutive-loan-count`;
262
+ const existingCount = Number(localStorage.getItem(storageKey));
263
+
264
+ // want to increase browse-count by 1 if,
265
+ // you consecutive reading a book.
266
+ if (action === 'browseAgain') {
267
+ newCount = existingCount ? existingCount + 1 : 1;
268
+ }
269
+ localStorage.setItem(storageKey, newCount);
270
+ }
271
+ }
272
+
175
273
  deleteLoanCookies() {
176
274
  const date = new Date();
177
275
  date.setTime(date.getTime() - 24 * 60 * 60 * 1000); // one day ago
@@ -75,7 +75,9 @@ export default class GetLendingActions {
75
75
  const disableBorrow = lendingStatus.loanCount >= lendingStatus.maxLoans;
76
76
 
77
77
  let primaryTitleText = '';
78
- if (lendingStatus.user_has_browsed) {
78
+ let isBrowsed =
79
+ lendingStatus.user_has_browsed && !lendingStatus.browsingExpired;
80
+ if (isBrowsed) {
79
81
  primaryTitleText = this.getBrowseCountdownTitle();
80
82
  } else {
81
83
  primaryTitleText = `Your loan of this book has ${lendingStatus.daysLeftOnLoan} days left.`;
@@ -94,6 +96,7 @@ export default class GetLendingActions {
94
96
  this.actionsConfig.adminAccessConfig(),
95
97
  this.actionsConfig.purchaseConfig(),
96
98
  ],
99
+ borrowType: isBrowsed ? 'browsed' : 'borrowed',
97
100
  };
98
101
  }
99
102
 
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Helper function to fetch real-time lending status
3
+ */
4
+ export default function getRealTimeLendingStatus(identifier) {
5
+ let url = `https://ia-petabox-neeraj.archive.org/services/book/lending-info.php?identifier=${identifier}`;
6
+
7
+ if (window.location.pathname === '/demo/') {
8
+ url = ''; // change URL to prevent error on localhost
9
+ }
10
+
11
+ fetch(url)
12
+ .then(response => {
13
+ // return { response_code: '400', data: 'error happened' };
14
+ return response.json();
15
+ })
16
+ .then(data => {
17
+ let newLendingInfo = data;
18
+ const newLendingStatus = data?.lendingStatus;
19
+ newLendingInfo = { ...newLendingInfo, ...newLendingStatus };
20
+ newLendingInfo.lendingStatus = '';
21
+
22
+ return newLendingInfo;
23
+ })
24
+ .catch(data => {
25
+ return data;
26
+ });
27
+ }
@@ -2,6 +2,7 @@
2
2
  import { html, css, LitElement } from 'lit-element';
3
3
 
4
4
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
5
+ import { ModalConfig } from '@internetarchive/modal-manager';
5
6
 
6
7
  import './components/collapsible-action-group.js';
7
8
  import './components/book-title-bar.js';
@@ -9,6 +10,8 @@ import './components/text-group.js';
9
10
  import './components/info-icon.js';
10
11
 
11
12
  import GetLendingActions from './core/services/get-lending-actions.js';
13
+ import getRealTimeLendingStatus from './core/services/get-realtime-lending-status.js';
14
+
12
15
  import { mobileContainerWidth } from './core/config/constants.js';
13
16
 
14
17
  export default class IABookActions extends LitElement {
@@ -22,6 +25,8 @@ export default class IABookActions extends LitElement {
22
25
  bwbPurchaseUrl: { type: String },
23
26
  barType: { type: String },
24
27
  sharedObserver: { attribute: false },
28
+ disableActionGroup: { type: Boolean },
29
+ modalConfig: { type: Object },
25
30
  };
26
31
  }
27
32
 
@@ -40,6 +45,9 @@ export default class IABookActions extends LitElement {
40
45
  this.primaryColor = 'primary';
41
46
  this.secondaryActions = [];
42
47
  this.lendingOptions = {};
48
+ this.disableActionGroup = false;
49
+ this.modalConfig = {};
50
+ this.realtimeInternal = 0; // store intervalID
43
51
  }
44
52
 
45
53
  disconnectedCallback() {
@@ -51,7 +59,26 @@ export default class IABookActions extends LitElement {
51
59
  this.sharedObserver = new SharedResizeObserver();
52
60
  this.setupResizeObserver();
53
61
  }
62
+
63
+ if (!Object.keys(this.modalConfig).length) {
64
+ this.modalConfig = new ModalConfig();
65
+ this.modalConfig.headerColor = '#d9534f';
66
+ }
54
67
  this.setupLendingToolbarActions();
68
+
69
+ // fetch latest lending status after an internal
70
+ if (
71
+ this.lendingStatus.user_has_browsed ||
72
+ this.lendingStatus.user_has_borrowed
73
+ ) {
74
+ this.realtimeInternal = setInterval(() => {
75
+ const latestLendingStatus = getRealTimeLendingStatus(
76
+ 'naturalhistoryof00unse_4'
77
+ );
78
+ console.log(latestLendingStatus);
79
+ if (latestLendingStatus) this.lendingStatus = latestLendingStatus;
80
+ }, 30000); // 60000 ms = 1 min
81
+ }
55
82
  }
56
83
 
57
84
  updated(changed) {
@@ -67,8 +94,7 @@ export default class IABookActions extends LitElement {
67
94
  }
68
95
 
69
96
  browseHasExpired() {
70
- const browsingExpired = true;
71
- const currStatus = { ...this.lendingStatus, browsingExpired };
97
+ const currStatus = { ...this.lendingStatus, browsingExpired: true };
72
98
  this.lendingStatus = currStatus;
73
99
  }
74
100
 
@@ -140,10 +166,10 @@ export default class IABookActions extends LitElement {
140
166
  return action != null;
141
167
  });
142
168
 
143
- if (
144
- this.lendingStatus.user_has_browsed &&
145
- !this.lendingStatus.browseHasExpired
146
- ) {
169
+ this.borrowType = actions.borrowType;
170
+ if (this.borrowType === 'browsed') {
171
+ // start timer for browsed.
172
+ // when browse is completed, we shows browse-again button
147
173
  this.startBrowseTimer();
148
174
  }
149
175
  }
@@ -172,13 +198,69 @@ export default class IABookActions extends LitElement {
172
198
  .primaryActions=${this.primaryActions}
173
199
  .secondaryActions=${this.secondaryActions}
174
200
  .width=${this.width}
175
- .hasAdminAccess=${this.hasAdminAccess}
201
+ .borrowType=${this.borrowType}
202
+ ?hasAdminAccess=${this.hasAdminAccess}
203
+ ?disabled=${this.disableActionGroup}
204
+ @lendingActionError=${this.handleLendingActionError}
176
205
  >
177
206
  </collapsible-action-group>
178
207
  ${this.textGroupTemplate} ${this.infoIconTemplate}
179
208
  `;
180
209
  }
181
210
 
211
+ /*
212
+ * handle lending errors occure on different operation like
213
+ * browse book, borrow book, borrowed book token etc...
214
+ */
215
+ handleLendingActionError(e) {
216
+ // toggle activity loader
217
+ this.disableActionGroup = !this.disableActionGroup;
218
+
219
+ const context = e?.detail?.context;
220
+ const errorMsg = e?.detail?.data?.error;
221
+
222
+ if (errorMsg) this.showErrorModal(context, errorMsg);
223
+
224
+ // update action bar state if book is not available to browse or borrow.
225
+ if (errorMsg && errorMsg.match(/not available to borrow/gm)) {
226
+ let currStatus = this.lendingStatus;
227
+ if (context === 'browse_book') {
228
+ currStatus = {
229
+ ...this.lendingStatus,
230
+ available_to_browse: false,
231
+ };
232
+ } else if (context === 'borrow_book') {
233
+ currStatus = {
234
+ ...this.lendingStatus,
235
+ available_to_borrow: false,
236
+ };
237
+ }
238
+ this.lendingStatus = currStatus;
239
+ }
240
+ }
241
+
242
+ /* show error message if something went wrong */
243
+ async showErrorModal(context, errorMsg) {
244
+ // add modal-manager in DOM to show alert message
245
+ this.modal = document.createElement('modal-manager');
246
+ this.modal.id = 'action-bar-modal';
247
+ await document.body.appendChild(this.modal);
248
+
249
+ // fallback if <modal-manager> is not found!
250
+ if (!this.modal) {
251
+ alert(errorMsg);
252
+ return;
253
+ }
254
+
255
+ this.disableActionGroup = false;
256
+ this.modalConfig.title = 'Lending error';
257
+ this.modalConfig.message = errorMsg;
258
+
259
+ this.modal.showModal({
260
+ config: this.modalConfig,
261
+ });
262
+ }
263
+
182
264
  get iconClass() {
183
265
  return this.width <= mobileContainerWidth ? 'mobile' : 'desktop';
184
266
  }
@@ -1,5 +1,6 @@
1
1
  import { html, fixture, expect } from '@open-wc/testing';
2
2
  import '../../src/components/collapsible-action-group.js';
3
+ import Sinon from 'sinon';
3
4
 
4
5
  const container = ({
5
6
  primaryActions = [
@@ -23,10 +24,12 @@ const container = ({
23
24
  text: 'Purchase',
24
25
  },
25
26
  ],
27
+ borrowType = '',
26
28
  } = {}) =>
27
29
  html`<collapsible-action-group
28
30
  .primaryActions=${primaryActions}
29
31
  .secondaryActions=${secondaryActions}
32
+ .borrowType=${borrowType}
30
33
  ></collapsible-action-group>`;
31
34
 
32
35
  describe('<collapsible-action-group>', () => {
@@ -39,6 +42,22 @@ describe('<collapsible-action-group>', () => {
39
42
  const primaryButton = primaryActionContainer.querySelector('.ia-button');
40
43
  expect(primaryButton.innerText).to.equal('Borrow for 1 hour');
41
44
  });
45
+
46
+ it('check if loader is active and action-group is disabled', async () => {
47
+ const el = await fixture(container());
48
+
49
+ const primaryActionContainer = el.shadowRoot.querySelector('.primary');
50
+ primaryActionContainer
51
+ .querySelector('.ia-button')
52
+ .dispatchEvent(new CustomEvent('toggle-loader'));
53
+ el.disabled = true;
54
+ await el.updateComplete;
55
+
56
+ expect(el.disabled).to.be.true;
57
+ expect(el.shadowRoot.querySelector('div').classList.contains('disabled')).to
58
+ .be.true;
59
+ });
60
+
42
61
  it('fires analytics events', async () => {
43
62
  const el = await fixture(container());
44
63
  const primaryActionContainer = el.shadowRoot.querySelector('.primary');
@@ -66,3 +85,27 @@ describe('<collapsible-action-group>', () => {
66
85
  expect(eAnalytics.action).to.equal('Browse');
67
86
  });
68
87
  });
88
+
89
+ describe('fetch loan token and localStorage for browsed/borrowsed book', () => {
90
+ beforeEach(() => {
91
+ const localStorageMock = {
92
+ getItem: Sinon.spy(),
93
+ setItem: Sinon.spy(),
94
+ };
95
+ Object.defineProperty(window, 'localStorage', {
96
+ value: localStorageMock,
97
+ });
98
+ });
99
+
100
+ it('test localStorage function when borrowType is browsed', async () => {
101
+ await fixture(container({ borrowType: 'browsed' }));
102
+ expect(localStorage.getItem).to.be.calledOnce;
103
+ expect(localStorage.getItem).to.be.calledWith('consecutive-loan-count');
104
+ });
105
+
106
+ it('test localStorage function when borrowType is borrowed', async () => {
107
+ await fixture(container({ borrowType: 'borrowed' }));
108
+ expect(localStorage.getItem).to.not.be.calledOnce;
109
+ expect(localStorage.getItem).to.not.be.calledWith('consecutive-loan-count');
110
+ });
111
+ });
@@ -106,6 +106,64 @@ describe('Primary Actions data', () => {
106
106
  });
107
107
 
108
108
  describe('Borrow status actions', () => {
109
+ it('Update available_to_browse key when book is not available to borrow', async () => {
110
+ const el = await fixture(
111
+ container({
112
+ userid: '@user1',
113
+ lendingStatus: {
114
+ is_lendable: true,
115
+ available_to_browse: true,
116
+ available_to_borrow: true,
117
+ },
118
+ })
119
+ );
120
+
121
+ const errorEvent = new CustomEvent('lendingActionError');
122
+ el.addEventListener('lendingActionError', () => {
123
+ el.handleLendingActionError({
124
+ detail: {
125
+ context: 'browse_book',
126
+ data: { error: 'not available to borrow' },
127
+ },
128
+ });
129
+ });
130
+ el.dispatchEvent(errorEvent);
131
+ await el.updateComplete;
132
+
133
+ // removed available_to_browse from lending bar
134
+ expect(el.lendingStatus.available_to_browse).to.be.false;
135
+ expect(el.primaryActions[0].text).to.equal('Borrow for 14 days');
136
+ });
137
+
138
+ it('Update available_to_borrow key when book is not available to borrow', async () => {
139
+ const el = await fixture(
140
+ container({
141
+ userid: '@user1',
142
+ lendingStatus: {
143
+ is_lendable: true,
144
+ available_to_browse: true,
145
+ available_to_borrow: true,
146
+ },
147
+ })
148
+ );
149
+
150
+ const errorEvent = new Event('lendingActionError');
151
+ el.addEventListener('lendingActionError', () => {
152
+ el.handleLendingActionError({
153
+ detail: {
154
+ context: 'borrow_book',
155
+ data: { error: 'not available to borrow' },
156
+ },
157
+ });
158
+ });
159
+ el.dispatchEvent(errorEvent);
160
+ await el.updateComplete;
161
+
162
+ // removed available_to_borrow from lending bar
163
+ expect(el.lendingStatus.available_to_borrow).to.be.false;
164
+ expect(el.primaryActions[0].text).to.equal('Borrow for 1 hour');
165
+ });
166
+
109
167
  it('Check action for borrowable book without user', async () => {
110
168
  const el = await fixture(
111
169
  container({