@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 +45 -2
- package/package.json +3 -1
- package/src/assets/styles/collapsible-action-group.js +10 -2
- package/src/components/collapsible-action-group.js +57 -8
- package/src/core/services/actions-handler/actions-handler-service.js +33 -24
- package/src/core/services/actions-handler/actions-handler.js +109 -11
- package/src/core/services/get-lending-actions.js +4 -1
- package/src/core/services/get-realtime-lending-status.js +27 -0
- package/src/ia-book-actions.js +89 -7
- package/test/components/collapsible-action-group.test.js +43 -0
- package/test/ia-book-actions.test.js +58 -0
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 =
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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?.
|
|
31
|
+
window?.archive_analytics?.send_event_no_sampling(
|
|
28
32
|
eventCategory,
|
|
29
33
|
eventAction,
|
|
30
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|
package/src/ia-book-actions.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
.
|
|
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({
|