@internetarchive/ia-book-actions 0.0.6-3 → 0.0.6-7

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,16 @@
71
71
  float: left;
72
72
  margin: 2px;
73
73
  }
74
+
75
+ #ui-overlay {
76
+ position: fixed;
77
+ top: 0;
78
+ left: 0;
79
+ width: 100%;
80
+ height: 100%;
81
+ background: black;
82
+ opacity: 0.3;
83
+ }
74
84
  </style>
75
85
  </head>
76
86
 
@@ -96,7 +106,7 @@
96
106
  iaBookActions.userid = '';
97
107
  iaBookActions.identifier = identifier;
98
108
  iaBookActions.bookTitle = 'Contemporary Black biography. Volume 39 : profiles from the interContemporary Black biography. Volume 39';
99
- iaBookActions.lendingStatus = defaultLendingStatus;
109
+ iaBookActions.lendingStatus = canBrowseAndBorrow;
100
110
  iaBookActions.bwbPurchaseUrl = '';
101
111
  iaBookActions.barType = '';
102
112
 
@@ -146,7 +156,7 @@
146
156
  if (e.target.value === 'user_has_browsed') {
147
157
  lendingStatus1.user_has_browsed = true;
148
158
  lendingStatus1.available_to_browse = false;
149
- lendingStatus1.secondsLeftOnLoan = 10;
159
+ lendingStatus1.secondsLeftOnLoan = 30;
150
160
  lendingStatus1.browsingExpired = false;
151
161
  } else if(e.target.value === 'browsingExpired') {
152
162
  lendingStatus1.user_has_browsed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.0.6-3",
3
+ "version": "0.0.6-7",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -21,6 +21,7 @@
21
21
  "@internetarchive/ia-icons": "^1.2.2",
22
22
  "@internetarchive/icon-bookmark": "^1.1.3",
23
23
  "@internetarchive/icon-info": "^1.2.6",
24
+ "@internetarchive/icon-close": "^1.1.3",
24
25
  "@internetarchive/shared-resize-observer": "^0.0.1",
25
26
  "lit-element": "^2.4.0",
26
27
  "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;
@@ -27,6 +27,7 @@ export class CollapsibleActionGroup extends ActionsHandler {
27
27
  hasAdminAccess: { type: Boolean },
28
28
  dropdownArrow: { type: String },
29
29
  disabled: { type: Boolean },
30
+ bookHasBrowsed: { type: Boolean },
30
31
  };
31
32
  }
32
33
 
@@ -45,6 +46,13 @@ export class CollapsibleActionGroup extends ActionsHandler {
45
46
  this.title = '';
46
47
  this.loaderIcon = 'https://archive.org/upload/images/tree/loading.gif';
47
48
  this.disabled = false;
49
+ this.bookHasBrowsed = false;
50
+ }
51
+
52
+ firstUpdated() {
53
+ if (this.bookHasBrowsed) {
54
+ this.emitFetchLoanToken();
55
+ }
48
56
  }
49
57
 
50
58
  updated(changed) {
@@ -53,6 +61,23 @@ export class CollapsibleActionGroup extends ActionsHandler {
53
61
  this.resetActions();
54
62
  }
55
63
  }
64
+
65
+ // this is execute on demo page only
66
+ if (changed.has('bookHasBrowsed')) {
67
+ this.emitFetchLoanToken();
68
+ }
69
+ }
70
+
71
+ /* emit custom event to fetch loan token */
72
+ emitFetchLoanToken() {
73
+ this.dispatchEvent(
74
+ new CustomEvent('fetchLoanToken', {
75
+ detail: {
76
+ event: { category: 'action' },
77
+ bookHasBrowsed: this.bookHasBrowsed,
78
+ },
79
+ })
80
+ );
56
81
  }
57
82
 
58
83
  /**
@@ -0,0 +1,139 @@
1
+ /* eslint-disable */
2
+ import { LitElement, html, css } from 'lit-element';
3
+ import { classMap } from 'lit-html/directives/class-map';
4
+
5
+ import '@internetarchive/icon-close/icon-close.js';
6
+ import buttonBaseStyle from '../assets/styles/ia-button.js';
7
+
8
+ export class ShowDialog extends LitElement {
9
+ static get properties() {
10
+ return {
11
+ title: { type: String },
12
+ body: { type: String },
13
+ actions: { type: Array },
14
+ opened: { type: Boolean },
15
+ };
16
+ }
17
+
18
+ constructor() {
19
+ super();
20
+ this.title = 'Error';
21
+ this.body =
22
+ 'Unexpected error: loan does not exist. Please try deleting your archive.org cookie.';
23
+ this.actions = [];
24
+ this.opened = false;
25
+ }
26
+
27
+ updated(changed) {
28
+ if (changed.get('opened') === false) {
29
+ this.addOverlay();
30
+ } else {
31
+ this.removeOverlay();
32
+ }
33
+ }
34
+
35
+ render() {
36
+ return html`
37
+ <div
38
+ class="${classMap({
39
+ dialog: true,
40
+ opened: this.opened,
41
+ closed: !this.opened,
42
+ })}"
43
+ >
44
+ <div class="dialog-head">
45
+ ${this.title}
46
+ <ia-icon-close
47
+ @click="${() =>
48
+ this.dispatchEvent(new CustomEvent('dialogAlertClose'))}"
49
+ ></ia-icon-close>
50
+ </div>
51
+ <div class="dialog-body">${this.body}</div>
52
+ <div class="dialog-foot">${this.renderActionButton}</div>
53
+ </div>
54
+ `;
55
+ }
56
+
57
+ /* exceptional case to use dom element */
58
+ addOverlay() {
59
+ if (this.opened && !document.getElementById('ui-overlay')) {
60
+ const overlayElement = document.createElement('div');
61
+ overlayElement.id = 'ui-overlay';
62
+ document.getElementsByTagName('body')[0].appendChild(overlayElement);
63
+ }
64
+ }
65
+
66
+ /* exceptional case to use dom element */
67
+ removeOverlay() {
68
+ const overlayElement = document.getElementById('ui-overlay');
69
+ if (overlayElement) overlayElement.remove();
70
+ }
71
+
72
+ get renderActionButton() {
73
+ if (!this.actions) return;
74
+
75
+ return this.actions.map(
76
+ action =>
77
+ html`<a class=${action.className} href=${action.href}>
78
+ ${action.text}
79
+ </button>`
80
+ );
81
+ }
82
+
83
+ static get styles() {
84
+ const dialogStyle = css`
85
+ :host {
86
+ color: #343434;
87
+ font-size: 1.6rem;
88
+ }
89
+ .dialog {
90
+ position: absolute;
91
+ background: #fff;
92
+ border-radius: 1.2rem;
93
+ z-index: 3000;
94
+ flex-direction: column;
95
+ min-width: 300px;
96
+ max-width: 400px;
97
+ left: 50%;
98
+ transform: translateX(-50%);
99
+
100
+ border: 2px solid #ccc;
101
+ border-radius: 12px;
102
+ box-shadow: 0px 0px 27px 6px rgb(66 66 66 / 80%);
103
+ }
104
+ .dialog-head,
105
+ .dialog-body,
106
+ .dialog-foot {
107
+ padding: 1rem;
108
+ user-select: none;
109
+ text-align: left;
110
+ }
111
+ .dialog-body {
112
+ border: 1px solid #d8d8d8;
113
+ }
114
+ ia-icon-close {
115
+ position: absolute;
116
+ right: 0.5rem;
117
+ top: 0.5rem;
118
+ width: 2.5rem;
119
+ height: 2.5rem;
120
+ vertical-align: middle;
121
+ --iconFillColor: #333;
122
+ cursor: pointer;
123
+ }
124
+ .opened {
125
+ display: flex;
126
+ }
127
+ .closed {
128
+ display: none;
129
+ }
130
+ a {
131
+ text-decoration: none;
132
+ }
133
+ `;
134
+
135
+ return [buttonBaseStyle, dialogStyle];
136
+ }
137
+ }
138
+
139
+ customElements.define('show-dialog', ShowDialog);
@@ -18,7 +18,7 @@ export default function ActionsHandlerService(options) {
18
18
 
19
19
  let baseHost = '';
20
20
  if (window.location.pathname === '/demo/') {
21
- baseHost = `/demo/1`;
21
+ baseHost = `/demo/`;
22
22
  } else {
23
23
  baseHost = `/services/loans/loan`;
24
24
  }
@@ -32,8 +32,18 @@ export default function ActionsHandlerService(options) {
32
32
  body: formData,
33
33
  })
34
34
  .then(response => {
35
+ // test changes, (won't affect you)
35
36
  if (baseHost == '/demo/1' || baseHost == '/demo/') {
36
- return { error: 'There was an error.' };
37
+ if (option.action == 'create_token') {
38
+ return {
39
+ error: 'loan token not found. please try again later.',
40
+ };
41
+ }
42
+
43
+ return {
44
+ error:
45
+ 'This book is not available to borrow at this time. Please try again later.',
46
+ };
37
47
  }
38
48
 
39
49
  // The response is a Response instance.
@@ -42,7 +52,6 @@ export default function ActionsHandlerService(options) {
42
52
  })
43
53
  .then(data => {
44
54
  // `data` is the parsed version of the JSON returned from the above endpoint.
45
- console.log(data.error);
46
55
  if (!data.error) {
47
56
  option?.success(data);
48
57
  } else {
@@ -15,6 +15,8 @@ export default class ActionsHandler extends LitElement {
15
15
  super();
16
16
  this.identifier = identifier;
17
17
  this.ajaxTimeout = 6000;
18
+ this.loanTokenPollingDelay = 20000; // 20000 ms = 20 sec
19
+ this.loanTokenInterval = null;
18
20
  this.bindEvents();
19
21
  }
20
22
 
@@ -84,29 +86,43 @@ export default class ActionsHandler extends LitElement {
84
86
  const { category, action } = detail.event;
85
87
  this.sendEvent(category, action);
86
88
  });
89
+
90
+ this.addEventListener('fetchLoanToken', ({ detail }) => {
91
+ const bookHasBrowsed = detail?.bookHasBrowsed;
92
+
93
+ // fetch loan token and set an interval
94
+ if (bookHasBrowsed) {
95
+ console.log('token poll started...');
96
+
97
+ this.loanTokenInterval = setInterval(() => {
98
+ this.handleLoanTokenPoller();
99
+ }, this.loanTokenPollingDelay);
100
+ } else {
101
+ // if book is not browsed, just clear token polling interval
102
+ clearInterval(this.loanTokenInterval);
103
+ }
104
+ });
87
105
  }
88
106
 
89
107
  handleBrowseIt() {
90
108
  const context = 'browse_book';
91
- this.toggleLoader(context);
109
+ this.ActionError(context);
92
110
 
93
111
  ActionsHandlerService({
94
112
  action: context,
95
113
  identifier: this.identifier,
96
114
  success: () => {
97
- // this.handleReadItNow();
115
+ this.handleReadItNow();
98
116
  },
99
117
  error: data => {
100
- console.log(data);
101
- alert(data.error);
102
- this.toggleLoader(context);
118
+ this.ActionError(context, data);
103
119
  },
104
120
  });
105
121
  }
106
122
 
107
123
  handleReturnIt() {
108
124
  const context = 'return_loan';
109
- this.toggleLoader(context);
125
+ this.ActionError(context);
110
126
 
111
127
  ActionsHandlerService({
112
128
  action: context,
@@ -116,16 +132,14 @@ export default class ActionsHandler extends LitElement {
116
132
  URLHelper.goToUrl(`/details/${this.identifier}`, true);
117
133
  },
118
134
  error: data => {
119
- console.log(data);
120
- alert(data.error);
121
- this.toggleLoader(context);
135
+ this.ActionError(context, data);
122
136
  },
123
137
  });
124
138
  }
125
139
 
126
140
  handleBorrowIt() {
127
141
  const context = 'borrow_book';
128
- this.toggleLoader(context);
142
+ this.ActionError(context);
129
143
 
130
144
  ActionsHandlerService({
131
145
  action: context,
@@ -134,16 +148,14 @@ export default class ActionsHandler extends LitElement {
134
148
  this.handleReadItNow();
135
149
  },
136
150
  error: data => {
137
- console.log(data);
138
- alert(data.error);
139
- this.toggleLoader(context);
151
+ this.ActionError(context, data);
140
152
  },
141
153
  });
142
154
  }
143
155
 
144
156
  handleReserveIt() {
145
157
  const context = 'join_waitlist';
146
- this.toggleLoader(context);
158
+ this.ActionError(context);
147
159
 
148
160
  ActionsHandlerService({
149
161
  action: context,
@@ -152,16 +164,14 @@ export default class ActionsHandler extends LitElement {
152
164
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
153
165
  },
154
166
  error: data => {
155
- console.log(data);
156
- alert(data.error);
157
- this.toggleLoader(context);
167
+ this.ActionError(context, data);
158
168
  },
159
169
  });
160
170
  }
161
171
 
162
172
  handleRemoveFromWaitingList() {
163
173
  const context = 'leave_waitlist';
164
- this.toggleLoader(context);
174
+ this.ActionError(context);
165
175
 
166
176
  ActionsHandlerService({
167
177
  action: context,
@@ -170,13 +180,36 @@ export default class ActionsHandler extends LitElement {
170
180
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
171
181
  },
172
182
  error: data => {
173
- console.log(data);
174
- alert(data.error);
175
- this.toggleLoader(context);
183
+ this.ActionError(context, data);
176
184
  },
177
185
  });
178
186
  }
179
187
 
188
+ handleLoanTokenPoller() {
189
+ const context = 'create_token';
190
+ this.ActionError(context);
191
+
192
+ ActionsHandlerService({
193
+ identifier: this.identifier,
194
+ action: context,
195
+ loader: false,
196
+ success: () => {
197
+ this.handleReadItNow();
198
+ },
199
+ error: data => {
200
+ this.ActionError(context, data);
201
+ },
202
+ });
203
+ }
204
+
205
+ ActionError(context, data = {}) {
206
+ this.dispatchEvent(
207
+ new CustomEvent('lendingActionError', {
208
+ detail: { context, data },
209
+ })
210
+ );
211
+ }
212
+
180
213
  handleLoginOk() {
181
214
  /* eslint class-methods-use-this: "off" */
182
215
  const target = `/account/login?referer=${encodeURIComponent(
@@ -188,9 +221,6 @@ export default class ActionsHandler extends LitElement {
188
221
  handleReadItNow(extraParam) {
189
222
  const currentParams = new URLSearchParams(window.location.search);
190
223
 
191
- // Delete the ?q= parameter just after borrow a book. see: WEBDEV-3979
192
- currentParams.delete('q');
193
-
194
224
  if (extraParam) {
195
225
  // append extraParam key-value in currentParams
196
226
  const extraParams = new URLSearchParams(extraParam);
@@ -226,12 +256,4 @@ export default class ActionsHandler extends LitElement {
226
256
  cookie += '; path=/; domain=.archive.org;';
227
257
  document.cookie = cookie;
228
258
  }
229
-
230
- toggleLoader(context) {
231
- this.dispatchEvent(
232
- new CustomEvent('toggle-loader', {
233
- detail: context,
234
- })
235
- );
236
- }
237
259
  }
@@ -7,6 +7,7 @@ import './components/collapsible-action-group.js';
7
7
  import './components/book-title-bar.js';
8
8
  import './components/text-group.js';
9
9
  import './components/info-icon.js';
10
+ import './components/dialog-alert.js';
10
11
 
11
12
  import GetLendingActions from './core/services/get-lending-actions.js';
12
13
  import { mobileContainerWidth } from './core/config/constants.js';
@@ -23,6 +24,8 @@ export default class IABookActions extends LitElement {
23
24
  barType: { type: String },
24
25
  sharedObserver: { attribute: false },
25
26
  disable: { type: Boolean },
27
+ dialogVisible: { type: Boolean },
28
+ bookHasBrowsed: { type: Boolean },
26
29
  };
27
30
  }
28
31
 
@@ -41,7 +44,9 @@ export default class IABookActions extends LitElement {
41
44
  this.primaryColor = 'primary';
42
45
  this.secondaryActions = [];
43
46
  this.lendingOptions = {};
44
- this.disable = false;
47
+ this.disableActionGroup = false;
48
+ this.dialogVisible = false;
49
+ this.bookHasBrowsed = false;
45
50
  }
46
51
 
47
52
  disconnectedCallback() {
@@ -141,10 +146,12 @@ export default class IABookActions extends LitElement {
141
146
  return action != null;
142
147
  });
143
148
 
144
- if (
149
+ this.bookHasBrowsed =
145
150
  this.lendingStatus.user_has_browsed &&
146
- !this.lendingStatus.browseHasExpired
147
- ) {
151
+ !this.lendingStatus.browsingExpired;
152
+ if (this.bookHasBrowsed) {
153
+ // start timer for 1-hour borrowed book.
154
+ // when 1 hour is completed, we shows browse-again button
148
155
  this.startBrowseTimer();
149
156
  }
150
157
  }
@@ -154,9 +161,21 @@ export default class IABookActions extends LitElement {
154
161
  <section class="lending-wrapper">
155
162
  ${this.barType === 'title' ? this.bookTitleBar : this.bookActionBar}
156
163
  </section>
164
+
165
+ <show-dialog
166
+ .title=${this.dialogTitle}
167
+ .body=${this.dialogBody}
168
+ .actions=${this.dialogActions}
169
+ ?opened="${this.dialogVisible}"
170
+ @dialogAlertClose="${() => this.closeDialogAlert()}"
171
+ ></show-dialog>
157
172
  `;
158
173
  }
159
174
 
175
+ closeDialogAlert() {
176
+ this.dialogVisible = false;
177
+ }
178
+
160
179
  get bookTitleBar() {
161
180
  return html`<book-title-bar
162
181
  .identifier=${this.identifier}
@@ -174,24 +193,61 @@ export default class IABookActions extends LitElement {
174
193
  .secondaryActions=${this.secondaryActions}
175
194
  .width=${this.width}
176
195
  .hasAdminAccess=${this.hasAdminAccess}
177
- .disabled=${this.disable}
178
- @toggle-loader=${this.toggleLoader}
196
+ .disabled=${this.disableActionGroup}
197
+ .bookHasBrowsed=${this.bookHasBrowsed}
198
+ @lendingActionError=${this.handleLendingActionError}
179
199
  >
180
200
  </collapsible-action-group>
181
201
  ${this.textGroupTemplate} ${this.infoIconTemplate}
182
202
  `;
183
203
  }
184
204
 
185
- toggleLoader(e) {
186
- // if activity loader is disabled and action is browse or borrow,
187
- // just change the lendingStatus to update action buttons
188
- if (this.disable && ['browse_book', 'borrow_book'].includes(e.detail)) {
189
- console.log('lendingStatus is changed to redraw action-buttons');
190
- const currStatus = { ...this.lendingStatus, available_to_browse: false };
205
+ handleLendingActionError(e) {
206
+ // toggle activity loader
207
+ this.disableActionGroup = !this.disableActionGroup;
208
+
209
+ const context = e?.detail?.context;
210
+ const errorMsg = e?.detail?.data?.error;
211
+
212
+ // early exist if you don't have errorMsg
213
+ if (!errorMsg) return;
214
+
215
+ this.initializeDialogAlert(context, errorMsg);
216
+
217
+ // update action bar state if book is not available to browse or borrow.
218
+ if (errorMsg && errorMsg.match(/not available to borrow/gm)) {
219
+ let currStatus = this.lendingStatus;
220
+ if (context === 'browse_book') {
221
+ currStatus = {
222
+ ...this.lendingStatus,
223
+ available_to_browse: false,
224
+ };
225
+ } else if (context === 'borrow_book') {
226
+ currStatus = {
227
+ ...this.lendingStatus,
228
+ available_to_borrow: false,
229
+ };
230
+ }
191
231
  this.lendingStatus = currStatus;
192
232
  }
233
+ }
193
234
 
194
- this.disable = !this.disable;
235
+ /* set props value to show dialog alert */
236
+ initializeDialogAlert(context, errorMsg) {
237
+ this.dialogVisible = true;
238
+ this.dialogTitle = 'Sorry!';
239
+ this.dialogBody = errorMsg;
240
+ this.dialogActions = [];
241
+
242
+ if (context === 'create_token') {
243
+ this.dialogActions = [
244
+ {
245
+ className: 'ia-button primary',
246
+ text: 'Back to Item Details',
247
+ href: `/details/${this.lendingStatus.bookUrl}`,
248
+ },
249
+ ];
250
+ }
195
251
  }
196
252
 
197
253
  get iconClass() {
@@ -106,6 +106,65 @@ 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
+ console.log(el.lendingStatus);
134
+ // removed available_to_browse from lending bar
135
+ expect(el.lendingStatus.available_to_browse).to.be.false;
136
+ expect(el.primaryActions[0].text).to.equal('Borrow for 14 days');
137
+ });
138
+
139
+ it('Update available_to_borrow key when book is not available to borrow', async () => {
140
+ const el = await fixture(
141
+ container({
142
+ userid: '@user1',
143
+ lendingStatus: {
144
+ is_lendable: true,
145
+ available_to_browse: true,
146
+ available_to_borrow: true,
147
+ },
148
+ })
149
+ );
150
+
151
+ const errorEvent = new Event('lendingActionError');
152
+ el.addEventListener('lendingActionError', () => {
153
+ el.handleLendingActionError({
154
+ detail: {
155
+ context: 'borrow_book',
156
+ data: { error: 'not available to borrow' },
157
+ },
158
+ });
159
+ });
160
+ el.dispatchEvent(errorEvent);
161
+ await el.updateComplete;
162
+
163
+ // removed available_to_borrow from lending bar
164
+ expect(el.lendingStatus.available_to_borrow).to.be.false;
165
+ expect(el.primaryActions[0].text).to.equal('Borrow for 1 hour');
166
+ });
167
+
109
168
  it('Check action for borrowable book without user', async () => {
110
169
  const el = await fixture(
111
170
  container({