@internetarchive/ia-book-actions 0.0.6-4 → 0.0.6-8

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
 
@@ -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-4",
3
+ "version": "0.0.6-8",
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);
@@ -8,17 +8,14 @@ export default function ActionsHandlerService(options) {
8
8
  const option = {
9
9
  action: null,
10
10
  identifier: '',
11
- data: {},
12
11
  success() {},
13
12
  error() {},
14
- loader: true,
15
- type: 'POST',
16
13
  ...options,
17
14
  };
18
15
 
19
16
  let baseHost = '';
20
17
  if (window.location.pathname === '/demo/') {
21
- baseHost = `/demo/1`;
18
+ baseHost = `/demo/`;
22
19
  } else {
23
20
  baseHost = `/services/loans/loan`;
24
21
  }
@@ -32,8 +29,20 @@ export default function ActionsHandlerService(options) {
32
29
  body: formData,
33
30
  })
34
31
  .then(response => {
32
+ // test changes, (won't affect you)
35
33
  if (baseHost == '/demo/1' || baseHost == '/demo/') {
36
- return {"error":"Unexpected error.\nPlease email this link to openlibrary@archive.org with the subject: Unexpected error. Thank you. \nMessage:UNAVAILABLE"};
34
+ if (option.action == 'create_token') {
35
+ // return {'success': false, 'error': 'loan token not found. please try again later.',};
36
+ return {
37
+ success: true,
38
+ token: '1640945898-f77a0cb615190798f1faf2dafe277b2e',
39
+ };
40
+ }
41
+
42
+ return {
43
+ error:
44
+ 'This book is not available to borrow at this time. Please try again later.',
45
+ };
37
46
  }
38
47
 
39
48
  // The response is a Response instance.
@@ -42,7 +51,6 @@ export default function ActionsHandlerService(options) {
42
51
  })
43
52
  .then(data => {
44
53
  // `data` is the parsed version of the JSON returned from the above endpoint.
45
- console.log('actual reponse for endpoint', data);
46
54
  if (!data.error) {
47
55
  option?.success(data);
48
56
  } 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,11 +86,27 @@ 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.toggleActionBarState(context);
109
+ this.ActionError(context);
92
110
 
93
111
  ActionsHandlerService({
94
112
  action: context,
@@ -97,16 +115,14 @@ export default class ActionsHandler extends LitElement {
97
115
  this.handleReadItNow();
98
116
  },
99
117
  error: data => {
100
- console.log(data);
101
- alert(data.error);
102
- this.toggleActionBarState(context, data);
118
+ this.ActionError(context, data);
103
119
  },
104
120
  });
105
121
  }
106
122
 
107
123
  handleReturnIt() {
108
124
  const context = 'return_loan';
109
- this.toggleActionBarState(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.toggleActionBarState(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.toggleActionBarState(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.toggleActionBarState(context, data);
151
+ this.ActionError(context, data);
140
152
  },
141
153
  });
142
154
  }
143
155
 
144
156
  handleReserveIt() {
145
157
  const context = 'join_waitlist';
146
- this.toggleActionBarState(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.toggleActionBarState(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.toggleActionBarState(context);
174
+ this.ActionError(context);
165
175
 
166
176
  ActionsHandlerService({
167
177
  action: context,
@@ -170,13 +180,30 @@ 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.toggleActionBarState(context);
183
+ this.ActionError(context, data);
184
+ },
185
+ });
186
+ }
187
+
188
+ handleLoanTokenPoller() {
189
+ const context = 'create_token';
190
+ ActionsHandlerService({
191
+ identifier: this.identifier,
192
+ action: context,
193
+ error: data => {
194
+ this.ActionError(context, data);
176
195
  },
177
196
  });
178
197
  }
179
198
 
199
+ ActionError(context, data = {}) {
200
+ this.dispatchEvent(
201
+ new CustomEvent('lendingActionError', {
202
+ detail: { context, data },
203
+ })
204
+ );
205
+ }
206
+
180
207
  handleLoginOk() {
181
208
  /* eslint class-methods-use-this: "off" */
182
209
  const target = `/account/login?referer=${encodeURIComponent(
@@ -188,9 +215,6 @@ export default class ActionsHandler extends LitElement {
188
215
  handleReadItNow(extraParam) {
189
216
  const currentParams = new URLSearchParams(window.location.search);
190
217
 
191
- // Delete the ?q= parameter just after borrow a book. see: WEBDEV-3979
192
- currentParams.delete('q');
193
-
194
218
  if (extraParam) {
195
219
  // append extraParam key-value in currentParams
196
220
  const extraParams = new URLSearchParams(extraParam);
@@ -226,12 +250,4 @@ export default class ActionsHandler extends LitElement {
226
250
  cookie += '; path=/; domain=.archive.org;';
227
251
  document.cookie = cookie;
228
252
  }
229
-
230
- toggleActionBarState(context, data = {}) {
231
- this.dispatchEvent(
232
- new CustomEvent('toggle-action-bar-state', {
233
- detail: { context, data }
234
- })
235
- );
236
- }
237
253
  }
@@ -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,27 +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-action-bar-state=${this.toggleActionBarState}
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
- toggleActionBarState(e) {
186
- console.log(e)
187
-
188
- // update action bar states is book is not available to browse or borrow.
189
- if (e?.detail?.data?.error) {
190
- const errorMsg = e.detail.data.error;
191
- if (errorMsg.match(/(^|\W)UNAVAILABLE($|\W)/)) {
192
- const currStatus = { ...this.lendingStatus, available_to_browse: false };
193
- this.lendingStatus = currStatus;
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
+ };
194
230
  }
231
+ this.lendingStatus = currStatus;
195
232
  }
233
+ }
196
234
 
197
- 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
+ }
198
251
  }
199
252
 
200
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({