cozy-client 60.30.6 → 60.32.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/dist/CozyClient.js +1 -1
- package/dist/models/__fixtures__/io.cozy.banners.json +807 -0
- package/dist/models/banner.js +399 -0
- package/dist/models/banner.spec.js +381 -0
- package/dist/models/index.js +5 -1
- package/package.json +2 -2
- package/types/models/banner.d.ts +87 -0
- package/types/models/index.d.ts +2 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
exports.dismiss = exports.getActiveBanner = exports.getActiveBanners = exports.BANNERS_DOCTYPE = void 0;
|
|
9
|
+
|
|
10
|
+
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
|
|
11
|
+
|
|
12
|
+
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
|
|
13
|
+
|
|
14
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
15
|
+
|
|
16
|
+
var _dsl = require("../queries/dsl");
|
|
17
|
+
|
|
18
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
|
19
|
+
|
|
20
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
21
|
+
|
|
22
|
+
var BANNERS_DOCTYPE = 'io.cozy.banners';
|
|
23
|
+
exports.BANNERS_DOCTYPE = BANNERS_DOCTYPE;
|
|
24
|
+
var KNOWN_SEVERITIES = ['info', 'warning', 'error'];
|
|
25
|
+
var KNOWN_SURFACES = ['banner', 'modal'];
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} BannerCta
|
|
28
|
+
* @property {string} label - The label, plain text
|
|
29
|
+
* @property {string} url - Where the label points
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} Banner An io.cozy.banners document
|
|
34
|
+
* @property {string} [_id] - Identifier of the document
|
|
35
|
+
* @property {string} bannerId - Identifier of the banner itself
|
|
36
|
+
* @property {string} category - What the banner is about
|
|
37
|
+
* @property {'info'|'warning'|'error'} severity - How loudly to render it
|
|
38
|
+
* @property {'banner'|'modal'} surface - Where to render it
|
|
39
|
+
* @property {string} [title] - Optional heading, used on the modal surface
|
|
40
|
+
* @property {string} text - The message, already localized
|
|
41
|
+
* @property {string} lang - BCP 47 tag of the language `text` and `cta.label` are in
|
|
42
|
+
* @property {BannerCta} [cta] - Optional call to action
|
|
43
|
+
* @property {BannerCta} [secondaryCta] - Optional lesser action, needs a cta
|
|
44
|
+
* @property {boolean} dismissible - Whether the client offers a dismiss control
|
|
45
|
+
* @property {string} [dismissedAt] - When the user dismissed it
|
|
46
|
+
* @property {number} priority - Sort order, highest first
|
|
47
|
+
* @property {string} startsAt - Start of the validity window, inclusive
|
|
48
|
+
* @property {string} [endsAt] - End of the validity window, exclusive
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The doctype writes an ISO 8601 timestamp with an explicit offset, and nothing
|
|
53
|
+
* else parses. `Date.parse` on its own falls back to the engine's legacy parser
|
|
54
|
+
* for anything it does not recognise, so a malformed bound would resolve to a
|
|
55
|
+
* real instant in one browser and to NaN in another, and the shared contract
|
|
56
|
+
* vectors could no longer say what every client displays.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
60
|
+
/** A bound that is absent leaves that end of the window open, unlike an unparseable one. */
|
|
61
|
+
|
|
62
|
+
var toTime = function toTime(value) {
|
|
63
|
+
if (value == null) return null;
|
|
64
|
+
if (typeof value !== 'string' || !ISO_TIMESTAMP.test(value)) return NaN;
|
|
65
|
+
return Date.parse(value);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
var isDismissed = function isDismissed(banner) {
|
|
69
|
+
return (banner === null || banner === void 0 ? void 0 : banner.dismissedAt) != null;
|
|
70
|
+
};
|
|
71
|
+
/** The window is inclusive on startsAt, exclusive on endsAt, open when absent. */
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
var isInWindow = function isInWindow(banner, now) {
|
|
75
|
+
var startsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.startsAt);
|
|
76
|
+
var endsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.endsAt);
|
|
77
|
+
|
|
78
|
+
if (startsAt === null || Number.isNaN(startsAt) || Number.isNaN(endsAt)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
var at = now.getTime();
|
|
83
|
+
if (at < startsAt) return false;
|
|
84
|
+
return endsAt === null || at < endsAt;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Highest priority first, then bannerId, compared code unit by code unit so
|
|
88
|
+
* that every client orders equal priorities the same way. `localeCompare`
|
|
89
|
+
* would depend on the runtime locale.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
var rankOf = function rankOf(banner) {
|
|
94
|
+
return Number.isFinite(banner === null || banner === void 0 ? void 0 : banner.priority) ? banner.priority : 0;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
var byPriority = function byPriority(a, b) {
|
|
98
|
+
var byRank = rankOf(b) - rankOf(a);
|
|
99
|
+
if (byRank !== 0) return byRank;
|
|
100
|
+
if (a.bannerId < b.bannerId) return -1;
|
|
101
|
+
if (a.bannerId > b.bannerId) return 1;
|
|
102
|
+
return 0;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The doctype specifies an absolute `https://` URL, so anything else is dropped
|
|
106
|
+
* rather than handed to a client that would put it in an `href`: the documents
|
|
107
|
+
* live in a database the applications can write, and a `javascript:` URL there
|
|
108
|
+
* would run in the origin of whichever application rendered the banner.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
var isSafeCta = function isSafeCta(cta) {
|
|
113
|
+
return typeof (cta === null || cta === void 0 ? void 0 : cta.url) === 'string' && /^https:\/\//i.test(cta.url);
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The doctype allows a new severity or surface without a version bump, so a
|
|
117
|
+
* value this client does not know is rendered rather than dropped.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
var withFallbacks = function withFallbacks(banner) {
|
|
122
|
+
var cta = isSafeCta(banner.cta) ? banner.cta : undefined;
|
|
123
|
+
return _objectSpread(_objectSpread({}, banner), {}, {
|
|
124
|
+
severity: KNOWN_SEVERITIES.includes(banner.severity) ? banner.severity : 'warning',
|
|
125
|
+
surface: KNOWN_SURFACES.includes(banner.surface) ? banner.surface : 'banner',
|
|
126
|
+
cta: cta,
|
|
127
|
+
// The secondary action is the lesser one, so it is never promoted to the
|
|
128
|
+
// only thing on offer: without a primary it is dropped rather than shown.
|
|
129
|
+
secondaryCta: cta && isSafeCta(banner.secondaryCta) ? banner.secondaryCta : undefined
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* `client.query` resolves undefined instead of rejecting when the client has an
|
|
134
|
+
* `onError` callback, which would otherwise read as "no banners apply".
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
var readData = function readData(response) {
|
|
139
|
+
if (response === null || response === undefined) {
|
|
140
|
+
throw new Error("The ".concat(BANNERS_DOCTYPE, " query resolved nothing"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return response.data;
|
|
144
|
+
};
|
|
145
|
+
/** The stack link answers a missing document with null, the pouch link with []. */
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
var readOne = function readOne(response) {
|
|
149
|
+
var data = readData(response);
|
|
150
|
+
var doc = Array.isArray(data) ? data[0] : data;
|
|
151
|
+
return doc !== null && doc !== void 0 && doc._id ? doc : null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
var isConflict = function isConflict(error) {
|
|
155
|
+
var _error$message;
|
|
156
|
+
|
|
157
|
+
return (error === null || error === void 0 ? void 0 : error.status) === 409 || /Document update conflict/.test((_error$message = error === null || error === void 0 ? void 0 : error.message) !== null && _error$message !== void 0 ? _error$message : '');
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* The banners to display, in order.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} client - A CozyClient instance
|
|
163
|
+
* @param {object} [options] - Options
|
|
164
|
+
* @param {Date} [options.now] - The current time
|
|
165
|
+
* @returns {Promise<Banner[]>} The banners to render
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
var getActiveBanners = /*#__PURE__*/function () {
|
|
170
|
+
var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
|
|
171
|
+
var _ref2,
|
|
172
|
+
_ref2$now,
|
|
173
|
+
now,
|
|
174
|
+
data,
|
|
175
|
+
_args = arguments;
|
|
176
|
+
|
|
177
|
+
return _regenerator.default.wrap(function _callee$(_context) {
|
|
178
|
+
while (1) {
|
|
179
|
+
switch (_context.prev = _context.next) {
|
|
180
|
+
case 0:
|
|
181
|
+
_ref2 = _args.length > 1 && _args[1] !== undefined ? _args[1] : {}, _ref2$now = _ref2.now, now = _ref2$now === void 0 ? new Date() : _ref2$now;
|
|
182
|
+
_context.t0 = readData;
|
|
183
|
+
_context.next = 4;
|
|
184
|
+
return client.query((0, _dsl.Q)(BANNERS_DOCTYPE).UNSAFE_noLimit(), {
|
|
185
|
+
as: "".concat(BANNERS_DOCTYPE, "/all")
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
case 4:
|
|
189
|
+
_context.t1 = _context.sent;
|
|
190
|
+
data = (0, _context.t0)(_context.t1);
|
|
191
|
+
return _context.abrupt("return", (Array.isArray(data) ? data : []).filter(function (banner) {
|
|
192
|
+
return !isDismissed(banner) && isInWindow(banner, now);
|
|
193
|
+
}).sort(byPriority).map(withFallbacks));
|
|
194
|
+
|
|
195
|
+
case 7:
|
|
196
|
+
case "end":
|
|
197
|
+
return _context.stop();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}, _callee);
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
return function getActiveBanners(_x) {
|
|
204
|
+
return _ref.apply(this, arguments);
|
|
205
|
+
};
|
|
206
|
+
}();
|
|
207
|
+
/**
|
|
208
|
+
* The single banner to display, for a surface that shows one at a time.
|
|
209
|
+
*
|
|
210
|
+
* A native client rendering one slot reads this rather than taking the head of
|
|
211
|
+
* the list itself, so the ordering rules stay in one place.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} client - A CozyClient instance
|
|
214
|
+
* @param {object} [options] - Options
|
|
215
|
+
* @param {Date} [options.now] - The current time
|
|
216
|
+
* @returns {Promise<Banner|null>} The highest priority banner, or null
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
exports.getActiveBanners = getActiveBanners;
|
|
221
|
+
|
|
222
|
+
var getActiveBanner = /*#__PURE__*/function () {
|
|
223
|
+
var _ref3 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(client, options) {
|
|
224
|
+
var _yield$getActiveBanne;
|
|
225
|
+
|
|
226
|
+
return _regenerator.default.wrap(function _callee2$(_context2) {
|
|
227
|
+
while (1) {
|
|
228
|
+
switch (_context2.prev = _context2.next) {
|
|
229
|
+
case 0:
|
|
230
|
+
_context2.next = 2;
|
|
231
|
+
return getActiveBanners(client, options);
|
|
232
|
+
|
|
233
|
+
case 2:
|
|
234
|
+
_context2.t1 = _yield$getActiveBanne = _context2.sent[0];
|
|
235
|
+
_context2.t0 = _context2.t1 !== null;
|
|
236
|
+
|
|
237
|
+
if (!_context2.t0) {
|
|
238
|
+
_context2.next = 6;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_context2.t0 = _yield$getActiveBanne !== void 0;
|
|
243
|
+
|
|
244
|
+
case 6:
|
|
245
|
+
if (!_context2.t0) {
|
|
246
|
+
_context2.next = 10;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_context2.t2 = _yield$getActiveBanne;
|
|
251
|
+
_context2.next = 11;
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case 10:
|
|
255
|
+
_context2.t2 = null;
|
|
256
|
+
|
|
257
|
+
case 11:
|
|
258
|
+
return _context2.abrupt("return", _context2.t2);
|
|
259
|
+
|
|
260
|
+
case 12:
|
|
261
|
+
case "end":
|
|
262
|
+
return _context2.stop();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}, _callee2);
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
return function getActiveBanner(_x2, _x3) {
|
|
269
|
+
return _ref3.apply(this, arguments);
|
|
270
|
+
};
|
|
271
|
+
}();
|
|
272
|
+
/**
|
|
273
|
+
* Records a dismissal on the stored document, retrying once on a conflict.
|
|
274
|
+
*
|
|
275
|
+
* The stored document is written back rather than the one the caller holds,
|
|
276
|
+
* whose severity and surface have had the fallbacks applied.
|
|
277
|
+
*
|
|
278
|
+
* @param {object} client - A CozyClient instance
|
|
279
|
+
* @param {Banner} banner - The banner to dismiss
|
|
280
|
+
* @param {object} [options] - Options
|
|
281
|
+
* @param {string} [options.dismissedAt] - The timestamp to record
|
|
282
|
+
* @returns {Promise<{data: Banner|null}>} The dismissed document, null when gone
|
|
283
|
+
*/
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
exports.getActiveBanner = getActiveBanner;
|
|
287
|
+
|
|
288
|
+
var dismiss = /*#__PURE__*/function () {
|
|
289
|
+
var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(client, banner) {
|
|
290
|
+
var _ref5,
|
|
291
|
+
dismissedAt,
|
|
292
|
+
at,
|
|
293
|
+
write,
|
|
294
|
+
_args4 = arguments;
|
|
295
|
+
|
|
296
|
+
return _regenerator.default.wrap(function _callee4$(_context4) {
|
|
297
|
+
while (1) {
|
|
298
|
+
switch (_context4.prev = _context4.next) {
|
|
299
|
+
case 0:
|
|
300
|
+
_ref5 = _args4.length > 2 && _args4[2] !== undefined ? _args4[2] : {}, dismissedAt = _ref5.dismissedAt;
|
|
301
|
+
|
|
302
|
+
if (banner !== null && banner !== void 0 && banner._id) {
|
|
303
|
+
_context4.next = 3;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
throw new Error('A banner needs an _id to be dismissed');
|
|
308
|
+
|
|
309
|
+
case 3:
|
|
310
|
+
at = dismissedAt !== null && dismissedAt !== void 0 ? dismissedAt : new Date().toISOString();
|
|
311
|
+
|
|
312
|
+
write = /*#__PURE__*/function () {
|
|
313
|
+
var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
|
|
314
|
+
var stored;
|
|
315
|
+
return _regenerator.default.wrap(function _callee3$(_context3) {
|
|
316
|
+
while (1) {
|
|
317
|
+
switch (_context3.prev = _context3.next) {
|
|
318
|
+
case 0:
|
|
319
|
+
_context3.t0 = readOne;
|
|
320
|
+
_context3.next = 3;
|
|
321
|
+
return client.query((0, _dsl.Q)(BANNERS_DOCTYPE).getById(banner._id));
|
|
322
|
+
|
|
323
|
+
case 3:
|
|
324
|
+
_context3.t1 = _context3.sent;
|
|
325
|
+
stored = (0, _context3.t0)(_context3.t1);
|
|
326
|
+
|
|
327
|
+
if (stored) {
|
|
328
|
+
_context3.next = 7;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return _context3.abrupt("return", {
|
|
333
|
+
data: null
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
case 7:
|
|
337
|
+
if (!isDismissed(stored)) {
|
|
338
|
+
_context3.next = 9;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return _context3.abrupt("return", {
|
|
343
|
+
data: stored
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
case 9:
|
|
347
|
+
return _context3.abrupt("return", client.save(_objectSpread(_objectSpread({}, stored), {}, {
|
|
348
|
+
_type: BANNERS_DOCTYPE,
|
|
349
|
+
dismissedAt: at
|
|
350
|
+
})));
|
|
351
|
+
|
|
352
|
+
case 10:
|
|
353
|
+
case "end":
|
|
354
|
+
return _context3.stop();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}, _callee3);
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
return function write() {
|
|
361
|
+
return _ref6.apply(this, arguments);
|
|
362
|
+
};
|
|
363
|
+
}();
|
|
364
|
+
|
|
365
|
+
_context4.prev = 5;
|
|
366
|
+
_context4.next = 8;
|
|
367
|
+
return write();
|
|
368
|
+
|
|
369
|
+
case 8:
|
|
370
|
+
return _context4.abrupt("return", _context4.sent);
|
|
371
|
+
|
|
372
|
+
case 11:
|
|
373
|
+
_context4.prev = 11;
|
|
374
|
+
_context4.t0 = _context4["catch"](5);
|
|
375
|
+
|
|
376
|
+
if (isConflict(_context4.t0)) {
|
|
377
|
+
_context4.next = 15;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
throw _context4.t0;
|
|
382
|
+
|
|
383
|
+
case 15:
|
|
384
|
+
return _context4.abrupt("return", write());
|
|
385
|
+
|
|
386
|
+
case 16:
|
|
387
|
+
case "end":
|
|
388
|
+
return _context4.stop();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}, _callee4, null, [[5, 11]]);
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
return function dismiss(_x4, _x5) {
|
|
395
|
+
return _ref4.apply(this, arguments);
|
|
396
|
+
};
|
|
397
|
+
}();
|
|
398
|
+
|
|
399
|
+
exports.dismiss = dismiss;
|